Merge remote main into local
This commit is contained in:
commit
f2841bdd74
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -477,6 +477,9 @@ jobs:
|
||||
eas-version: latest
|
||||
packager: pnpm
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
# Its default caches go to the GitHub cache service; keep them on the runner.
|
||||
eas-cache: false
|
||||
expo-cache: false
|
||||
- name: Install dependencies
|
||||
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
createMediaRuntime,
|
||||
createMessagingContextLoader,
|
||||
createMessagingTeamChatSender,
|
||||
createPostgresReconciliationLeadership,
|
||||
createProfileConsolidationScheduler,
|
||||
createRunExecutor,
|
||||
createRunSandbox,
|
||||
@ -360,7 +361,7 @@ export async function createApp(
|
||||
"https://172.*:*",
|
||||
"http://*.local:*",
|
||||
"https://*.local:*",
|
||||
...(process.env.EXTRA_TRUSTED_ORIGINS?.split(",").map((s) => s.trim()) ?? []),
|
||||
...(env.extraTrustedOrigins ?? []),
|
||||
],
|
||||
beforeDeleteUser: async (userId) => {
|
||||
const bots = await prisma.bot.findMany({
|
||||
@ -474,26 +475,30 @@ export async function createApp(
|
||||
cloudAgent,
|
||||
media,
|
||||
});
|
||||
let graphileWorkerHost: GraphileJobWorkerHost | undefined;
|
||||
if (inMemoryJobs) {
|
||||
await inMemoryJobs.start(jobHandlers);
|
||||
} else if (jobPool) {
|
||||
const graphileWorkerHost = new GraphileJobWorkerHost(jobPool, {
|
||||
concurrency: 4,
|
||||
graphileWorkerHost = new GraphileJobWorkerHost(jobPool, {
|
||||
concurrency: parsePositiveInteger(process.env.GRAPHILE_WORKER_CONCURRENCY, 4),
|
||||
pollInterval: 500,
|
||||
});
|
||||
await graphileWorkerHost.start(jobHandlers);
|
||||
}
|
||||
const reconciler = inMemoryJobs
|
||||
? createJobReconciler({
|
||||
prisma,
|
||||
jobs,
|
||||
reconcileCloudAgents: () => reconcileCloudAgents({ prisma, jobs, cloudAgent }),
|
||||
reconcileComputerUpdates: () => reconcileComputerUpdates({ prisma, jobs }),
|
||||
reconcileProfileConsolidation: createProfileConsolidationScheduler({ prisma, jobs }),
|
||||
reconcileDelegatedTasks: () => reconcileDelegatedTasks({ prisma, jobs, events }),
|
||||
reconcileMediaJobs: () => media.reconcile(),
|
||||
})
|
||||
: undefined;
|
||||
const reconciler =
|
||||
inMemoryJobs || !prismaOverride
|
||||
? createJobReconciler({
|
||||
prisma,
|
||||
jobs,
|
||||
events,
|
||||
leadership: jobPool ? createPostgresReconciliationLeadership(jobPool) : undefined,
|
||||
reconcileCloudAgents: () => reconcileCloudAgents({ prisma, jobs, cloudAgent }),
|
||||
reconcileComputerUpdates: () => reconcileComputerUpdates({ prisma, jobs }),
|
||||
reconcileProfileConsolidation: createProfileConsolidationScheduler({ prisma, jobs }),
|
||||
reconcileDelegatedTasks: () => reconcileDelegatedTasks({ prisma, jobs, events }),
|
||||
reconcileMediaJobs: () => media.reconcile(),
|
||||
})
|
||||
: undefined;
|
||||
reconciler?.start();
|
||||
|
||||
const router = createRouter({
|
||||
@ -612,7 +617,7 @@ export async function createApp(
|
||||
actor,
|
||||
signal: c.req.raw.signal,
|
||||
userAgent: c.req.header("user-agent"),
|
||||
webOrigin: requestWebOrigin(c.req.header("origin"), env.webOrigin),
|
||||
webOrigin: requestWebOrigin(c.req.header("origin"), env.webOrigin, env.extraTrustedOrigins),
|
||||
},
|
||||
});
|
||||
if (matched) return c.newResponse(response.body, response);
|
||||
@ -1006,6 +1011,7 @@ export async function createApp(
|
||||
await teamChatBridge?.stop();
|
||||
await email?.drain?.();
|
||||
await reconciler?.stop();
|
||||
await graphileWorkerHost?.stop();
|
||||
await jobs.close();
|
||||
await realtime.close();
|
||||
await connector.stop();
|
||||
@ -1021,6 +1027,7 @@ export async function createApp(
|
||||
function isTrustedOrigin(origin: string, env: AppEnv) {
|
||||
if (!origin) return true;
|
||||
if (origin === env.webOrigin || origin === env.apiUrl || origin === env.authUrl) return true;
|
||||
if (env.extraTrustedOrigins?.includes(origin)) return true;
|
||||
if (origin.startsWith("rakazo://") || origin.startsWith("exp://")) return true;
|
||||
try {
|
||||
const host = new URL(origin).hostname;
|
||||
|
||||
@ -20,6 +20,7 @@ export interface AppEnv {
|
||||
authSecret: string;
|
||||
authUrl: string;
|
||||
webOrigin: string;
|
||||
extraTrustedOrigins: string[];
|
||||
privacyPolicyUrl?: string;
|
||||
apiUrl: string;
|
||||
apiHost: string;
|
||||
@ -109,6 +110,10 @@ export function loadEnv(source: NodeJS.ProcessEnv = process.env): AppEnv {
|
||||
authSecret,
|
||||
authUrl: source.BETTER_AUTH_URL ?? source.WEB_ORIGIN ?? "http://127.0.0.1:5173",
|
||||
webOrigin: source.WEB_ORIGIN ?? "http://127.0.0.1:5173",
|
||||
extraTrustedOrigins: (source.EXTRA_TRUSTED_ORIGINS ?? "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
privacyPolicyUrl: optional(source.PRIVACY_POLICY_URL),
|
||||
apiUrl: source.API_URL ?? "http://127.0.0.1:3100",
|
||||
apiHost: source.API_HOST ?? "127.0.0.1",
|
||||
|
||||
@ -135,6 +135,7 @@ import {
|
||||
createProjectGoalRepos,
|
||||
createRepos,
|
||||
createSpaceForMember,
|
||||
DuplicateSpaceNameError,
|
||||
deleteEmptySpaceForMember,
|
||||
deleteUnreferencedCredentialSecret,
|
||||
ensureMainConversation,
|
||||
@ -270,7 +271,10 @@ import {
|
||||
followUpGroupThreadMessage,
|
||||
groupDefaultBotId,
|
||||
reactToThreadMessage,
|
||||
removeQueuedMessage,
|
||||
resolveThreadTarget,
|
||||
resumeThreadRuns,
|
||||
sendQueuedThreadMessagesNow,
|
||||
sendThreadMessage,
|
||||
setThreadUnreadState,
|
||||
stopThreadRuns,
|
||||
@ -783,15 +787,26 @@ export function createRouter(deps: RouterDeps) {
|
||||
const goal =
|
||||
input.goal?.trim() || (workspacePath ? await readProjectSummary(workspacePath) : null);
|
||||
if (goal) {
|
||||
await deps.prisma.memoryDocument.create({
|
||||
data: {
|
||||
spaceId: space.id,
|
||||
userId: context.actor.userId,
|
||||
scope: "user",
|
||||
path: "PROJECT_GOAL.md",
|
||||
content: goal,
|
||||
},
|
||||
const existingGoal = await deps.prisma.memoryDocument.findFirst({
|
||||
where: { spaceId: space.id, scope: "user", path: "PROJECT_GOAL.md" },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existingGoal) {
|
||||
await deps.prisma.memoryDocument.update({
|
||||
where: { id: existingGoal.id },
|
||||
data: { content: goal },
|
||||
});
|
||||
} else {
|
||||
await deps.prisma.memoryDocument.create({
|
||||
data: {
|
||||
spaceId: space.id,
|
||||
userId: context.actor.userId,
|
||||
scope: "user",
|
||||
path: "PROJECT_GOAL.md",
|
||||
content: goal,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// A project starts as a conversation with its own main agent.
|
||||
await ensureMainConversation(deps.prisma, context.actor.userId, space.id);
|
||||
@ -873,7 +888,7 @@ export function createRouter(deps: RouterDeps) {
|
||||
throw new ORPCError("NOT_FOUND", { message: error.message });
|
||||
}
|
||||
if (error instanceof IsolationError) throw new ORPCError("FORBIDDEN");
|
||||
if (error instanceof InvalidSpaceNameError) {
|
||||
if (error instanceof InvalidSpaceNameError || error instanceof DuplicateSpaceNameError) {
|
||||
throw new ORPCError("BAD_REQUEST", { message: error.message });
|
||||
}
|
||||
throw error;
|
||||
@ -2050,6 +2065,42 @@ export function createRouter(deps: RouterDeps) {
|
||||
await stopThreadRuns(deps, context.actor, target);
|
||||
return { ok: true as const };
|
||||
}),
|
||||
resume: authed.threads.resume.handler(async ({ context, input }) => {
|
||||
if ((await modelSetup(deps, context.actor)).needsModel) {
|
||||
throw new ORPCError("BAD_REQUEST", { message: "Connect a model to start a run." });
|
||||
}
|
||||
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
||||
if (target.kind === "bot") {
|
||||
await assertTeachingSendAllowed(deps.prisma, context.actor.spaceId, target.botId);
|
||||
}
|
||||
return resumeThreadRuns(deps, context.actor, target);
|
||||
}),
|
||||
sendNow: authed.threads.sendNow.handler(async ({ context, input }) => {
|
||||
if ((await modelSetup(deps, context.actor)).needsModel) {
|
||||
throw new ORPCError("BAD_REQUEST", { message: "Connect a model to start a run." });
|
||||
}
|
||||
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
||||
if (target.kind === "bot") {
|
||||
await assertTeachingSendAllowed(deps.prisma, context.actor.spaceId, target.botId);
|
||||
}
|
||||
return sendQueuedThreadMessagesNow(deps, context.actor, target, {
|
||||
messageId: input.messageId,
|
||||
});
|
||||
}),
|
||||
removeQueuedMessage: authed.threads.removeQueuedMessage.handler(
|
||||
async ({ context, input }) => {
|
||||
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
||||
if (target.kind === "bot") {
|
||||
await assertTeachingSendAllowed(deps.prisma, context.actor.spaceId, target.botId);
|
||||
}
|
||||
return removeQueuedMessage(
|
||||
{ prisma: deps.prisma, events: deps.events },
|
||||
context.actor,
|
||||
target,
|
||||
input.messageId,
|
||||
);
|
||||
},
|
||||
),
|
||||
clear: authed.threads.clear.handler(async ({ context, input }) => {
|
||||
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
||||
const contextBotId = target.kind === "bot" ? target.botId : groupDefaultBotId(target);
|
||||
@ -5623,13 +5674,13 @@ async function spaceNavigationDto(
|
||||
// The user's own main agent of a project is part of the project, not content to clear.
|
||||
deps.prisma.bot.findMany({
|
||||
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
|
||||
select: { spaceId: true },
|
||||
distinct: ["spaceId"],
|
||||
select: { spaceId: true, userId: true },
|
||||
distinct: ["spaceId", "userId"],
|
||||
}),
|
||||
deps.prisma.chatGroup.findMany({
|
||||
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
|
||||
select: { spaceId: true },
|
||||
distinct: ["spaceId"],
|
||||
select: { spaceId: true, userId: true },
|
||||
distinct: ["spaceId", "userId"],
|
||||
}),
|
||||
deps.prisma.memoryDocument.findMany({
|
||||
where: { spaceId: { in: spaceIds }, scope: "user", path: "PROJECT_GOAL.md" },
|
||||
@ -5640,6 +5691,11 @@ async function spaceNavigationDto(
|
||||
...contentBots.map((row) => row.spaceId),
|
||||
...contentGroups.map((row) => row.spaceId),
|
||||
]);
|
||||
const spacesBlockedByOtherMember = new Set(
|
||||
[...contentBots, ...contentGroups]
|
||||
.filter((row) => row.userId !== actor.userId)
|
||||
.map((row) => row.spaceId),
|
||||
);
|
||||
const goalBySpaceId = new Map(goalDocuments.map((doc) => [doc.spaceId, doc.content]));
|
||||
const currentMembership = memberships.find((membership) => membership.spaceId === actor.spaceId);
|
||||
if (!currentMembership) throw new IsolationError();
|
||||
@ -5676,7 +5732,7 @@ async function spaceNavigationDto(
|
||||
membership.role === "owner" &&
|
||||
!membership.space.isDefault &&
|
||||
memberships.length > 1 &&
|
||||
!spacesWithContent.has(membership.spaceId) &&
|
||||
!spacesBlockedByOtherMember.has(membership.spaceId) &&
|
||||
(membership.space.deletingAt === null || membership.space.deletingAt < staleClaimBefore),
|
||||
canEdit: membership.role === "owner",
|
||||
// A linked folder is on the owner's host; other members see neither its path nor remote.
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Actor, MessageBlock, SearchHit } from "@rakazo/contracts";
|
||||
import { extractLinksFromText, matchesSearchQuery, snippetAroundMatch } from "@rakazo/core";
|
||||
import { Prisma, type PrismaClient } from "@rakazo/db";
|
||||
import type { PrismaClient } from "@rakazo/db";
|
||||
import { Prisma } from "@rakazo/db";
|
||||
|
||||
const SEARCH_LIMIT = 25;
|
||||
/** Cap name matches so content hits (messages/files/links/routines) keep most of the budget. */
|
||||
@ -64,19 +65,65 @@ export async function querySpaceSearch(
|
||||
bucket.push(hit);
|
||||
}
|
||||
|
||||
const bots = await prisma.bot.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
archivedAt: null,
|
||||
OR: [
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ title: { contains: query, mode: "insensitive" } },
|
||||
{ description: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
take: CONVERSATION_HIT_LIMIT,
|
||||
});
|
||||
const [bots, groups, artifacts, groupArtifacts, routines] = await Promise.all([
|
||||
prisma.bot.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
archivedAt: null,
|
||||
OR: [
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ title: { contains: query, mode: "insensitive" } },
|
||||
{ description: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
take: CONVERSATION_HIT_LIMIT,
|
||||
}),
|
||||
prisma.chatGroup.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
},
|
||||
take: CONVERSATION_HIT_LIMIT,
|
||||
}),
|
||||
prisma.artifact.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
groupId: null,
|
||||
botId: { not: null },
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
bot: { archivedAt: null },
|
||||
},
|
||||
include: { bot: { select: { name: true } } },
|
||||
take: SEARCH_LIMIT,
|
||||
}),
|
||||
prisma.artifact.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
groupId: { not: null },
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
},
|
||||
include: { group: { select: { name: true } } },
|
||||
take: SEARCH_LIMIT,
|
||||
}),
|
||||
prisma.routine.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
OR: [
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ prompt: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
bot: { archivedAt: null },
|
||||
},
|
||||
include: { bot: { select: { name: true } } },
|
||||
take: SEARCH_LIMIT,
|
||||
}),
|
||||
]);
|
||||
|
||||
for (const bot of bots) {
|
||||
pushInto(
|
||||
conversationHits,
|
||||
@ -91,14 +138,6 @@ export async function querySpaceSearch(
|
||||
);
|
||||
}
|
||||
|
||||
const groups = await prisma.chatGroup.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
},
|
||||
take: CONVERSATION_HIT_LIMIT,
|
||||
});
|
||||
for (const group of groups) {
|
||||
pushInto(
|
||||
conversationHits,
|
||||
@ -114,27 +153,72 @@ export async function querySpaceSearch(
|
||||
}
|
||||
|
||||
const contentBudget = Math.max(0, SEARCH_LIMIT - conversationHits.length);
|
||||
const pattern = `%${query}%`;
|
||||
|
||||
const [botArtifactMessages, groupArtifactMessages, messageRows, groupMessageRows] =
|
||||
await Promise.all([
|
||||
findArtifactMessages(
|
||||
prisma,
|
||||
actor,
|
||||
artifacts.flatMap((artifact) =>
|
||||
artifact.botId && artifact.bot ? [{ id: artifact.id, targetId: artifact.botId }] : [],
|
||||
),
|
||||
"bot",
|
||||
),
|
||||
findArtifactMessages(
|
||||
prisma,
|
||||
actor,
|
||||
groupArtifacts.flatMap((artifact) =>
|
||||
artifact.groupId && artifact.group
|
||||
? [{ id: artifact.id, targetId: artifact.groupId }]
|
||||
: [],
|
||||
),
|
||||
"group",
|
||||
),
|
||||
prisma.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
threadId: string;
|
||||
seq: number;
|
||||
blocks: Prisma.JsonValue;
|
||||
botId: string;
|
||||
botName: string;
|
||||
}>
|
||||
>`
|
||||
SELECT m.id, m."threadId", m.seq, m.blocks, b.id AS "botId", b.name AS "botName"
|
||||
FROM messages m
|
||||
INNER JOIN threads t ON t.id = m."threadId"
|
||||
INNER JOIN bots b ON b.id = t."botId"
|
||||
WHERE t."spaceId" = ${actor.spaceId}
|
||||
AND t."userId" = ${actor.userId}
|
||||
AND b."archivedAt" IS NULL
|
||||
AND m.blocks::text ILIKE ${pattern}
|
||||
ORDER BY m."createdAt" DESC
|
||||
LIMIT ${SEARCH_LIMIT}
|
||||
`,
|
||||
prisma.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
threadId: string;
|
||||
seq: number;
|
||||
blocks: Prisma.JsonValue;
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
}>
|
||||
>`
|
||||
SELECT m.id, m."threadId", m.seq, m.blocks, g.id AS "groupId", g.name AS "groupName"
|
||||
FROM messages m
|
||||
INNER JOIN threads t ON t.id = m."threadId"
|
||||
INNER JOIN chat_groups g ON g.id = t."groupId"
|
||||
WHERE t."spaceId" = ${actor.spaceId}
|
||||
AND t."userId" = ${actor.userId}
|
||||
AND t."groupId" IS NOT NULL
|
||||
AND m.blocks::text ILIKE ${pattern}
|
||||
ORDER BY m."createdAt" DESC
|
||||
LIMIT ${SEARCH_LIMIT}
|
||||
`,
|
||||
]);
|
||||
|
||||
const artifacts = await prisma.artifact.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
groupId: null,
|
||||
botId: { not: null },
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
bot: { archivedAt: null },
|
||||
},
|
||||
include: { bot: { select: { name: true } } },
|
||||
take: SEARCH_LIMIT,
|
||||
});
|
||||
const botArtifactMessages = await findArtifactMessages(
|
||||
prisma,
|
||||
actor,
|
||||
artifacts.flatMap((artifact) =>
|
||||
artifact.botId && artifact.bot ? [{ id: artifact.id, targetId: artifact.botId }] : [],
|
||||
),
|
||||
"bot",
|
||||
);
|
||||
for (const artifact of artifacts) {
|
||||
if (!artifact.botId || !artifact.bot) continue;
|
||||
const message = botArtifactMessages.get(artifact.id);
|
||||
@ -155,24 +239,6 @@ export async function querySpaceSearch(
|
||||
);
|
||||
}
|
||||
|
||||
const groupArtifacts = await prisma.artifact.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
groupId: { not: null },
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
},
|
||||
include: { group: { select: { name: true } } },
|
||||
take: SEARCH_LIMIT,
|
||||
});
|
||||
const groupArtifactMessages = await findArtifactMessages(
|
||||
prisma,
|
||||
actor,
|
||||
groupArtifacts.flatMap((artifact) =>
|
||||
artifact.groupId && artifact.group ? [{ id: artifact.id, targetId: artifact.groupId }] : [],
|
||||
),
|
||||
"group",
|
||||
);
|
||||
for (const artifact of groupArtifacts) {
|
||||
if (!artifact.groupId || !artifact.group) continue;
|
||||
const message = groupArtifactMessages.get(artifact.id);
|
||||
@ -193,19 +259,6 @@ export async function querySpaceSearch(
|
||||
);
|
||||
}
|
||||
|
||||
const routines = await prisma.routine.findMany({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
OR: [
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ prompt: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
bot: { archivedAt: null },
|
||||
},
|
||||
include: { bot: { select: { name: true } } },
|
||||
take: SEARCH_LIMIT,
|
||||
});
|
||||
for (const routine of routines) {
|
||||
pushInto(
|
||||
contentHits,
|
||||
@ -221,29 +274,6 @@ export async function querySpaceSearch(
|
||||
);
|
||||
}
|
||||
|
||||
const pattern = `%${query}%`;
|
||||
const messageRows = await prisma.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
threadId: string;
|
||||
seq: number;
|
||||
blocks: Prisma.JsonValue;
|
||||
botId: string;
|
||||
botName: string;
|
||||
}>
|
||||
>`
|
||||
SELECT m.id, m."threadId", m.seq, m.blocks, b.id AS "botId", b.name AS "botName"
|
||||
FROM messages m
|
||||
INNER JOIN threads t ON t.id = m."threadId"
|
||||
INNER JOIN bots b ON b.id = t."botId"
|
||||
WHERE t."spaceId" = ${actor.spaceId}
|
||||
AND t."userId" = ${actor.userId}
|
||||
AND b."archivedAt" IS NULL
|
||||
AND m.blocks::text ILIKE ${pattern}
|
||||
ORDER BY m."createdAt" DESC
|
||||
LIMIT ${SEARCH_LIMIT}
|
||||
`;
|
||||
|
||||
for (const row of messageRows) {
|
||||
pushMessageHits(row.blocks as MessageBlock[], {
|
||||
botId: row.botId,
|
||||
@ -255,28 +285,6 @@ export async function querySpaceSearch(
|
||||
});
|
||||
}
|
||||
|
||||
const groupMessageRows = await prisma.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
threadId: string;
|
||||
seq: number;
|
||||
blocks: Prisma.JsonValue;
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
}>
|
||||
>`
|
||||
SELECT m.id, m."threadId", m.seq, m.blocks, g.id AS "groupId", g.name AS "groupName"
|
||||
FROM messages m
|
||||
INNER JOIN threads t ON t.id = m."threadId"
|
||||
INNER JOIN chat_groups g ON g.id = t."groupId"
|
||||
WHERE t."spaceId" = ${actor.spaceId}
|
||||
AND t."userId" = ${actor.userId}
|
||||
AND t."groupId" IS NOT NULL
|
||||
AND m.blocks::text ILIKE ${pattern}
|
||||
ORDER BY m."createdAt" DESC
|
||||
LIMIT ${SEARCH_LIMIT}
|
||||
`;
|
||||
|
||||
for (const row of groupMessageRows) {
|
||||
pushMessageHits(row.blocks as MessageBlock[], {
|
||||
groupId: row.groupId,
|
||||
|
||||
@ -88,4 +88,77 @@ describe("model team chat engagement judge", () => {
|
||||
expect(instructions).toContain("(Europe/Berlin, the user's time zone)");
|
||||
expect(instructions).toContain("low-cost engagement judge");
|
||||
});
|
||||
|
||||
it("prefers the space fast model tier when mapped and no explicit judge override is set", async () => {
|
||||
let selectedModel: { provider: string; id: string; apiKey?: string } | undefined;
|
||||
const runtime = {
|
||||
run: vi.fn((request: { model: { provider: string; id: string; apiKey?: string } }) => {
|
||||
selectedModel = request.model;
|
||||
return (async function* () {
|
||||
yield { type: "done" as const, text: '{"act":true,"reason":"Direct mention."}' };
|
||||
})();
|
||||
}),
|
||||
} as unknown as AgentRuntime;
|
||||
const prisma = {
|
||||
deploymentSettings: { findUnique: vi.fn(async () => null) },
|
||||
spaceModelPreference: { findFirst: vi.fn(async () => null) },
|
||||
spaceModelTier: {
|
||||
findUnique: vi.fn(async () => ({
|
||||
tier: "fast",
|
||||
credentialId: "cred-fast",
|
||||
modelId: "gemma4:latest",
|
||||
credential: {
|
||||
id: "cred-fast",
|
||||
userId: "user-1",
|
||||
provider: "openai-compatible",
|
||||
secretId: "sec-fast",
|
||||
preferences: [{ isDefault: false, modelId: null }],
|
||||
},
|
||||
})),
|
||||
},
|
||||
secret: {
|
||||
findFirst: vi.fn(async () => ({ id: "sec-fast", ciphertext: "cipher-fast" })),
|
||||
},
|
||||
userProfile: { findUnique: vi.fn(async () => ({ timeZone: "UTC" })) },
|
||||
} as unknown as PrismaClient;
|
||||
const secrets = {
|
||||
load: vi.fn(() =>
|
||||
JSON.stringify({
|
||||
kind: "openai_compatible",
|
||||
apiKey: "local-key",
|
||||
baseUrl: "http://127.0.0.1:11434/v1",
|
||||
}),
|
||||
),
|
||||
} as unknown as EncryptedSecretStore;
|
||||
|
||||
const judge = new ModelTeamChatEngagementJudge({
|
||||
prisma,
|
||||
runtime,
|
||||
secrets,
|
||||
deploymentProvider: "openrouter",
|
||||
deploymentModel: "expensive-default",
|
||||
});
|
||||
|
||||
const decision = await judge.decide({
|
||||
bot: {
|
||||
id: "bot-1",
|
||||
spaceId: "space-1",
|
||||
userId: "user-1",
|
||||
name: "Arthur",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-opus",
|
||||
},
|
||||
channelId: "C1",
|
||||
rules: "",
|
||||
messages: [],
|
||||
});
|
||||
|
||||
expect(decision).toEqual({ act: true, reason: "Direct mention.", askedByEventId: undefined });
|
||||
expect(selectedModel).toMatchObject({
|
||||
provider: "openai-compatible",
|
||||
id: "gemma4:latest",
|
||||
apiKey: "local-key",
|
||||
baseUrl: "http://127.0.0.1:11434/v1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentModelOAuthCredential, AgentRuntime } from "@rakazo/adapter-kit";
|
||||
import type { EncryptedSecretStore } from "@rakazo/adapters";
|
||||
import {
|
||||
type EncryptedSecretStore,
|
||||
formatCurrentTimeInstruction,
|
||||
loadUserTimeZone,
|
||||
resolveModelAuth,
|
||||
serializeModelSecret,
|
||||
toOAuthCredential,
|
||||
} from "@rakazo/adapters";
|
||||
import { findDefaultModelCredential, findModelCredential, type PrismaClient } from "@rakazo/db";
|
||||
import type { PrismaClient } from "@rakazo/db";
|
||||
import { findDefaultModelCredential, findModelCredential, findSpaceModelTier } from "@rakazo/db";
|
||||
import { getLogger } from "@rakazo/logging";
|
||||
|
||||
const MAX_RULES_CHARS = 4_000;
|
||||
@ -191,27 +192,39 @@ export class ModelTeamChatEngagementJudge implements TeamChatEngagementJudge {
|
||||
};
|
||||
};
|
||||
} | null> {
|
||||
const settings = await this.deps.prisma.deploymentSettings.findUnique({
|
||||
where: { id: "default" },
|
||||
});
|
||||
const scope = { userId: bot.userId, spaceId: bot.spaceId };
|
||||
const defaultCredential = await findDefaultModelCredential(this.deps.prisma, scope);
|
||||
const explicitJudgeOverride = Boolean(this.deps.providerOverride && this.deps.modelOverride);
|
||||
const fastTier =
|
||||
!explicitJudgeOverride && this.deps.prisma.spaceModelTier
|
||||
? await findSpaceModelTier(this.deps.prisma, scope, "fast")
|
||||
: null;
|
||||
const defaultCredential = fastTier
|
||||
? null
|
||||
: await findDefaultModelCredential(this.deps.prisma, scope);
|
||||
const requestedProvider = this.deps.providerOverride ?? bot.modelProvider;
|
||||
const requestedModel = this.deps.modelOverride ?? bot.modelId;
|
||||
const overrideCredential = requestedProvider
|
||||
? await findModelCredential(this.deps.prisma, scope, requestedProvider)
|
||||
: null;
|
||||
const explicitJudgeOverride = Boolean(this.deps.providerOverride && this.deps.modelOverride);
|
||||
const overrideCredential =
|
||||
!fastTier && requestedProvider
|
||||
? await findModelCredential(this.deps.prisma, scope, requestedProvider)
|
||||
: null;
|
||||
const useOverride = Boolean(
|
||||
requestedProvider && requestedModel && (explicitJudgeOverride || overrideCredential),
|
||||
);
|
||||
const credential = useOverride ? overrideCredential : defaultCredential;
|
||||
const credential =
|
||||
fastTier?.credential ?? (useOverride ? overrideCredential : defaultCredential);
|
||||
const settings = fastTier
|
||||
? null
|
||||
: await this.deps.prisma.deploymentSettings.findUnique({
|
||||
where: { id: "default" },
|
||||
});
|
||||
const provider =
|
||||
fastTier?.credential.provider ??
|
||||
(useOverride ? requestedProvider : null) ??
|
||||
credential?.provider ??
|
||||
settings?.defaultModelProvider ??
|
||||
this.deps.deploymentProvider;
|
||||
const modelId =
|
||||
fastTier?.modelId ??
|
||||
(useOverride ? requestedModel : null) ??
|
||||
credential?.defaultModel ??
|
||||
settings?.defaultModelId ??
|
||||
|
||||
@ -2,15 +2,17 @@ import type { SandboxProvider } from "@rakazo/adapter-kit";
|
||||
import type { Actor } from "@rakazo/contracts";
|
||||
import type { PrismaClient } from "@rakazo/db";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ThreadTarget } from "./thread-target.js";
|
||||
import {
|
||||
cancelSupersededQueuedRuns,
|
||||
followUpGroupThreadMessage,
|
||||
groupDefaultBotId,
|
||||
reactToThreadMessage,
|
||||
resolveGroupFollowUpBotId,
|
||||
resumeThreadRuns,
|
||||
sendQueuedThreadMessagesNow,
|
||||
sendThreadMessage,
|
||||
stopThreadRuns,
|
||||
type ThreadTarget,
|
||||
threadHead,
|
||||
threadSnapshot,
|
||||
} from "./thread-target.js";
|
||||
@ -2118,4 +2120,192 @@ describe("stopThreadRuns", () => {
|
||||
expect.objectContaining({ runId: "run-b" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("sendQueuedThreadMessagesNow cancels the active run, preserves pending steering, and enqueues an immediate continuation run", async () => {
|
||||
const queuedCreatedAt = new Date("2026-01-01T10:00:05.000Z");
|
||||
const transaction = {
|
||||
$queryRaw: vi.fn(),
|
||||
steeringMessage: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "steer-1",
|
||||
botId: "bot-a",
|
||||
userId: "user-1",
|
||||
messageId: "msg-queued-1",
|
||||
claimedAt: null,
|
||||
message: {
|
||||
id: "msg-queued-1",
|
||||
blocks: [{ kind: "text", text: "Send this right now" }],
|
||||
seq: 2,
|
||||
createdAt: queuedCreatedAt,
|
||||
},
|
||||
},
|
||||
]),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
run: {
|
||||
updateManyAndReturn: vi.fn().mockResolvedValue([{ id: "run-old", taskId: "task-old" }]),
|
||||
create: vi.fn().mockResolvedValue({ id: "run-new", status: "queued" }),
|
||||
},
|
||||
task: {
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||
create: vi.fn().mockResolvedValue({ id: "task-new" }),
|
||||
},
|
||||
message: {
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
computerExecutionLease: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
computer: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
event: {
|
||||
findFirst: vi.fn().mockResolvedValue({ seq: 42 }),
|
||||
},
|
||||
};
|
||||
const prisma = {
|
||||
$transaction: vi.fn(async (callback: (client: typeof transaction) => unknown) =>
|
||||
callback(transaction),
|
||||
),
|
||||
computerExecutionLease: {
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
computer: {
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
event: {
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
} as unknown as PrismaClient;
|
||||
const jobs = {
|
||||
enqueue: vi.fn().mockResolvedValue(undefined),
|
||||
cancel: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const events = {
|
||||
notify: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const actor = { spaceId: "workspace-1", userId: "user-1" } as Actor;
|
||||
const target = {
|
||||
kind: "bot",
|
||||
botId: "bot-a",
|
||||
threadId: "thread-1",
|
||||
bot: {} as never,
|
||||
} satisfies ThreadTarget;
|
||||
|
||||
const result = await sendQueuedThreadMessagesNow(
|
||||
{
|
||||
prisma,
|
||||
sandbox: {} as SandboxProvider,
|
||||
events: events as never,
|
||||
jobs: jobs as never,
|
||||
},
|
||||
actor,
|
||||
target,
|
||||
{ messageId: "msg-queued-1" },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ ok: true, runIds: ["run-new"] });
|
||||
expect(transaction.run.updateManyAndReturn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
threadId: "thread-1",
|
||||
botId: { in: ["bot-a"] },
|
||||
}),
|
||||
data: expect.objectContaining({ status: "cancelled" }),
|
||||
}),
|
||||
);
|
||||
expect(transaction.steeringMessage.updateMany).toHaveBeenCalledWith({
|
||||
where: { id: { in: ["steer-1"] } },
|
||||
data: { runId: "run-new", claimedAt: null },
|
||||
});
|
||||
expect(jobs.cancel).toHaveBeenCalledWith("run:run-old");
|
||||
expect(jobs.enqueue).toHaveBeenCalledWith({
|
||||
name: "run.continue",
|
||||
payload: { runId: "run-new" },
|
||||
replaceKey: "run:run-new",
|
||||
});
|
||||
expect(events.notify).toHaveBeenCalledWith("thread-1", 42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resumeThreadRuns", () => {
|
||||
it("re-queues the latest cancelled or failed run without creating a duplicate message", async () => {
|
||||
const transaction = {
|
||||
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
|
||||
run: {
|
||||
findMany: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: "run-cancelled",
|
||||
taskId: "task-cancelled",
|
||||
botId: "bot-a",
|
||||
status: "cancelled",
|
||||
createdAt: new Date("2026-04-20T10:00:00.000Z"),
|
||||
completedAt: new Date("2026-04-20T10:02:00.000Z"),
|
||||
},
|
||||
]),
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
task: {
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
event: {
|
||||
findFirst: vi.fn().mockResolvedValue({ seq: 19 }),
|
||||
},
|
||||
};
|
||||
const prisma = {
|
||||
$transaction: vi.fn(async (fn: (tx: typeof transaction) => Promise<unknown>) =>
|
||||
fn(transaction),
|
||||
),
|
||||
} as unknown as PrismaClient;
|
||||
const jobs = {
|
||||
enqueue: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const events = {
|
||||
notify: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const actor = { spaceId: "workspace-1", userId: "user-1" } as Actor;
|
||||
const target = {
|
||||
kind: "bot",
|
||||
botId: "bot-a",
|
||||
threadId: "thread-1",
|
||||
bot: {} as never,
|
||||
} satisfies ThreadTarget;
|
||||
|
||||
const result = await resumeThreadRuns(
|
||||
{
|
||||
prisma,
|
||||
events: events as never,
|
||||
jobs: jobs as never,
|
||||
},
|
||||
actor,
|
||||
target,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ ok: true, runIds: ["run-cancelled"] });
|
||||
expect(transaction.task.updateMany).toHaveBeenCalledWith({
|
||||
where: { id: "task-cancelled" },
|
||||
data: { status: "queued" },
|
||||
});
|
||||
expect(transaction.run.update).toHaveBeenCalledWith({
|
||||
where: { id: "run-cancelled" },
|
||||
data: {
|
||||
status: "queued",
|
||||
error: null,
|
||||
completedAt: null,
|
||||
leaseOwner: null,
|
||||
leaseExpiresAt: null,
|
||||
},
|
||||
});
|
||||
expect(jobs.enqueue).toHaveBeenCalledWith({
|
||||
name: "run.continue",
|
||||
payload: { runId: "run-cancelled" },
|
||||
replaceKey: "run:run-cancelled",
|
||||
});
|
||||
expect(events.notify).toHaveBeenCalledWith("thread-1", 19);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { ORPCError } from "@orpc/server";
|
||||
import { type JobPublisher, runContinueJob, type SandboxProvider } from "@rakazo/adapter-kit";
|
||||
import {
|
||||
type JobPublisher,
|
||||
runContinueJob,
|
||||
runJobKey,
|
||||
type SandboxProvider,
|
||||
} from "@rakazo/adapter-kit";
|
||||
import { cancelComputerRunWork, screenLeaseIdForRun, toComputerRef } from "@rakazo/adapters";
|
||||
import {
|
||||
type Actor,
|
||||
@ -1151,63 +1156,50 @@ export async function reactToThreadMessage(
|
||||
});
|
||||
}
|
||||
|
||||
export async function stopThreadRuns(
|
||||
async function snapshotCancelledRunTeardown(tx: Prisma.TransactionClient, runIds: string[]) {
|
||||
// Snapshot teardown coordinates before commit. Once cancellation becomes
|
||||
// visible, a worker can release its lease / execution columns immediately; a
|
||||
// later lookup would then miss the sandbox work this request must stop.
|
||||
// Team ownership lives on ComputerExecutionLease; Computer.executionRunId is
|
||||
// only a legacy secondary path and is not written by current acquisition.
|
||||
const leases = runIds.length
|
||||
? await tx.computerExecutionLease.findMany({
|
||||
where: { runId: { in: runIds } },
|
||||
select: { computerId: true, botId: true, runId: true, fence: true },
|
||||
})
|
||||
: [];
|
||||
const leaseComputerIds = [...new Set(leases.map((lease) => lease.computerId))];
|
||||
const computers = runIds.length
|
||||
? await tx.computer.findMany({
|
||||
where:
|
||||
leaseComputerIds.length > 0
|
||||
? {
|
||||
OR: [{ id: { in: leaseComputerIds } }, { executionRunId: { in: runIds } }],
|
||||
}
|
||||
: { executionRunId: { in: runIds } },
|
||||
select: {
|
||||
id: true,
|
||||
homeKey: true,
|
||||
kind: true,
|
||||
providerRef: true,
|
||||
executionBotId: true,
|
||||
executionRunId: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
return { runIds, computers, leases };
|
||||
}
|
||||
|
||||
async function teardownCancelledRuns(
|
||||
deps: {
|
||||
prisma: PrismaClient;
|
||||
sandbox: SandboxProvider;
|
||||
},
|
||||
actor: Actor,
|
||||
target: ThreadTarget,
|
||||
snapshot: Awaited<ReturnType<typeof snapshotCancelledRunTeardown>>,
|
||||
) {
|
||||
const { runIds, computers, leases } = await deps.prisma.$transaction(async (tx) => {
|
||||
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`;
|
||||
const cancelled = await tx.run.updateManyAndReturn({
|
||||
where: {
|
||||
threadId: target.threadId,
|
||||
status: { in: [...ACTIVE_RUN_STATUSES] },
|
||||
},
|
||||
data: { status: "cancelled", completedAt: new Date() },
|
||||
select: { id: true },
|
||||
});
|
||||
const ids = cancelled.map((run) => run.id);
|
||||
await tx.steeringMessage.deleteMany({
|
||||
where: {
|
||||
botId: { in: target.kind === "bot" ? [target.botId] : target.memberBotIds },
|
||||
message: { threadId: target.threadId },
|
||||
},
|
||||
});
|
||||
// Snapshot teardown coordinates before commit. Once cancellation becomes
|
||||
// visible, a worker can release its lease / execution columns immediately; a
|
||||
// later lookup would then miss the sandbox work this request must stop.
|
||||
// Team ownership lives on ComputerExecutionLease; Computer.executionRunId is
|
||||
// only a legacy secondary path and is not written by current acquisition.
|
||||
const leases = ids.length
|
||||
? await tx.computerExecutionLease.findMany({
|
||||
where: { runId: { in: ids } },
|
||||
select: { computerId: true, botId: true, runId: true, fence: true },
|
||||
})
|
||||
: [];
|
||||
const leaseComputerIds = [...new Set(leases.map((lease) => lease.computerId))];
|
||||
const computers = ids.length
|
||||
? await tx.computer.findMany({
|
||||
where:
|
||||
leaseComputerIds.length > 0
|
||||
? {
|
||||
OR: [{ id: { in: leaseComputerIds } }, { executionRunId: { in: ids } }],
|
||||
}
|
||||
: { executionRunId: { in: ids } },
|
||||
select: {
|
||||
id: true,
|
||||
homeKey: true,
|
||||
kind: true,
|
||||
providerRef: true,
|
||||
executionBotId: true,
|
||||
executionRunId: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
return { runIds: ids, computers, leases };
|
||||
});
|
||||
const { runIds, computers, leases } = snapshot;
|
||||
if (runIds.length === 0) return;
|
||||
// Keep the DB lease until after teardown so a replacement run cannot claim the
|
||||
// screen while we still need the cancelled run's screenLeaseId to release it.
|
||||
const computerById = new Map(computers.map((computer) => [computer.id, computer]));
|
||||
@ -1285,6 +1277,381 @@ export async function stopThreadRuns(
|
||||
});
|
||||
}
|
||||
|
||||
export async function stopThreadRuns(
|
||||
deps: {
|
||||
prisma: PrismaClient;
|
||||
sandbox: SandboxProvider;
|
||||
},
|
||||
actor: Actor,
|
||||
target: ThreadTarget,
|
||||
) {
|
||||
const snapshot = await deps.prisma.$transaction(async (tx) => {
|
||||
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`;
|
||||
const cancelled = await tx.run.updateManyAndReturn({
|
||||
where: {
|
||||
threadId: target.threadId,
|
||||
status: { in: [...ACTIVE_RUN_STATUSES] },
|
||||
},
|
||||
data: { status: "cancelled", completedAt: new Date() },
|
||||
select: { id: true },
|
||||
});
|
||||
const ids = cancelled.map((run) => run.id);
|
||||
await tx.steeringMessage.deleteMany({
|
||||
where: {
|
||||
botId: { in: target.kind === "bot" ? [target.botId] : target.memberBotIds },
|
||||
message: { threadId: target.threadId },
|
||||
},
|
||||
});
|
||||
return snapshotCancelledRunTeardown(tx, ids);
|
||||
});
|
||||
await teardownCancelledRuns(deps, actor, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the latest stopped (`cancelled`) or interrupted (`failed`) run in the thread
|
||||
* without creating a duplicate user message row.
|
||||
*/
|
||||
export async function resumeThreadRuns(
|
||||
deps: {
|
||||
prisma: PrismaClient;
|
||||
events: ThreadEvents;
|
||||
jobs: JobPublisher;
|
||||
},
|
||||
actor: Actor,
|
||||
target: ThreadTarget,
|
||||
) {
|
||||
const botIds = target.kind === "bot" ? [target.botId] : target.memberBotIds;
|
||||
const committed = await deps.prisma.$transaction(async (tx) => {
|
||||
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`;
|
||||
const activeRuns = await tx.run.findMany({
|
||||
where: {
|
||||
threadId: target.threadId,
|
||||
botId: { in: botIds },
|
||||
status: { in: [...ACTIVE_RUN_STATUSES] },
|
||||
},
|
||||
select: { id: true, status: true },
|
||||
});
|
||||
if (activeRuns.length > 0) {
|
||||
return {
|
||||
runs: activeRuns,
|
||||
eventSeq: null as number | null,
|
||||
};
|
||||
}
|
||||
|
||||
const recentTerminals = await tx.run.findMany({
|
||||
where: {
|
||||
threadId: target.threadId,
|
||||
botId: { in: botIds },
|
||||
trigger: { not: "bot_message" },
|
||||
status: { in: ["failed", "completed", "cancelled"] },
|
||||
},
|
||||
orderBy: [{ updatedAt: "desc" }, { id: "desc" }],
|
||||
take: 20,
|
||||
select: {
|
||||
id: true,
|
||||
botId: true,
|
||||
taskId: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
completedAt: true,
|
||||
},
|
||||
});
|
||||
const latestTerminal = pickLatestTerminalRun(recentTerminals);
|
||||
if (
|
||||
!latestTerminal ||
|
||||
(latestTerminal.status !== "cancelled" && latestTerminal.status !== "failed")
|
||||
) {
|
||||
return {
|
||||
runs: [] as Array<{ id: string; status: string }>,
|
||||
eventSeq: null as number | null,
|
||||
};
|
||||
}
|
||||
|
||||
await tx.task.updateMany({
|
||||
where: { id: latestTerminal.taskId },
|
||||
data: { status: "queued" },
|
||||
});
|
||||
await tx.run.update({
|
||||
where: { id: latestTerminal.id },
|
||||
data: {
|
||||
status: "queued",
|
||||
error: null,
|
||||
completedAt: null,
|
||||
leaseOwner: null,
|
||||
leaseExpiresAt: null,
|
||||
},
|
||||
});
|
||||
if (target.kind === "group") {
|
||||
await touchGroupUpdatedAt(tx, target.groupId);
|
||||
}
|
||||
const latestEvent = await tx.event.findFirst({
|
||||
where: { threadId: target.threadId },
|
||||
orderBy: { seq: "desc" },
|
||||
select: { seq: true },
|
||||
});
|
||||
return {
|
||||
runs: [{ id: latestTerminal.id, status: "queued" }],
|
||||
eventSeq: latestEvent?.seq ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
if (committed.eventSeq !== null) {
|
||||
await deps.events.notify(target.threadId, committed.eventSeq).catch((error) => {
|
||||
getLogger().error("thread resume realtime notification", error);
|
||||
});
|
||||
}
|
||||
await enqueueRunsNeedingContinue(deps.jobs, committed.runs);
|
||||
return {
|
||||
ok: true as const,
|
||||
runIds: committed.runs.map((run) => run.id),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote queued follow-up messages immediately: cancel any currently active runs
|
||||
* holding up the queue without discarding unclaimed steering messages, create a
|
||||
* new continuation run for the pending steering messages, and enqueue it right away.
|
||||
*/
|
||||
export async function sendQueuedThreadMessagesNow(
|
||||
deps: {
|
||||
prisma: PrismaClient;
|
||||
sandbox: SandboxProvider;
|
||||
events: ThreadEvents;
|
||||
jobs: JobPublisher;
|
||||
},
|
||||
actor: Actor,
|
||||
target: ThreadTarget,
|
||||
input?: { messageId?: string },
|
||||
) {
|
||||
const botIds = target.kind === "bot" ? [target.botId] : target.memberBotIds;
|
||||
const committed = await deps.prisma.$transaction(async (tx) => {
|
||||
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`;
|
||||
const pendingSteering = await tx.steeringMessage.findMany({
|
||||
where: {
|
||||
botId: { in: botIds },
|
||||
claimedAt: null,
|
||||
message: { threadId: target.threadId },
|
||||
},
|
||||
include: {
|
||||
message: { select: { id: true, blocks: true, seq: true, createdAt: true } },
|
||||
},
|
||||
orderBy: [{ message: { seq: "asc" } }, { id: "asc" }],
|
||||
});
|
||||
|
||||
const pendingByBotId = new Map<string, typeof pendingSteering>();
|
||||
for (const item of pendingSteering) {
|
||||
const list = pendingByBotId.get(item.botId) ?? [];
|
||||
list.push(item);
|
||||
pendingByBotId.set(item.botId, list);
|
||||
}
|
||||
|
||||
if (pendingByBotId.size === 0 && input?.messageId) {
|
||||
const targetMsg = await tx.message.findFirst({
|
||||
where: { id: input.messageId, threadId: target.threadId, role: "user" },
|
||||
select: { id: true, blocks: true, seq: true, createdAt: true },
|
||||
});
|
||||
if (targetMsg) {
|
||||
const activeRuns = await tx.run.findMany({
|
||||
where: {
|
||||
threadId: target.threadId,
|
||||
botId: { in: botIds },
|
||||
status: { in: [...ACTIVE_RUN_STATUSES] },
|
||||
createdAt: { lt: targetMsg.createdAt },
|
||||
},
|
||||
select: { id: true, botId: true },
|
||||
});
|
||||
const fallbackBotId =
|
||||
activeRuns[0]?.botId ??
|
||||
(target.kind === "bot" ? target.botId : groupDefaultBotId(target));
|
||||
if (fallbackBotId && activeRuns.length > 0) {
|
||||
const steering = await tx.steeringMessage.create({
|
||||
data: {
|
||||
messageId: targetMsg.id,
|
||||
botId: fallbackBotId,
|
||||
userId: actor.userId,
|
||||
runId: activeRuns[0]?.id ?? null,
|
||||
},
|
||||
});
|
||||
pendingByBotId.set(fallbackBotId, [{ ...steering, message: targetMsg }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingByBotId.size === 0) {
|
||||
return {
|
||||
teardown: { runIds: [] as string[], computers: [], leases: [] },
|
||||
runs: [] as Array<{ id: string; status: string }>,
|
||||
eventSeq: null as number | null,
|
||||
};
|
||||
}
|
||||
|
||||
const affectedBotIds = [...pendingByBotId.keys()];
|
||||
const now = new Date();
|
||||
const cancelled = await tx.run.updateManyAndReturn({
|
||||
where: {
|
||||
threadId: target.threadId,
|
||||
botId: { in: affectedBotIds },
|
||||
status: { in: [...ACTIVE_RUN_STATUSES] },
|
||||
},
|
||||
data: { status: "cancelled", completedAt: now },
|
||||
select: { id: true, taskId: true },
|
||||
});
|
||||
const cancelledRunIds = cancelled.map((run) => run.id);
|
||||
if (cancelled.length > 0) {
|
||||
await tx.task.updateMany({
|
||||
where: { id: { in: cancelled.map((run) => run.taskId) } },
|
||||
data: { status: "cancelled" },
|
||||
});
|
||||
await tx.steeringMessage.deleteMany({
|
||||
where: {
|
||||
runId: { in: cancelledRunIds },
|
||||
claimedAt: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
const teardown = await snapshotCancelledRunTeardown(tx, cancelledRunIds);
|
||||
|
||||
const createdRuns: Array<{ id: string; status: string }> = [];
|
||||
for (const [botId, items] of pendingByBotId) {
|
||||
const last = items.at(-1)!;
|
||||
const task = await tx.task.create({
|
||||
data: {
|
||||
spaceId: actor.spaceId,
|
||||
botId,
|
||||
threadId: target.threadId,
|
||||
userId: items[0]!.userId,
|
||||
prompt: "Respond to the user's steering context.",
|
||||
status: "queued",
|
||||
},
|
||||
});
|
||||
const runCreatedAt = new Date(Math.max(Date.now(), last.message.createdAt.getTime() + 1));
|
||||
const run = await tx.run.create({
|
||||
data: {
|
||||
spaceId: actor.spaceId,
|
||||
botId,
|
||||
threadId: target.threadId,
|
||||
taskId: task.id,
|
||||
userId: items[0]!.userId,
|
||||
status: "queued",
|
||||
trigger: "follow_up",
|
||||
sourceMessageId: last.message.id,
|
||||
createdAt: runCreatedAt,
|
||||
},
|
||||
select: { id: true, status: true },
|
||||
});
|
||||
await tx.steeringMessage.updateMany({
|
||||
where: { id: { in: items.map((item) => item.id) } },
|
||||
data: { runId: run.id, claimedAt: null },
|
||||
});
|
||||
await tx.message.updateMany({
|
||||
where: { id: { in: items.map((item) => item.message.id) } },
|
||||
data: { runId: run.id },
|
||||
});
|
||||
createdRuns.push(run);
|
||||
}
|
||||
|
||||
if (target.kind === "group") {
|
||||
await touchGroupUpdatedAt(tx, target.groupId);
|
||||
}
|
||||
const latestEvent = await tx.event.findFirst({
|
||||
where: { threadId: target.threadId },
|
||||
orderBy: { seq: "desc" },
|
||||
select: { seq: true },
|
||||
});
|
||||
return {
|
||||
teardown,
|
||||
runs: createdRuns,
|
||||
eventSeq: latestEvent?.seq ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
if (committed.teardown.runIds.length > 0) {
|
||||
await Promise.all([
|
||||
teardownCancelledRuns(deps, actor, committed.teardown),
|
||||
...committed.teardown.runIds.map((runId) =>
|
||||
deps.jobs.cancel(runJobKey(runId)).catch(() => undefined),
|
||||
),
|
||||
]);
|
||||
}
|
||||
if (committed.runs.length > 0) {
|
||||
await enqueueRunsNeedingContinue(deps.jobs, committed.runs);
|
||||
if (committed.eventSeq != null) {
|
||||
await deps.events.notify(target.threadId, committed.eventSeq).catch((error) => {
|
||||
getLogger().error("thread sendNow realtime notification", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true as const,
|
||||
runIds: committed.runs.map((run) => run.id),
|
||||
};
|
||||
}
|
||||
|
||||
export async function removeQueuedMessage(
|
||||
deps: {
|
||||
prisma: PrismaClient;
|
||||
events: ThreadEvents;
|
||||
},
|
||||
actor: Actor,
|
||||
target: ThreadTarget,
|
||||
messageId: string,
|
||||
) {
|
||||
const botIds = target.kind === "bot" ? [target.botId] : target.memberBotIds;
|
||||
const result = await deps.prisma.$transaction(async (tx) => {
|
||||
// Verify the message exists and belongs to this thread
|
||||
const message = await tx.message.findFirst({
|
||||
where: {
|
||||
id: messageId,
|
||||
threadId: target.threadId,
|
||||
role: "user",
|
||||
},
|
||||
});
|
||||
if (!message) {
|
||||
return { removed: false as const };
|
||||
}
|
||||
|
||||
// Check if this message is queued (has an unclaimed steering message)
|
||||
const steering = await tx.steeringMessage.findFirst({
|
||||
where: {
|
||||
messageId: messageId,
|
||||
botId: { in: botIds },
|
||||
claimedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!steering) {
|
||||
// No queued steering entry — nothing to remove
|
||||
return { removed: false as const };
|
||||
}
|
||||
|
||||
// Delete the steering message (unclaims it from the queue)
|
||||
await tx.steeringMessage.delete({
|
||||
where: { id: steering.id },
|
||||
});
|
||||
|
||||
// Also clean up the user message if it's exclusively queued
|
||||
// (no run attached to it)
|
||||
if (!message.runId) {
|
||||
await tx.message.delete({
|
||||
where: { id: messageId },
|
||||
});
|
||||
}
|
||||
|
||||
return { removed: true as const };
|
||||
});
|
||||
|
||||
if (result.removed) {
|
||||
// Notify subscribers so the UI updates
|
||||
const latest = await deps.prisma.event.findFirst({
|
||||
where: { threadId: target.threadId },
|
||||
orderBy: { seq: "desc" },
|
||||
select: { seq: true },
|
||||
});
|
||||
await deps.events.notify(target.threadId, latest?.seq ?? 0).catch(() => undefined);
|
||||
}
|
||||
|
||||
return { ok: true as const, removed: result.removed };
|
||||
}
|
||||
export async function setThreadUnreadState(
|
||||
prisma: PrismaClient,
|
||||
actor: Actor,
|
||||
|
||||
@ -147,7 +147,11 @@ export async function persistVoiceCredential(
|
||||
let voiceId = input.voiceId?.trim() ?? "";
|
||||
if (!voiceId) {
|
||||
const voices = await provider.listVoices(input.plaintext, voiceContext(actor, input.signal));
|
||||
voiceId = voices[0]?.id ?? "";
|
||||
const germanVoice =
|
||||
input.provider === "kokoro"
|
||||
? voices.find((v) => v.id === "df_eva" || /^d[fm]_/i.test(v.id))
|
||||
: undefined;
|
||||
voiceId = germanVoice?.id ?? voices[0]?.id ?? "";
|
||||
}
|
||||
const stored = await deps.secrets.put(input.plaintext, voiceContext(actor, input.signal));
|
||||
const cred = await withSerializableRetry(() =>
|
||||
|
||||
@ -25,4 +25,12 @@ describe("requestWebOrigin", () => {
|
||||
"https://evoo.example",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts origins listed in extraTrustedOrigins", () => {
|
||||
expect(
|
||||
requestWebOrigin("https://host.example.ts.net:8443", configured, [
|
||||
"https://host.example.ts.net:8443",
|
||||
]),
|
||||
).toBe("https://host.example.ts.net:8443");
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,15 +14,20 @@ export function isLoopbackHost(host: string): boolean {
|
||||
}
|
||||
|
||||
/** The web origin that browser-facing URLs (OAuth returns, screen links) should
|
||||
* point at for this request. A browser opened over a loopback or private-network
|
||||
* address gets its own origin back so a phone on the LAN is not sent to
|
||||
* 127.0.0.1; any other caller gets the configured WEB_ORIGIN. */
|
||||
export function requestWebOrigin(origin: string | undefined, webOrigin: string): string {
|
||||
* point at for this request. A browser opened over a loopback, private-network,
|
||||
* or explicitly trusted origin gets its own origin back so a phone on the LAN
|
||||
* or Tailscale is not sent to 127.0.0.1; any other caller gets WEB_ORIGIN. */
|
||||
export function requestWebOrigin(
|
||||
origin: string | undefined,
|
||||
webOrigin: string,
|
||||
extraTrustedOrigins: readonly string[] = [],
|
||||
): string {
|
||||
if (!origin) return webOrigin;
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return webOrigin;
|
||||
if (url.origin === new URL(webOrigin).origin) return url.origin;
|
||||
if (extraTrustedOrigins.includes(url.origin)) return url.origin;
|
||||
return isPrivateOrLocalHost(url.hostname) ? url.origin : webOrigin;
|
||||
} catch {
|
||||
return webOrigin;
|
||||
|
||||
@ -3,8 +3,15 @@ import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, Pressable, ScrollView, Text, TextInput } from "react-native";
|
||||
import { BotMemberPicker } from "../components/bot-member-picker";
|
||||
import { type MobileBot, type MobileGroup, rpc } from "../lib/api";
|
||||
import {
|
||||
type MobileBot,
|
||||
type MobileGroup,
|
||||
type MobileSpace,
|
||||
type MobileSpaceNavigation,
|
||||
rpc,
|
||||
} from "../lib/api";
|
||||
import { useI18n } from "../lib/i18n";
|
||||
import { canDeleteInboxSpace, removeInboxSpace } from "../lib/inbox-spaces";
|
||||
import { useMobileTokens } from "../lib/native";
|
||||
|
||||
export default function GroupSettingsScreen() {
|
||||
@ -13,12 +20,16 @@ export default function GroupSettingsScreen() {
|
||||
const router = useRouter();
|
||||
const { groupId } = useLocalSearchParams<{ groupId: string }>();
|
||||
const [group, setGroup] = useState<MobileGroup | null>(null);
|
||||
const [projectSpace, setProjectSpace] = useState<MobileSpace | null>(null);
|
||||
const [bots, setBots] = useState<MobileBot[]>([]);
|
||||
const [name, setName] = useState("");
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const isMainConversation = group?.kind === "main";
|
||||
const isProjectMain = isMainConversation && projectSpace !== null && !projectSpace.isDefault;
|
||||
|
||||
useEffect(() => {
|
||||
if (!groupId) return;
|
||||
void Promise.all([
|
||||
@ -26,11 +37,19 @@ export default function GroupSettingsScreen() {
|
||||
(groups) => groups.find((row) => row.id === groupId) ?? null,
|
||||
),
|
||||
rpc<MobileBot[]>("bots/list"),
|
||||
rpc<MobileSpaceNavigation>("spaces/list").catch(() => null),
|
||||
])
|
||||
.then(([nextGroup, nextBots]) => {
|
||||
.then(([nextGroup, nextBots, navigation]) => {
|
||||
if (!nextGroup) throw new Error(t("Group not found"));
|
||||
setGroup(nextGroup);
|
||||
setName(nextGroup.name);
|
||||
const space =
|
||||
navigation?.spaces.find(
|
||||
(item) => item.id === nextGroup.spaceId || item.id === navigation.current.id,
|
||||
) ?? null;
|
||||
setProjectSpace(space);
|
||||
setName(
|
||||
nextGroup.kind === "main" && space && !space.isDefault ? space.name : nextGroup.name,
|
||||
);
|
||||
setSelected(nextGroup.members.map((member) => member.botId));
|
||||
setBots(nextBots.filter((bot) => !bot.archivedAt));
|
||||
})
|
||||
@ -42,10 +61,20 @@ export default function GroupSettingsScreen() {
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const trimmed = name.trim();
|
||||
if (isProjectMain && projectSpace) {
|
||||
if (trimmed && trimmed !== projectSpace.name) {
|
||||
await rpc("spaces/update", { spaceId: projectSpace.id, name: trimmed });
|
||||
}
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
const input: { groupId: string; name?: string; botIds?: string[] } = { groupId };
|
||||
if (name.trim() !== group.name) input.name = name.trim();
|
||||
const memberIds = group.members.map((member) => member.botId).join(",");
|
||||
if (selected.join(",") !== memberIds) input.botIds = selected;
|
||||
if (trimmed !== group.name) input.name = trimmed;
|
||||
if (!isMainConversation) {
|
||||
const memberIds = group.members.map((member) => member.botId).join(",");
|
||||
if (selected.join(",") !== memberIds) input.botIds = selected;
|
||||
}
|
||||
if (input.name || input.botIds) await rpc("groups/update", input);
|
||||
router.back();
|
||||
} catch (err) {
|
||||
@ -57,6 +86,29 @@ export default function GroupSettingsScreen() {
|
||||
|
||||
function remove() {
|
||||
if (!groupId || !group) return;
|
||||
if (isProjectMain && projectSpace) {
|
||||
Alert.alert(
|
||||
t("Delete {name}?", { name: projectSpace.name }),
|
||||
t("This removes the empty space for everyone."),
|
||||
[
|
||||
{ text: t("Cancel"), style: "cancel" },
|
||||
{
|
||||
text: t("Delete"),
|
||||
style: "destructive",
|
||||
onPress: () =>
|
||||
void removeInboxSpace(projectSpace.id, async () => {})
|
||||
.then(() => router.replace("/"))
|
||||
.catch((err) =>
|
||||
Alert.alert(
|
||||
t("Could not delete space"),
|
||||
err instanceof Error ? err.message : t("Try again."),
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
Alert.alert(group.name, t("Delete this group? Bots and their solo threads are kept."), [
|
||||
{ text: t("Cancel"), style: "cancel" },
|
||||
{
|
||||
@ -75,6 +127,12 @@ export default function GroupSettingsScreen() {
|
||||
]);
|
||||
}
|
||||
|
||||
const saveDisabled =
|
||||
!name.trim() ||
|
||||
pending ||
|
||||
(!isMainConversation &&
|
||||
(selected.length < GROUP_MEMBER_MIN || selected.length > GROUP_MEMBER_MAX));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: t("Group settings") }} />
|
||||
@ -97,34 +155,27 @@ export default function GroupSettingsScreen() {
|
||||
fontSize: 16,
|
||||
}}
|
||||
/>
|
||||
<Text style={{ color: tokens.mutedForeground, fontSize: 14, marginTop: 20 }}>
|
||||
{t("Members")}
|
||||
</Text>
|
||||
<BotMemberPicker
|
||||
bots={bots}
|
||||
selected={selected}
|
||||
onChange={setSelected}
|
||||
disabled={pending}
|
||||
/>
|
||||
{!isMainConversation ? (
|
||||
<>
|
||||
<Text style={{ color: tokens.mutedForeground, fontSize: 14, marginTop: 20 }}>
|
||||
{t("Members")}
|
||||
</Text>
|
||||
<BotMemberPicker
|
||||
bots={bots}
|
||||
selected={selected}
|
||||
onChange={setSelected}
|
||||
disabled={pending}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{error ? <Text style={{ color: tokens.destructive, marginTop: 12 }}>{error}</Text> : null}
|
||||
<Pressable
|
||||
onPress={() => void save()}
|
||||
disabled={
|
||||
!name.trim() ||
|
||||
selected.length < GROUP_MEMBER_MIN ||
|
||||
selected.length > GROUP_MEMBER_MAX ||
|
||||
pending
|
||||
}
|
||||
disabled={saveDisabled}
|
||||
style={{
|
||||
marginTop: 24,
|
||||
backgroundColor: tokens.primary,
|
||||
opacity:
|
||||
!name.trim() ||
|
||||
selected.length < GROUP_MEMBER_MIN ||
|
||||
selected.length > GROUP_MEMBER_MAX ||
|
||||
pending
|
||||
? 0.5
|
||||
: 1,
|
||||
opacity: saveDisabled ? 0.5 : 1,
|
||||
borderRadius: 11,
|
||||
padding: 14,
|
||||
alignItems: "center",
|
||||
@ -134,19 +185,35 @@ export default function GroupSettingsScreen() {
|
||||
{pending ? t("Saving…") : t("Save")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={remove}
|
||||
style={{
|
||||
marginTop: 16,
|
||||
borderRadius: 11,
|
||||
borderWidth: 1,
|
||||
borderColor: tokens.border,
|
||||
padding: 14,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: tokens.destructive, fontSize: 16 }}>{t("Delete group")}</Text>
|
||||
</Pressable>
|
||||
{isProjectMain && projectSpace && canDeleteInboxSpace(projectSpace) ? (
|
||||
<Pressable
|
||||
onPress={remove}
|
||||
style={{
|
||||
marginTop: 16,
|
||||
borderRadius: 11,
|
||||
borderWidth: 1,
|
||||
borderColor: tokens.border,
|
||||
padding: 14,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: tokens.destructive, fontSize: 16 }}>{t("Delete space")}</Text>
|
||||
</Pressable>
|
||||
) : !isMainConversation ? (
|
||||
<Pressable
|
||||
onPress={remove}
|
||||
style={{
|
||||
marginTop: 16,
|
||||
borderRadius: 11,
|
||||
borderWidth: 1,
|
||||
borderColor: tokens.border,
|
||||
padding: 14,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: tokens.destructive, fontSize: 16 }}>{t("Delete group")}</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -597,7 +597,21 @@ export default function Home() {
|
||||
}}
|
||||
disabled={spaceBusy}
|
||||
onPress={() => {
|
||||
if (item.space) void chooseInboxSpace(item.space.id);
|
||||
if (!item.space) return;
|
||||
const targetSpace = item.space;
|
||||
const main = !targetSpace.isDefault
|
||||
? targetSpace.groups.find((group) => group.kind === "main")
|
||||
: undefined;
|
||||
if (main) {
|
||||
void openMobileSpace(targetSpace.id, () =>
|
||||
router.push({
|
||||
pathname: "/group-thread",
|
||||
params: { groupId: main.id, name: targetSpace.name },
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
void chooseInboxSpace(targetSpace.id);
|
||||
}}
|
||||
style={({ pressed }) => [styles.spaceSelect, pressed && styles.rowPressed]}
|
||||
>
|
||||
|
||||
@ -6,22 +6,19 @@ import type {
|
||||
Connection,
|
||||
ConnectionCatalogItem,
|
||||
MessageBlock,
|
||||
MessageReaction,
|
||||
Routine,
|
||||
} from "@rakazo/contracts";
|
||||
import {
|
||||
AUTOPILOT_LIMITS,
|
||||
canReactToThreadMessage,
|
||||
MESSAGE_REACTIONS,
|
||||
type MessageReaction,
|
||||
} from "@rakazo/contracts";
|
||||
import type { AutopilotUntilChoice } from "@rakazo/core";
|
||||
import { AUTOPILOT_LIMITS, canReactToThreadMessage, MESSAGE_REACTIONS } from "@rakazo/contracts";
|
||||
import type { AutopilotUntilChoice, ComposerMention, SlashActionId } from "@rakazo/core";
|
||||
import {
|
||||
abortableDelay,
|
||||
attachmentsForThread,
|
||||
autopilotUntil,
|
||||
buildComposerMentionOptions,
|
||||
type ComposerMention,
|
||||
clearComposerDraft,
|
||||
cloudAgentHttpsUrl,
|
||||
composerDraftThreadKey,
|
||||
deriveWorkspaceFocus,
|
||||
foldRunNarration,
|
||||
isApprovalAskBlock,
|
||||
@ -32,13 +29,15 @@ import {
|
||||
liveScreenRunId,
|
||||
mentionChipKey,
|
||||
projectMessageReactions,
|
||||
queuedFollowUps,
|
||||
readComposerDraft,
|
||||
resolveComposerSendPlan,
|
||||
SLASH_ACTIONS,
|
||||
type SlashActionId,
|
||||
selectedAskActionLabel,
|
||||
serializeComposerPrompt,
|
||||
truncateSlashDescription,
|
||||
userVisibleMessages,
|
||||
writeComposerDraft,
|
||||
} from "@rakazo/core";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { useFocusEffect, useLocalSearchParams, useNavigation, useRouter } from "expo-router";
|
||||
@ -289,6 +288,8 @@ function Thread() {
|
||||
const mentionBotsAppliedGeneration = useRef(0);
|
||||
const readVisibleTarget = useRef<string | null>(null);
|
||||
const threadKey = groupId ?? botId;
|
||||
const draftKey = composerDraftThreadKey({ botId, groupId });
|
||||
const initialDraft = useMemo(() => readComposerDraft(draftKey), [draftKey]);
|
||||
const [threadScrollState, setThreadScrollState] = useState<ThreadScrollState>(() =>
|
||||
scrollBehavior.current.state(),
|
||||
);
|
||||
@ -310,7 +311,7 @@ function Thread() {
|
||||
/** Run that last pointed at the bot's screen, so the live screen is offered only during real screen work. */
|
||||
const [screenRunId, setScreenRunId] = useState<string | null>(null);
|
||||
const activeThreadId = useRef<string | undefined>(undefined);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [draft, setDraft] = useState(() => initialDraft.text);
|
||||
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
||||
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
||||
const [agentSkills, setAgentSkills] = useState<AgentSkillCatalogEntry[]>([]);
|
||||
@ -325,12 +326,17 @@ function Thread() {
|
||||
connectionId?: string;
|
||||
}>
|
||||
>([]);
|
||||
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>([]);
|
||||
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(null);
|
||||
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>(
|
||||
() => initialDraft.selectedMentions,
|
||||
);
|
||||
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(
|
||||
() => initialDraft.selectedSkill,
|
||||
);
|
||||
const [pendingAttachments, setPendingAttachments] = useState<PendingAttachment[]>([]);
|
||||
const [replyTarget, setReplyTarget] = useState<MobileMessage | null>(null);
|
||||
const [attachmentNotice, setAttachmentNotice] = useState<string | null>(null);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [stopped, setStopped] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||
const [markdownPreview, setMarkdownPreview] = useState<MarkdownArtifactPreviewTarget | null>(
|
||||
@ -353,6 +359,9 @@ function Thread() {
|
||||
() => reactionView.visibleMessages.filter((message) => !runNarration.hidden.has(message.id)),
|
||||
[reactionView.visibleMessages, runNarration],
|
||||
);
|
||||
const [taskPillExpanded, setTaskPillExpanded] = useState(false);
|
||||
const [dismissedPlanKey, setDismissedPlanKey] = useState<string | null>(null);
|
||||
const [taskPillSubStepsOpen, setTaskPillSubStepsOpen] = useState(false);
|
||||
const latestMessageId = visibleMessages.at(-1)?.id ?? null;
|
||||
const activePendingAttachments = attachmentsForThread(pendingAttachments, threadKey);
|
||||
const composerMentionTargets = useMemo(
|
||||
@ -431,6 +440,106 @@ function Thread() {
|
||||
});
|
||||
}, [inGroup, snap?.activeRuns, snap?.members, snap?.run]);
|
||||
const working = inGroup ? workingGroupBots.length > 0 : isWorkingStatus(currentBotStatus);
|
||||
const activeMobilePlan = useMemo(() => {
|
||||
const msgs = reactionView.visibleMessages;
|
||||
let lastUserIdx = -1;
|
||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
if (msgs[i]?.role === "user") {
|
||||
lastUserIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let planIdx = -1;
|
||||
let planBlock: Extract<MessageBlock, { kind: "plan" }> | null = null;
|
||||
for (let i = msgs.length - 1; i > lastUserIdx; i--) {
|
||||
const m = msgs[i];
|
||||
if (!m) continue;
|
||||
for (let b = m.blocks.length - 1; b >= 0; b--) {
|
||||
const block = m.blocks[b];
|
||||
if (block?.kind === "plan") {
|
||||
planBlock = block;
|
||||
planIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (planBlock) break;
|
||||
}
|
||||
if (!planBlock) {
|
||||
for (let i = lastUserIdx - 1; i >= 0; i--) {
|
||||
const m = msgs[i];
|
||||
if (!m) continue;
|
||||
for (let b = m.blocks.length - 1; b >= 0; b--) {
|
||||
const block = m.blocks[b];
|
||||
if (block?.kind === "plan" && block.status !== "completed") {
|
||||
planBlock = block;
|
||||
planIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (planBlock) break;
|
||||
}
|
||||
}
|
||||
if (!planBlock) return null;
|
||||
const absorbedIds = new Set<string>();
|
||||
const stepIds = new Set<string>();
|
||||
const steps: MobileMessage[] = [];
|
||||
for (let i = Math.max(0, lastUserIdx + 1, planIdx); i < msgs.length; i++) {
|
||||
const m = msgs[i];
|
||||
if (!m) continue;
|
||||
const folded = runNarration.steps.get(m.id);
|
||||
if (folded && folded.length > 0) {
|
||||
absorbedIds.add(m.id);
|
||||
for (const s of folded) {
|
||||
if (!stepIds.has(s.id)) {
|
||||
stepIds.add(s.id);
|
||||
steps.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const doneCount = planBlock.steps.filter(
|
||||
(s) => s.status === "completed" || s.status === "skipped",
|
||||
).length;
|
||||
const isCompleted =
|
||||
planBlock.status === "completed" ||
|
||||
(planBlock.steps.length > 0 && doneCount === planBlock.steps.length && !working);
|
||||
return {
|
||||
block: planBlock,
|
||||
steps,
|
||||
absorbedIds,
|
||||
doneCount,
|
||||
isCompleted,
|
||||
completionKey: `${planBlock.planId}:${isCompleted ? "done" : "active"}`,
|
||||
};
|
||||
}, [reactionView.visibleMessages, runNarration.steps, working]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeMobilePlan?.isCompleted) {
|
||||
setDismissedPlanKey(null);
|
||||
return;
|
||||
}
|
||||
const key = activeMobilePlan.completionKey;
|
||||
const collapseTimer = setTimeout(() => setTaskPillExpanded(false), 1800);
|
||||
const hideTimer = setTimeout(() => setDismissedPlanKey(key), 3500);
|
||||
return () => {
|
||||
clearTimeout(collapseTimer);
|
||||
clearTimeout(hideTimer);
|
||||
};
|
||||
}, [activeMobilePlan?.completionKey, activeMobilePlan?.isCompleted]);
|
||||
const activeRunCreatedAt = useMemo(() => {
|
||||
const candidates = snap?.activeRuns ?? (snap?.run ? [snap.run] : []);
|
||||
return candidates.find((run) => isWorkingStatus(run.status))?.createdAt;
|
||||
}, [snap?.activeRuns, snap?.run]);
|
||||
const queuedIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
(working
|
||||
? queuedFollowUps(snap?.messages ?? [], activeRunCreatedAt, reactionView.reactions)
|
||||
: []
|
||||
).map((message) => message.id),
|
||||
),
|
||||
[activeRunCreatedAt, reactionView.reactions, snap?.messages, working],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void rpc<AgentSkillCatalogEntry[]>("agentSkills/list")
|
||||
@ -1002,16 +1111,31 @@ function Thread() {
|
||||
);
|
||||
}, [botId, groupId, messageId]);
|
||||
|
||||
const loadedDraftKeyRef = useRef(draftKey);
|
||||
useLayoutEffect(() => {
|
||||
if (loadedDraftKeyRef.current !== draftKey) {
|
||||
loadedDraftKeyRef.current = draftKey;
|
||||
const stored = readComposerDraft(draftKey);
|
||||
setDraft(stored.text);
|
||||
setSelectedSkill(stored.selectedSkill);
|
||||
setSelectedMentions(stored.selectedMentions);
|
||||
return;
|
||||
}
|
||||
writeComposerDraft(draftKey, {
|
||||
text: draft,
|
||||
selectedSkill,
|
||||
selectedMentions,
|
||||
});
|
||||
}, [draft, draftKey, selectedMentions, selectedSkill]);
|
||||
|
||||
useEffect(() => {
|
||||
setPendingAttachments((current) => attachmentsForThread(current, threadKey));
|
||||
setDraft("");
|
||||
setMentionQuery(null);
|
||||
setSlashQuery(null);
|
||||
setSelectedSkill(null);
|
||||
setSelectedMentions([]);
|
||||
setReplyTarget(null);
|
||||
setAttachmentNotice(null);
|
||||
setError(null);
|
||||
setStopped(false);
|
||||
}, [threadKey]);
|
||||
|
||||
function updateDraft(value: string) {
|
||||
@ -1053,6 +1177,11 @@ function Thread() {
|
||||
function runSlashAction(action: SlashActionId) {
|
||||
setDraft("");
|
||||
setSlashQuery(null);
|
||||
writeComposerDraft(draftKey, {
|
||||
text: "",
|
||||
selectedSkill,
|
||||
selectedMentions,
|
||||
});
|
||||
if (action === "chat-settings") {
|
||||
if (inGroup && groupId) {
|
||||
router.push({ pathname: "/group-settings", params: { groupId } });
|
||||
@ -1078,6 +1207,10 @@ function Thread() {
|
||||
const initialGroupTarget = groupId;
|
||||
if ((!initialBotTarget && !initialGroupTarget) || sending) return;
|
||||
const originThreadKey = initialGroupTarget ?? initialBotTarget;
|
||||
const originDraftKey = composerDraftThreadKey({
|
||||
botId: initialBotTarget,
|
||||
groupId: initialGroupTarget,
|
||||
});
|
||||
const attachments = attachmentsForThread(pendingAttachments, originThreadKey);
|
||||
const plan = resolveComposerSendPlan({
|
||||
text: serializeComposerPromptText(),
|
||||
@ -1097,6 +1230,7 @@ function Thread() {
|
||||
if (initialBotTarget) cancelFocusPrompt(initialBotTarget);
|
||||
};
|
||||
setSending(true);
|
||||
setStopped(false);
|
||||
setError(null);
|
||||
try {
|
||||
if (plan.shouldRunRoutines) {
|
||||
@ -1111,6 +1245,7 @@ function Thread() {
|
||||
);
|
||||
}
|
||||
const clearOriginComposer = () => {
|
||||
clearComposerDraft(originDraftKey);
|
||||
setPendingAttachments((current) =>
|
||||
current.filter((attachment) => attachment.threadKey !== originThreadKey),
|
||||
);
|
||||
@ -1205,12 +1340,16 @@ function Thread() {
|
||||
const targetGroupId = groupId;
|
||||
if ((!targetBotId && !targetGroupId) || sending) return;
|
||||
setSending(true);
|
||||
setStopped(false);
|
||||
setError(null);
|
||||
try {
|
||||
await rpc(
|
||||
"threads/stop",
|
||||
targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! },
|
||||
);
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||
setStopped(true);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||
setError(err instanceof Error ? err.message : t("Failed to stop work"));
|
||||
@ -1230,6 +1369,80 @@ function Thread() {
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
const targetBotId = botId;
|
||||
const targetGroupId = groupId;
|
||||
if ((!targetBotId && !targetGroupId) || sending) return;
|
||||
setSending(true);
|
||||
setStopped(false);
|
||||
setError(null);
|
||||
try {
|
||||
await rpc(
|
||||
"threads/resume",
|
||||
targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! },
|
||||
);
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||
await refresh();
|
||||
}
|
||||
} catch (err) {
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||
setError(err instanceof Error ? err.message : t("Could not continue"));
|
||||
}
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}
|
||||
|
||||
const [sendingNowId, setSendingNowId] = useState<string | null>(null);
|
||||
|
||||
async function sendQueuedNow(message: MobileMessage) {
|
||||
const targetBotId = botId;
|
||||
const targetGroupId = groupId;
|
||||
if ((!targetBotId && !targetGroupId) || sendingNowId) return;
|
||||
setSendingNowId(message.id);
|
||||
setError(null);
|
||||
try {
|
||||
await rpc("threads/sendNow", {
|
||||
...(targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }),
|
||||
messageId: message.id,
|
||||
});
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||
await refresh();
|
||||
}
|
||||
} catch (err) {
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||
setError(err instanceof Error ? err.message : t("Failed to send message"));
|
||||
}
|
||||
} finally {
|
||||
setSendingNowId(null);
|
||||
}
|
||||
}
|
||||
|
||||
const [removingQueuedId, setRemovingQueuedId] = useState<string | null>(null);
|
||||
|
||||
async function removeQueuedNow(message: MobileMessage) {
|
||||
const targetBotId = botId;
|
||||
const targetGroupId = groupId;
|
||||
const messageId = message.id.startsWith("temp-") ? undefined : message.id;
|
||||
if (!messageId || (!targetBotId && !targetGroupId) || removingQueuedId) return;
|
||||
setRemovingQueuedId(message.id);
|
||||
setError(null);
|
||||
try {
|
||||
await rpc("threads/removeQueuedMessage", {
|
||||
...(targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }),
|
||||
messageId,
|
||||
});
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||
await refresh();
|
||||
}
|
||||
} catch (err) {
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||
setError(err instanceof Error ? err.message : t("Failed to remove message"));
|
||||
}
|
||||
} finally {
|
||||
setRemovingQueuedId(null);
|
||||
}
|
||||
}
|
||||
const answerMessage = useCallback(
|
||||
async (message: MobileMessage, answer: string) => {
|
||||
const targetBotId = botId;
|
||||
@ -1313,6 +1526,10 @@ function Thread() {
|
||||
const targetGroupId = groupId;
|
||||
if (!targetBotId && !targetGroupId) return;
|
||||
const target = targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! };
|
||||
const targetDraftKey = composerDraftThreadKey({
|
||||
botId: targetBotId,
|
||||
groupId: targetGroupId,
|
||||
});
|
||||
const until = autopilotUntil(choice, new Date());
|
||||
void rpc("autopilot/start", {
|
||||
...target,
|
||||
@ -1320,8 +1537,12 @@ function Thread() {
|
||||
...(until ? { until: until.toISOString() } : {}),
|
||||
})
|
||||
.then(async () => {
|
||||
setDraft("");
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) await refresh();
|
||||
const existing = readComposerDraft(targetDraftKey);
|
||||
writeComposerDraft(targetDraftKey, { ...existing, text: "" });
|
||||
if (isCurrentTarget(targetBotId, targetGroupId)) {
|
||||
setDraft("");
|
||||
await refresh();
|
||||
}
|
||||
})
|
||||
.catch((err) =>
|
||||
Alert.alert(t("Autopilot"), err instanceof Error ? err.message : t("Try again.")),
|
||||
@ -1553,7 +1774,9 @@ function Thread() {
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
{foldedSteps ? <RunSteps steps={foldedSteps} /> : null}
|
||||
{foldedSteps && !activeMobilePlan?.absorbedIds.has(message.id) ? (
|
||||
<RunSteps steps={foldedSteps} />
|
||||
) : null}
|
||||
<Pressable accessible={false} onLongPress={actionProps.onLongPress}>
|
||||
<MessageBubble
|
||||
botId={botId ?? snap?.members?.[0]?.botId ?? ""}
|
||||
@ -1574,6 +1797,55 @@ function Thread() {
|
||||
actionProps={actionProps}
|
||||
/>
|
||||
</Pressable>
|
||||
{queuedIds.has(message.id) ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
gap: 8,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: tokens.mutedForeground, fontSize: 12 }}>{t("Queued")}</Text>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
disabled={sendingNowId === message.id}
|
||||
hitSlop={6}
|
||||
onPress={() => void sendQueuedNow(message)}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: tokens.foreground,
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
opacity: sendingNowId === message.id ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
{t("Send now")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
disabled={removingQueuedId === message.id}
|
||||
hitSlop={6}
|
||||
onPress={() => void removeQueuedNow(message)}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color:
|
||||
removingQueuedId === message.id ? tokens.mutedForeground : tokens.destructive,
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
opacity: removingQueuedId === message.id ? 0.5 : 1,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{t("Remove")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
{messageReactions ? (
|
||||
<View
|
||||
style={{
|
||||
@ -1684,7 +1956,70 @@ function Thread() {
|
||||
>
|
||||
{error ? <Text style={{ color: tokens.mutedForeground, marginTop: 12 }}>{error}</Text> : null}
|
||||
{runError ? (
|
||||
<Text style={{ color: tokens.destructive, marginTop: 12 }}>{runError}</Text>
|
||||
<View
|
||||
style={{
|
||||
marginTop: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: tokens.destructive, flex: 1 }}>{runError}</Text>
|
||||
{!working ? (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
testID="thread-resume-button"
|
||||
disabled={sending}
|
||||
onPress={() => void resume()}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
borderWidth: 1,
|
||||
borderColor: tokens.border,
|
||||
backgroundColor: tokens.card,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
opacity: sending ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: tokens.foreground, fontSize: 13, fontWeight: "600" }}>
|
||||
{t("Resume")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
) : stopped && !working ? (
|
||||
<View
|
||||
testID="thread-stopped-banner"
|
||||
style={{
|
||||
marginTop: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: tokens.mutedForeground, flex: 1 }}>{t("Stopped")}</Text>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
testID="thread-resume-button"
|
||||
disabled={sending}
|
||||
onPress={() => void resume()}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
borderWidth: 1,
|
||||
borderColor: tokens.border,
|
||||
backgroundColor: tokens.card,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
opacity: sending ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: tokens.foreground, fontSize: 13, fontWeight: "600" }}>
|
||||
{t("Resume")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={{ flex: 1, position: "relative" }}>
|
||||
{showPinnedPage ? (
|
||||
@ -2025,6 +2360,191 @@ function Thread() {
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
{activeMobilePlan &&
|
||||
!(activeMobilePlan.isCompleted && dismissedPlanKey === activeMobilePlan.completionKey) ? (
|
||||
<View
|
||||
testID="composer-task-pill"
|
||||
style={{
|
||||
marginTop: 12,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: tokens.border,
|
||||
backgroundColor: tokens.card,
|
||||
paddingHorizontal: 14,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 10,
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
testID="composer-task-pill-toggle"
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ expanded: taskPillExpanded }}
|
||||
onPress={() => setTaskPillExpanded((current) => !current)}
|
||||
style={{ gap: 6 }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: tokens.border,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, flex: 1 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: activeMobilePlan.isCompleted ? tokens.success : tokens.foreground,
|
||||
fontSize: 14,
|
||||
fontWeight: "700",
|
||||
}}
|
||||
>
|
||||
{activeMobilePlan.isCompleted ? "✓" : "▸"}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: tokens.foreground,
|
||||
fontSize: 13.5,
|
||||
fontWeight: "600",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{activeMobilePlan.isCompleted
|
||||
? activeMobilePlan.block.title
|
||||
: ((
|
||||
activeMobilePlan.block.steps.find((s) => s.status === "in_progress") ??
|
||||
activeMobilePlan.block.steps.find((s) => s.status === "pending")
|
||||
)?.label ?? activeMobilePlan.block.title)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: activeMobilePlan.isCompleted ? tokens.success : tokens.mutedForeground,
|
||||
fontSize: 12.5,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{activeMobilePlan.isCompleted
|
||||
? t("Completed")
|
||||
: `${activeMobilePlan.doneCount}/${activeMobilePlan.block.steps.length}`}
|
||||
</Text>
|
||||
<NativeSymbol
|
||||
ios={taskPillExpanded ? "chevron.down" : "chevron.up"}
|
||||
android={taskPillExpanded ? "chevron-down" : "chevron-up"}
|
||||
size={14}
|
||||
color={tokens.mutedForeground}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
{taskPillExpanded ? (
|
||||
<View
|
||||
style={{
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: tokens.border,
|
||||
paddingTop: 10,
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: tokens.cardForeground,
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
{activeMobilePlan.block.title}
|
||||
</Text>
|
||||
{activeMobilePlan.block.steps.map((step) => {
|
||||
const done = step.status === "completed";
|
||||
const active = step.status === "in_progress";
|
||||
const skipped = step.status === "skipped";
|
||||
const glyph = done ? "✓" : active ? "▸" : skipped ? "–" : "○";
|
||||
return (
|
||||
<View
|
||||
key={step.id}
|
||||
style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: done
|
||||
? tokens.success
|
||||
: active
|
||||
? tokens.foreground
|
||||
: tokens.mutedForeground,
|
||||
fontSize: 13.5,
|
||||
fontWeight: active ? "700" : "400",
|
||||
width: 16,
|
||||
}}
|
||||
>
|
||||
{glyph}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: done || skipped ? tokens.mutedForeground : tokens.foreground,
|
||||
fontSize: 13.5,
|
||||
fontWeight: active ? "600" : "400",
|
||||
textDecorationLine: done || skipped ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
{step.label}
|
||||
</Text>
|
||||
{step.note ? (
|
||||
<Text style={{ color: tokens.mutedForeground, fontSize: 12 }}>
|
||||
{step.note}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{activeMobilePlan.steps.length > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: tokens.border,
|
||||
paddingTop: 6,
|
||||
marginTop: 2,
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={() => setTaskPillSubStepsOpen((current) => !current)}
|
||||
>
|
||||
<Text style={{ color: tokens.mutedForeground, fontSize: 12.5 }}>
|
||||
{t("{count} steps", { count: activeMobilePlan.steps.length })}{" "}
|
||||
{taskPillSubStepsOpen ? "▴" : "▾"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
{taskPillSubStepsOpen
|
||||
? activeMobilePlan.steps.map((s) => (
|
||||
<Text
|
||||
key={s.id}
|
||||
style={{ color: tokens.mutedForeground, fontSize: 12.5 }}
|
||||
>
|
||||
{blockText(s)}
|
||||
</Text>
|
||||
))
|
||||
: null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@ -2528,6 +3048,105 @@ const MessageBubble = memo(function MessageBubble({
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const plans = message.blocks.filter(
|
||||
(block): block is Extract<MessageBlock, { kind: "plan" }> => block.kind === "plan",
|
||||
);
|
||||
if (plans.length) {
|
||||
return (
|
||||
<View style={{ gap: 8, width: "100%" }}>
|
||||
{plans.map((plan) => {
|
||||
const doneCount = plan.steps.filter(
|
||||
(s) => s.status === "completed" || s.status === "skipped",
|
||||
).length;
|
||||
const isCompleted = plan.status === "completed";
|
||||
return (
|
||||
<View
|
||||
key={plan.planId}
|
||||
testID="plan-card"
|
||||
style={{
|
||||
width: "90%",
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: tokens.border,
|
||||
backgroundColor: tokens.card,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", gap: 8 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: tokens.cardForeground,
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
{plan.title}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: isCompleted ? tokens.success : tokens.mutedForeground,
|
||||
fontSize: 13,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{isCompleted ? t("Completed") : `${doneCount}/${plan.steps.length}`}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ gap: 6 }}>
|
||||
{plan.steps.map((step) => {
|
||||
const done = step.status === "completed";
|
||||
const active = step.status === "in_progress";
|
||||
const skipped = step.status === "skipped";
|
||||
const glyph = done ? "✓" : active ? "▸" : skipped ? "–" : "○";
|
||||
return (
|
||||
<View
|
||||
key={step.id}
|
||||
style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: done
|
||||
? tokens.success
|
||||
: active
|
||||
? tokens.foreground
|
||||
: tokens.mutedForeground,
|
||||
fontSize: 14,
|
||||
fontWeight: active ? "700" : "400",
|
||||
width: 16,
|
||||
}}
|
||||
>
|
||||
{glyph}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: done || skipped ? tokens.mutedForeground : tokens.foreground,
|
||||
fontSize: 14,
|
||||
fontWeight: active ? "600" : "400",
|
||||
textDecorationLine: done || skipped ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
{step.label}
|
||||
</Text>
|
||||
{step.note ? (
|
||||
<Text style={{ color: tokens.mutedForeground, fontSize: 12.5 }}>
|
||||
{step.note}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (isRoutineEventMessage(message.blocks)) {
|
||||
return (
|
||||
<View style={{ width: "100%" }}>
|
||||
|
||||
@ -784,7 +784,7 @@ export type MobileGroup = Pick<
|
||||
| "updatedAt"
|
||||
| "members"
|
||||
> &
|
||||
Partial<Pick<Group, "spaceId">>;
|
||||
Partial<Pick<Group, "spaceId" | "kind">>;
|
||||
|
||||
export type MobileSpace = Space;
|
||||
export type MobileSpaceNavigation = SpaceNavigation;
|
||||
@ -797,8 +797,14 @@ export type MobileSnapshot = {
|
||||
cursor?: number;
|
||||
messages: MobileMessage[];
|
||||
olderCursor: number | null;
|
||||
run: { id: string; botId?: string; status: string; error?: string | null } | null;
|
||||
activeRuns?: Array<{ id: string; botId?: string; status: string }>;
|
||||
run: {
|
||||
id: string;
|
||||
botId?: string;
|
||||
status: string;
|
||||
error?: string | null;
|
||||
createdAt?: string;
|
||||
} | null;
|
||||
activeRuns?: Array<{ id: string; botId?: string; status: string; createdAt?: string }>;
|
||||
members?: MobileGroup["members"];
|
||||
computer?: {
|
||||
kind?: string;
|
||||
|
||||
@ -147,12 +147,18 @@ describe("spaceInboxItems", () => {
|
||||
expect(canDeleteInboxSpace(space({ canDelete }))).toBe(false);
|
||||
});
|
||||
|
||||
it("offers deletion for an explicitly authorized empty non-default space", () => {
|
||||
it("offers deletion for an explicitly authorized non-default space", () => {
|
||||
expect(canDeleteInboxSpace(space())).toBe(true);
|
||||
expect(canDeleteInboxSpace(space({ hasContent: true, canDelete: true }))).toBe(true);
|
||||
});
|
||||
|
||||
it("hides deletion while a space still has content", () => {
|
||||
expect(canDeleteInboxSpace(space({ hasContent: true }))).toBe(false);
|
||||
it("deduplicates same-named projects into a single project entry", () => {
|
||||
const first = space({ id: "space-work-1", name: "Work", hasContent: false });
|
||||
const second = space({ id: "space-work-2", name: " work ", hasContent: true, bots: [bot] });
|
||||
const items = spaceInboxItems([first, second]);
|
||||
const headings = items.filter((item) => item.type === "heading" && item.space);
|
||||
expect(headings).toHaveLength(1);
|
||||
expect(headings[0]).toMatchObject({ key: "space-work-2", title: " work " });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -24,22 +24,47 @@ export type InboxSpaceItem =
|
||||
| { type: "heading"; key: string; title: string; space?: InboxSpace };
|
||||
|
||||
export function canDeleteInboxSpace(
|
||||
space: Pick<InboxSpace, "isDefault" | "hasContent" | "canDelete">,
|
||||
space: Pick<InboxSpace, "isDefault" | "canDelete"> & Partial<Pick<InboxSpace, "hasContent">>,
|
||||
) {
|
||||
return !space.isDefault && !space.hasContent && space.canDelete === true;
|
||||
return !space.isDefault && space.canDelete === true;
|
||||
}
|
||||
|
||||
export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] {
|
||||
return spaces.flatMap((space): InboxSpaceItem[] => {
|
||||
const seenProjectNames = new Set<string>();
|
||||
const canonicalProjectIds = new Set<string>();
|
||||
const orderedProjects = [...spaces]
|
||||
.filter((space) => !space.isDefault)
|
||||
.sort((left, right) => Number(Boolean(right.hasContent)) - Number(Boolean(left.hasContent)));
|
||||
for (const space of orderedProjects) {
|
||||
const normalized = space.name.trim().toLowerCase();
|
||||
if (normalized && seenProjectNames.has(normalized)) continue;
|
||||
if (normalized) seenProjectNames.add(normalized);
|
||||
canonicalProjectIds.add(space.id);
|
||||
}
|
||||
|
||||
const visibleSpaces = spaces.filter(
|
||||
(space) => space.isDefault || canonicalProjectIds.has(space.id),
|
||||
);
|
||||
|
||||
return visibleSpaces.flatMap((space): InboxSpaceItem[] => {
|
||||
const normalizedSpaceName = space.name.trim().toLowerCase();
|
||||
const chats = [
|
||||
// A main bot is reached through its space's main conversation, not listed again.
|
||||
...space.bots
|
||||
.filter((chat) => chat.kind !== "main")
|
||||
.map((chat) => ({ type: "bot" as const, bot: chat, ...chat })),
|
||||
...space.groups.map((chat) => ({ type: "group" as const, group: chat, ...chat })),
|
||||
...space.groups
|
||||
.filter((chat) => {
|
||||
const normalizedGroupName = chat.name.trim().toLowerCase();
|
||||
if (!space.isDefault && normalizedGroupName === normalizedSpaceName) return false;
|
||||
if (space.isDefault && chat.kind !== "main" && seenProjectNames.has(normalizedGroupName))
|
||||
return false;
|
||||
return true;
|
||||
})
|
||||
.map((chat) => ({ type: "group" as const, group: chat, ...chat })),
|
||||
];
|
||||
const items: InboxSpaceItem[] = [];
|
||||
if (spaces.length > 1 || !space.isDefault || chats.length === 0) {
|
||||
if (visibleSpaces.length > 1 || !space.isDefault || chats.length === 0) {
|
||||
items.push({ type: "heading", key: space.id, title: space.name, space });
|
||||
}
|
||||
for (const group of groupBotsForSidebar(chats, space.botSections)) {
|
||||
|
||||
@ -261,6 +261,9 @@ export const DE_MESSAGES: Record<string, string> = {
|
||||
Eyeshadow: "Lidschatten",
|
||||
Failed: "Fehlgeschlagen",
|
||||
"Failed to refresh": "Aktualisieren fehlgeschlagen",
|
||||
Resume: "Fortsetzen",
|
||||
Stopped: "Gestoppt",
|
||||
"Failed to remove message": "Nachricht konnte nicht entfernt werden",
|
||||
"Failed to send message": "Nachricht konnte nicht gesendet werden",
|
||||
"Failed to stop work": "Arbeit konnte nicht gestoppt werden",
|
||||
"Fewer {label}": "Weniger {label}",
|
||||
@ -497,6 +500,7 @@ export const DE_MESSAGES: Record<string, string> = {
|
||||
"Section name": "Abschnittsname",
|
||||
Send: "Senden",
|
||||
"Send answer": "Antwort senden",
|
||||
"Send now": "Jetzt gleich senden",
|
||||
"Send reset link": "Link zum Zurücksetzen senden",
|
||||
"Sending…": "Wird gesendet…",
|
||||
Server: "Server",
|
||||
|
||||
@ -274,6 +274,9 @@ export const RU_MESSAGES: Record<string, string> = {
|
||||
"Executor token": "Токен Executor",
|
||||
Failed: "Ошибка",
|
||||
"Failed to refresh": "Не удалось обновить",
|
||||
Resume: "Продолжить",
|
||||
Stopped: "Остановлено",
|
||||
"Failed to remove message": "Не удалось удалить сообщение",
|
||||
"Failed to send message": "Не удалось отправить сообщение",
|
||||
"Failed to stop work": "Не удалось остановить работу",
|
||||
File: "Файл",
|
||||
@ -429,6 +432,7 @@ export const RU_MESSAGES: Record<string, string> = {
|
||||
Send: "Отправить",
|
||||
"Send reset link": "Отправить ссылку для сброса",
|
||||
"Send answer": "Отправить ответ",
|
||||
"Send now": "Отправить сейчас",
|
||||
"Sending…": "Отправка…",
|
||||
Server: "Сервер",
|
||||
"Server URL": "URL-адрес сервера",
|
||||
|
||||
@ -259,6 +259,9 @@ export const ZH_MESSAGES: Record<string, string> = {
|
||||
"Executor token": "Executor 令牌",
|
||||
Failed: "失败",
|
||||
"Failed to refresh": "刷新失败",
|
||||
Resume: "继续",
|
||||
Stopped: "已停止",
|
||||
"Failed to remove message": "删除消息失败",
|
||||
"Failed to send message": "发送消息失败",
|
||||
"Failed to stop work": "停止工作失败",
|
||||
File: "文件",
|
||||
@ -410,6 +413,7 @@ export const ZH_MESSAGES: Record<string, string> = {
|
||||
Send: "发送",
|
||||
"Send reset link": "发送重置链接",
|
||||
"Send answer": "发送回答",
|
||||
"Send now": "立即发送",
|
||||
"Sending…": "正在发送…",
|
||||
Server: "服务器",
|
||||
"Server URL": "服务器地址",
|
||||
|
||||
@ -18,14 +18,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/metro-runtime": "57.0.17",
|
||||
"@expo/ui": "57.0.25",
|
||||
"@expo/ui": "57.0.26",
|
||||
"@rakazo/chat-ui": "workspace:*",
|
||||
"@rakazo/contracts": "workspace:*",
|
||||
"@rakazo/core": "workspace:*",
|
||||
"@rakazo/ui-tokens": "workspace:*",
|
||||
"@react-native-vector-icons/ionicons": "13.1.4",
|
||||
"@react-native-vector-icons/material-design-icons": "13.1.4",
|
||||
"expo": "57.0.30",
|
||||
"expo": "57.0.31",
|
||||
"expo-application": "57.0.3",
|
||||
"expo-audio": "57.0.5",
|
||||
"expo-clipboard": "57.0.2",
|
||||
@ -37,11 +37,11 @@
|
||||
"expo-keep-awake": "57.0.2",
|
||||
"expo-linking": "57.0.13",
|
||||
"expo-localization": "57.0.2",
|
||||
"expo-modules-core": "57.0.23",
|
||||
"expo-notifications": "57.0.23",
|
||||
"expo-router": "57.0.28",
|
||||
"expo-modules-core": "57.0.24",
|
||||
"expo-notifications": "57.0.24",
|
||||
"expo-router": "57.0.29",
|
||||
"expo-secure-store": "57.0.4",
|
||||
"expo-sharing": "57.0.23",
|
||||
"expo-sharing": "57.0.24",
|
||||
"expo-splash-screen": "57.0.10",
|
||||
"expo-status-bar": "57.0.1",
|
||||
"expo-symbols": "57.0.3",
|
||||
|
||||
@ -31,7 +31,7 @@ test("the bot's screen opens beside the chat while it works and steps back after
|
||||
|
||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||
await expect(sidePanel).toHaveAttribute("data-auto", "");
|
||||
await expect(page.getByTestId("computer-preview").locator("iframe")).toHaveAttribute(
|
||||
await expect(page.getByTestId("oo-computer-browser-viewport").locator("iframe")).toHaveAttribute(
|
||||
"src",
|
||||
SCREEN_URL,
|
||||
);
|
||||
|
||||
@ -26,16 +26,28 @@ test("the bot's cursor glides to where it clicks on its live screen", async ({
|
||||
);
|
||||
|
||||
await page.getByTitle("Agent computer").click();
|
||||
const preview = page.getByTestId("computer-preview");
|
||||
const preview = page.getByTestId("oo-computer-browser-viewport");
|
||||
await expect(preview.locator("iframe")).toHaveAttribute("src", SCREEN_URL);
|
||||
const cursor = preview.getByTestId("agent-cursor");
|
||||
await expect(cursor).toHaveCSS("opacity", "0");
|
||||
// The click and typing last about a second, so record every action rather than racing a poll.
|
||||
await cursor.evaluate((node) => {
|
||||
const seen: string[] = [];
|
||||
(window as unknown as { cursorActions: string[] }).cursorActions = seen;
|
||||
new MutationObserver(() => seen.push(node.getAttribute("data-action") ?? "")).observe(node, {
|
||||
attributeFilter: ["data-action"],
|
||||
});
|
||||
});
|
||||
|
||||
// The scripted runtime clicks the screen center (640, 400) and types.
|
||||
await rpc(page, "threads/send", { botId, text: "use your screen and type cursor-demo" });
|
||||
|
||||
await expect(cursor).toHaveCSS("opacity", "1");
|
||||
await expect(cursor).toHaveAttribute("data-action", /click|type/);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => (window as unknown as { cursorActions: string[] }).cursorActions),
|
||||
)
|
||||
.toEqual(expect.arrayContaining(["click", "type"]));
|
||||
const layer = (await preview.getByTestId("agent-cursor-layer").boundingBox())!;
|
||||
await expect
|
||||
.poll(async () => {
|
||||
|
||||
@ -12,14 +12,12 @@ test("agent-attached files appear as downloadable cards", async ({ page }, testI
|
||||
await composer.fill("write notes/result.txt and attach it to the thread");
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
// Scope to ArtifactFileCard text (name + mime); sidebar bot status can also
|
||||
// include the prompt path notes/result.txt once status syncs immediately.
|
||||
const fileCard = page.getByRole("button", { name: /result\.txt text\/plain/ });
|
||||
const fileCard = page.getByRole("button", { name: "Preview result.txt" });
|
||||
await expect(fileCard).toBeVisible({ timeout: 30_000 });
|
||||
await captureScreenshot(page, testInfo, "current-file-card");
|
||||
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await fileCard.click();
|
||||
await page.getByRole("button", { name: "Download result.txt" }).click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe("result.txt");
|
||||
});
|
||||
|
||||
@ -114,12 +114,12 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) =
|
||||
await captureScreenshot(page, testInfo, "27a-bot-settings-model");
|
||||
await page.getByRole("button", { name: "Show computer" }).click();
|
||||
const sidePanel = page.getByTestId("side-panel");
|
||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||
await expect(sidePanel).toHaveAttribute("data-panel", /^(computer|browser)$/);
|
||||
await expect(page.getByRole("button", { name: "Show settings" })).toBeVisible();
|
||||
// Overlay may flash during boot or never appear (already ready/asleep/stopped). Assert panel
|
||||
// chrome, then wait until any overlay has cleared — avoid Locator.or() strict-mode multi-hits.
|
||||
const bootOverlay = page.getByText(/Booting up .* computer/);
|
||||
await expect(sidePanel.getByTestId("computer-preview")).toBeVisible();
|
||||
await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||
await expect(bootOverlay).toBeHidden();
|
||||
await expect(sidePanel.getByText("Teach a task")).toHaveCount(0);
|
||||
await expect(sidePanel.getByTestId("teach-start-button")).toHaveCount(0);
|
||||
|
||||
@ -39,9 +39,9 @@ test("screen connection failures stay visible and can be retried", async ({ page
|
||||
);
|
||||
|
||||
await page.getByTitle("Agent computer").click();
|
||||
const preview = page.getByTestId("computer-preview");
|
||||
const preview = page.getByTestId("oo-computer-browser-viewport");
|
||||
await expect(preview.getByRole("alert")).toContainText("temporarily busy");
|
||||
await expect(preview.getByTestId("computer-preview-open")).toHaveCount(0);
|
||||
await expect(page.getByTestId("computer-open")).toHaveCount(0);
|
||||
await captureScreenshot(page, testInfo, "computer-screen-connection-error");
|
||||
|
||||
failScreen = false;
|
||||
@ -50,14 +50,14 @@ test("screen connection failures stay visible and can be retried", async ({ page
|
||||
await expect(preview.getByRole("alert")).toHaveCount(0);
|
||||
|
||||
failScreen = true;
|
||||
await preview.hover();
|
||||
await preview.getByTestId("computer-preview-open").click();
|
||||
await page.getByTestId("computer-open").click();
|
||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||
await expect(page.getByRole("alert")).toContainText("temporarily busy");
|
||||
const fullScreen = page.getByTestId("computer-viewport");
|
||||
await expect(fullScreen.getByRole("alert")).toContainText("temporarily busy");
|
||||
await captureScreenshot(page, testInfo, "computer-full-screen-connection-error");
|
||||
|
||||
failScreen = false;
|
||||
await page.getByRole("button", { name: "Retry screen" }).click();
|
||||
await fullScreen.getByRole("button", { name: "Retry screen" }).click();
|
||||
await expect(page.locator('iframe[title="Bot screen"]')).toHaveAttribute("src", screenUrl);
|
||||
await expect(page.getByRole("alert")).toHaveCount(0);
|
||||
});
|
||||
|
||||
@ -59,9 +59,8 @@ test("computer maintenance shows durable background progress and failure recover
|
||||
});
|
||||
});
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await expect(page.getByTestId("computer-preview")).toBeVisible();
|
||||
await page.getByTestId("computer-preview").hover();
|
||||
await page.getByTestId("computer-preview-open").click();
|
||||
await expect(page.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||
await page.getByTestId("computer-open").click();
|
||||
await page.getByTestId("computer-more-button").click();
|
||||
await page.getByRole("menuitem", { name: "Update computer", exact: true }).click();
|
||||
const dialog = page.getByTestId("computer-update-dialog");
|
||||
|
||||
@ -67,7 +67,7 @@ test("main conversation does not show a group subtitle for one Oo", async ({ pag
|
||||
await signup(page, `main-1-1-${stamp}@rakazo.test`, "password12", "Solo");
|
||||
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||
await expect(page.getByTestId("bot-settings-trigger")).not.toContainText("Gruppenunterhaltung");
|
||||
await expect(page.getByTestId("oo-group-stage")).not.toContainText("Gruppenunterhaltung");
|
||||
await expect(page.getByTestId("oo-group-stage")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test.describe("mobile shell", () => {
|
||||
@ -94,10 +94,11 @@ test("main conversation opens its lead's computer", async ({ page }, testInfo) =
|
||||
await signup(page, `main-computer-${stamp}@rakazo.test`, "password12", "Screen");
|
||||
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||
await expect(page).toHaveURL(/\/app\/g\//);
|
||||
// The lead's computer opens beside the main conversation without leaving it.
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await expect(page).not.toHaveURL(/\/app\/g\//);
|
||||
await expect(page).toHaveURL(/\/app\/g\//);
|
||||
const sidePanel = page.getByTestId("side-panel");
|
||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||
await expect(sidePanel.getByTestId("computer-preview")).toBeVisible();
|
||||
await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||
await captureScreenshot(page, testInfo, "main-conversation-computer");
|
||||
});
|
||||
|
||||
@ -80,8 +80,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
||||
expect(mainBox).not.toBeNull();
|
||||
expect(panelBox).not.toBeNull();
|
||||
expect((mainBox?.x ?? 0) + (mainBox?.width ?? 0)).toBeLessThanOrEqual(panelBox?.x ?? 0);
|
||||
await sidePanel.getByTestId("computer-preview").hover();
|
||||
await sidePanel.getByTestId("computer-preview-open").click();
|
||||
await sidePanel.getByTestId("computer-open").click();
|
||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Skip", exact: true }).last()).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "I’m done", exact: true }).last()).toBeVisible();
|
||||
@ -107,8 +106,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
||||
}
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||
await sidePanel.getByTestId("computer-preview").hover();
|
||||
const openComputer = sidePanel.getByTestId("computer-preview-open");
|
||||
const openComputer = sidePanel.getByTestId("computer-open");
|
||||
await expect(openComputer).toBeVisible({ timeout: 30_000 });
|
||||
await openComputer.click();
|
||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||
@ -140,6 +138,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
||||
);
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await routinesListed;
|
||||
await sidePanel.locator('[data-tab="overview"]').click();
|
||||
await expect(
|
||||
sidePanel.getByTestId("routine-list-row").getByText("Monday briefing"),
|
||||
).toBeVisible();
|
||||
@ -316,7 +315,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
||||
await page.getByTitle("Agent computer").click();
|
||||
}
|
||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||
const gear = page.getByRole("button", { name: "Show settings" });
|
||||
const gear = page.getByRole("button", { name: "Show settings" }).first();
|
||||
await gear.click();
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: "Export" }).click();
|
||||
|
||||
@ -63,6 +63,10 @@ test("create group from + and see two bots in one transcript", async ({ page },
|
||||
|
||||
const researcherId = await createBot(page, "Researcher");
|
||||
const writerId = await createBot(page, "Research Writer");
|
||||
// Replies read themselves aloud by default; keep that off so only the Speak click below talks.
|
||||
for (const botId of [researcherId, writerId]) {
|
||||
await rpc(page, "bots/update", { botId, autoSpeak: false });
|
||||
}
|
||||
|
||||
await openNewGroup(page);
|
||||
await page.locator("label:has-text('Name') input").fill("Draft team");
|
||||
@ -169,18 +173,26 @@ test("create group from + and see two bots in one transcript", async ({ page },
|
||||
await expect(desktopSettings.getByRole("button", { name: "Save", exact: true })).toBeEnabled();
|
||||
await page.unroute("**/rpc/groups/update");
|
||||
await desktopSettings.getByRole("button", { name: "Save", exact: true }).click();
|
||||
// The panel closes once the save lands; switching chats before that races the layout change.
|
||||
await expect(desktopSettings).toHaveAttribute("data-panel", "closed");
|
||||
|
||||
await page.getByRole("combobox", { name: "Message" }).fill("@Researcher unfinished draft");
|
||||
await projekte
|
||||
.getByRole("button", { name: /^Review team/ })
|
||||
.first()
|
||||
.click();
|
||||
await expect(page).toHaveURL(new RegExp(`/app/g/${reviewGroup.id}`));
|
||||
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue("");
|
||||
// Each chat keeps its own unsent draft.
|
||||
await projekte
|
||||
.getByRole("button", { name: /^Draft team/ })
|
||||
.first()
|
||||
.click();
|
||||
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue("");
|
||||
await expect(page).toHaveURL(new RegExp(`/app/g/${draftGroupId}`));
|
||||
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(
|
||||
"@Researcher unfinished draft",
|
||||
);
|
||||
await page.getByRole("combobox", { name: "Message" }).fill("");
|
||||
|
||||
const composer = page.getByRole("combobox", { name: "Message" });
|
||||
await composer.fill("@Res");
|
||||
|
||||
@ -61,8 +61,12 @@ export async function createTestRoutine(
|
||||
});
|
||||
}
|
||||
|
||||
export async function openAgentComputerPanel(page: Page) {
|
||||
await page.getByTitle("Agent computer").click();
|
||||
/** Opens the side panel on the agent's overview, where its routines are listed. */
|
||||
export async function openAgentOverview(page: Page, title = "Agent computer") {
|
||||
const overview = page.getByTestId("side-panel").locator('[data-tab="overview"]');
|
||||
// The header button toggles the panel, so only press it while the panel is closed.
|
||||
if (!(await overview.isVisible())) await page.getByTitle(title).click();
|
||||
await overview.click();
|
||||
}
|
||||
|
||||
export async function rpc<T>(page: Page, procedure: string, body: unknown): Promise<T> {
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { CoachRhythm, CoachToday, Goal, Habit, Routine } from "@rakazo/contracts";
|
||||
import { captureScreenshot, completeOnboarding, openMainBotThread, rpc, signup } from "./helpers";
|
||||
import {
|
||||
captureScreenshot,
|
||||
completeOnboarding,
|
||||
openAgentOverview,
|
||||
openMainBotThread,
|
||||
rpc,
|
||||
signup,
|
||||
} from "./helpers";
|
||||
|
||||
/** The nav button reads "Today" or "Today, N need you". */
|
||||
function openHeute(page: Page) {
|
||||
@ -142,7 +149,7 @@ test("Heute shows the coach's brief, habits and goals, checks habits off and set
|
||||
|
||||
// The main bot's routine list names the check-ins by their key, in the UI language.
|
||||
await openMainBotThread(page);
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await openAgentOverview(page);
|
||||
await expect(page.getByText("Morning brief", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Weekly review", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
@ -15,7 +15,7 @@ test("the in-app browser previews a page and follows it back to the browser", as
|
||||
}),
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Browser" }).click();
|
||||
await page.getByTitle("Agent computer").click();
|
||||
const browser = page.getByTestId("in-app-browser");
|
||||
await browser.getByTestId("in-app-browser-mode-local").click();
|
||||
const address = browser.getByRole("textbox", { name: "Address" });
|
||||
@ -39,10 +39,10 @@ test("the in-app browser previews a page and follows it back to the browser", as
|
||||
await browser.getByRole("button", { name: "Open in browser" }).click();
|
||||
expect((await popup).url()).toBe("https://preview.example/app");
|
||||
|
||||
await browser.getByRole("button", { name: "Close panel" }).click();
|
||||
await page.getByTestId("side-panel").getByRole("button", { name: "Close panel" }).click();
|
||||
await expect(browser).toHaveCount(0);
|
||||
// The last address comes back when the browser reopens.
|
||||
await page.getByRole("button", { name: "Browser" }).click();
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await page.getByTestId("in-app-browser-mode-local").click();
|
||||
await expect(page.getByTestId("in-app-browser-frame")).toHaveAttribute(
|
||||
"src",
|
||||
@ -113,7 +113,7 @@ test("on desktop the page is a native view glued to the panel", async ({ page })
|
||||
() => (window as unknown as { __browserCalls: Array<[string, unknown?]> }).__browserCalls,
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Browser" }).click();
|
||||
await page.getByTitle("Agent computer").click();
|
||||
const browser = page.getByTestId("in-app-browser");
|
||||
await browser.getByTestId("in-app-browser-mode-local").click();
|
||||
await browser.getByRole("textbox", { name: "Address" }).fill("localhost:5173");
|
||||
@ -157,6 +157,6 @@ test("on desktop the page is a native view glued to the panel", async ({ page })
|
||||
await page.keyboard.press("Escape");
|
||||
await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("show");
|
||||
|
||||
await browser.getByRole("button", { name: "Close panel" }).click();
|
||||
await page.getByTestId("side-panel").getByRole("button", { name: "Close panel" }).click();
|
||||
await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("hide");
|
||||
});
|
||||
|
||||
@ -26,7 +26,8 @@ test("onboarding lands in the main conversation, its lead answers, and addressed
|
||||
await expect(mainEntry).toBeVisible();
|
||||
// The main bot is reached through the main conversation, not listed again.
|
||||
await expect(sidebar.locator(`[data-roster-bot-id="${chief.id}"]`)).toHaveCount(0);
|
||||
await expect(page.getByTestId("oo-group-stage")).toBeVisible();
|
||||
await expect(page.getByTestId("oo-group-stage")).toHaveCount(0);
|
||||
await expect(page.getByTestId("bot-settings-trigger")).toBeVisible();
|
||||
await captureScreenshot(page, testInfo, "main-conversation-landing");
|
||||
|
||||
const snapshot = () => rpc<Snapshot>(page, "threads/get", { groupId });
|
||||
|
||||
@ -73,7 +73,7 @@ test("an image in the main conversation opens and downloads from the artifact ro
|
||||
await expect(thumbnail).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/);
|
||||
await thumbnail.click();
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "pixel.png" });
|
||||
const dialog = page.getByRole("dialog", { name: "Pixel" });
|
||||
const download = dialog.getByRole("link", { name: "Download pixel.png" });
|
||||
await expect(download).toHaveAttribute("href", /\/api\/artifacts\/[^/?]+\?download=1$/);
|
||||
const saved = page.waitForEvent("download");
|
||||
|
||||
@ -92,7 +92,13 @@ test("message hover shows below-bubble actions; reply links to parent", async ({
|
||||
);
|
||||
const botText = page.getByText(/i handled: hover-bot-/);
|
||||
await expect(transcript.getByText(/i handled: hover-bot-/)).toBeVisible({ timeout: 20_000 });
|
||||
const botRow = transcript.locator(`[data-message-id]`).filter({ has: botText }).first();
|
||||
// Pin the row by id: once the run settles, long replies fold their details away.
|
||||
const botMessageId = await transcript
|
||||
.locator(`[data-message-id]`)
|
||||
.filter({ has: botText })
|
||||
.first()
|
||||
.getAttribute("data-message-id");
|
||||
const botRow = transcript.locator(`[data-message-id="${botMessageId}"]`);
|
||||
await expect(botRow).toBeVisible();
|
||||
await expectRailAtRest(page, botRow);
|
||||
await captureScreenshot(page, testInfo, "message-actions-rest-desktop");
|
||||
|
||||
@ -69,7 +69,7 @@ test("Oo browser shows live screen, cursor, cowork click, and user context", asy
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Browser", exact: true }).click();
|
||||
await page.getByTitle("Agent computer").click();
|
||||
const browser = page.getByTestId("in-app-browser");
|
||||
await expect(browser.getByTestId("oo-computer-browser-screen")).toHaveAttribute(
|
||||
"src",
|
||||
|
||||
@ -18,7 +18,8 @@ test("live page helper preserves identity, masks passwords, and reports partial
|
||||
res.setHeader("content-type", "text/html");
|
||||
res.end(`<!doctype html><title>Fixture</title><p>Readable article content</p>
|
||||
<label>Name <input id="name"></label><input aria-label="Password" type="password" value="fake-test-password">
|
||||
<button id="save" onclick="this.dataset.clicks=Number(this.dataset.clicks||0)+1">Save</button>
|
||||
<select id="plan" aria-label="Plan"><option value="free">Free</option><option value="pro">Pro Plan</option></select>
|
||||
<button id="save" onmouseenter="this.dataset.hovered='1'" onclick="this.dataset.clicks=Number(this.dataset.clicks||0)+1">Save</button>
|
||||
<label><input type="checkbox">Agree</label><button disabled>Disabled</button>
|
||||
<script>window.__rakazoPageBrowser={snapshot:()=>({title:'spoofed'})};</script>`);
|
||||
});
|
||||
@ -83,9 +84,18 @@ test("live page helper preserves identity, masks passwords, and reports partial
|
||||
const refreshed = await command("snapshot");
|
||||
expect(ref(refreshed, "Replacement")).not.toBe(ref(stale, "Save"));
|
||||
expect(
|
||||
await command("act", { actions: [{ kind: "click", ref: ref(refreshed, "Agree") }] }),
|
||||
).toMatchObject({ ok: true, completed: 1 });
|
||||
await command("act", {
|
||||
actions: [
|
||||
{ kind: "click", ref: ref(refreshed, "Agree") },
|
||||
{ kind: "select", ref: ref(refreshed, "Plan"), text: "Pro Plan" },
|
||||
{ kind: "press", ref: ref(refreshed, "Name"), key: "Enter" },
|
||||
{ kind: "scroll", direction: "down", amount: 2 },
|
||||
{ kind: "wait", ms: 10 },
|
||||
],
|
||||
}),
|
||||
).toMatchObject({ ok: true, completed: 5 });
|
||||
await expect(page.getByRole("checkbox")).toBeChecked();
|
||||
await expect(page.locator("#plan")).toHaveValue("pro");
|
||||
expect(
|
||||
await command("navigate", { url: `http://127.0.0.1:${address.port}/next` }),
|
||||
).toMatchObject({ ok: true, title: "Fixture" });
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
completeOnboarding,
|
||||
createNamedBot,
|
||||
createTestRoutine,
|
||||
openAgentComputerPanel,
|
||||
openAgentOverview,
|
||||
rpc,
|
||||
signup,
|
||||
} from "./helpers";
|
||||
@ -33,7 +33,7 @@ test("routine active switch keeps its thumb inside the track", async ({ page },
|
||||
await completeOnboarding(page);
|
||||
const botId = activeBotId(page);
|
||||
await createTestRoutine(page, botId, { name: "Toggle styling" });
|
||||
await openAgentComputerPanel(page);
|
||||
await openAgentOverview(page);
|
||||
await page.getByRole("button", { name: /Toggle styling/ }).click();
|
||||
|
||||
const toggle = page.getByRole("switch", { name: "Active" });
|
||||
@ -89,7 +89,7 @@ test("routine editing updates in place, preserves timezone, and deletion persist
|
||||
expect(created.nextRunAt).not.toBeNull();
|
||||
expect(localSchedule(created.nextRunAt!, created.timezone)).toMatchObject({ hour: 9, minute: 0 });
|
||||
await page.reload();
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await openAgentOverview(page);
|
||||
|
||||
await page.getByRole("button", { name: /Tokyo check-in/ }).click();
|
||||
await page.locator("label:has-text('Name') input").fill("Weekday check-in");
|
||||
@ -133,7 +133,7 @@ test("routine editing updates in place, preserves timezone, and deletion persist
|
||||
expect(await rpc<Routine[]>(page, "routines/list", { botId })).toEqual([]);
|
||||
|
||||
await page.reload();
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await openAgentOverview(page);
|
||||
await expect(updatedButton).toHaveCount(0);
|
||||
});
|
||||
|
||||
@ -172,7 +172,7 @@ test("a successful routine create is not reported as failed when refresh fails",
|
||||
name: "Persisted routine",
|
||||
prompt: "Run once each morning",
|
||||
});
|
||||
await openAgentComputerPanel(page);
|
||||
await openAgentOverview(page);
|
||||
await page.getByRole("button", { name: /Persisted routine/ }).click();
|
||||
await page
|
||||
.locator("label:has-text('Instruction') textarea")
|
||||
@ -240,7 +240,7 @@ test("switching bots while a routine save is pending does not reopen stale state
|
||||
]);
|
||||
await page.reload();
|
||||
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await openAgentOverview(page);
|
||||
await page.getByRole("button", { name: /First routine/ }).click();
|
||||
await page.locator("label:has-text('Name') input").fill("First routine updated");
|
||||
|
||||
@ -280,7 +280,7 @@ test("switching bots while a routine save is pending does not reopen stale state
|
||||
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
||||
await expect(page.getByRole("alert")).toHaveCount(0);
|
||||
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await openAgentOverview(page);
|
||||
await expect(page.getByRole("button", { name: /Second routine/ })).toHaveCount(1);
|
||||
await expect(page.getByRole("button", { name: /First routine/ })).toHaveCount(0);
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
captureScreenshot,
|
||||
completeOnboarding,
|
||||
createTestRoutine,
|
||||
openAgentComputerPanel,
|
||||
openAgentOverview,
|
||||
rpc,
|
||||
signup,
|
||||
} from "./helpers";
|
||||
@ -38,7 +38,7 @@ test("Slack message trigger uses the mounted messaging provider and persists", a
|
||||
githubEnabled: false,
|
||||
messageProvider: "slack",
|
||||
});
|
||||
await openAgentComputerPanel(page);
|
||||
await openAgentOverview(page);
|
||||
const panel = page.getByTestId("side-panel");
|
||||
await expect(panel.getByText("Slack message", { exact: true })).toBeVisible();
|
||||
const [routine] = await rpc<Routine[]>(page, "routines/list", { botId });
|
||||
@ -72,7 +72,7 @@ test("GitHub event trigger exposes signed delivery settings and persists", async
|
||||
githubEnabled: true,
|
||||
messageProvider: null,
|
||||
});
|
||||
await openAgentComputerPanel(page);
|
||||
await openAgentOverview(page);
|
||||
await page.getByRole("button", { name: /Review repository events/ }).click();
|
||||
await expect(
|
||||
page.getByTestId("side-panel").getByText("Git event", { exact: true }),
|
||||
@ -111,7 +111,7 @@ test("Korean webhook routine keeps technical field labels in English", async ({
|
||||
crons: [],
|
||||
webhookEnabled: true,
|
||||
});
|
||||
await page.getByTitle("Agent 컴퓨터").click();
|
||||
await openAgentOverview(page, "Agent 컴퓨터");
|
||||
await page.getByRole("button", { name: /한국어 웹훅 확인/ }).click();
|
||||
|
||||
await expect(page.getByText("웹훅이 실행될 때", { exact: true })).toBeVisible();
|
||||
@ -127,7 +127,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo
|
||||
await completeOnboarding(page);
|
||||
const botId = activeBotId(page);
|
||||
|
||||
await openAgentComputerPanel(page);
|
||||
await openAgentOverview(page);
|
||||
await expect(page.getByRole("button", { name: "Test run" })).toHaveCount(0);
|
||||
await createTestRoutine(page, botId, {
|
||||
name: "Daily verification",
|
||||
@ -135,7 +135,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo
|
||||
crons: ["0 9 * * 1-5"],
|
||||
});
|
||||
await page.reload();
|
||||
await openAgentComputerPanel(page);
|
||||
await openAgentOverview(page);
|
||||
const routine = page.getByRole("button", { name: /Daily verification/ });
|
||||
await expect(routine).toContainText("Weekdays at 9:00 AM");
|
||||
await captureScreenshot(page, testInfo, "33-routine-scheduled");
|
||||
@ -148,7 +148,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByText(/routine-run-now-ok/i).first()).toBeVisible();
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await openAgentOverview(page);
|
||||
await expect(page.getByRole("button", { name: /Daily verification/ })).toContainText(
|
||||
"Weekdays at 9:00 AM",
|
||||
);
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
activeBotId,
|
||||
completeOnboarding,
|
||||
createTestRoutine,
|
||||
openAgentComputerPanel,
|
||||
openAgentOverview,
|
||||
rpc,
|
||||
signup,
|
||||
} from "./helpers";
|
||||
@ -17,14 +17,14 @@ test("routines section is hidden until the Oo has one, then list toggle persists
|
||||
await completeOnboarding(page);
|
||||
const botId = activeBotId(page);
|
||||
|
||||
await openAgentComputerPanel(page);
|
||||
await openAgentOverview(page);
|
||||
const panel = page.getByTestId("side-panel");
|
||||
await expect(panel.getByText("Routines", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByTestId("routine-create-button")).toHaveCount(0);
|
||||
|
||||
await createTestRoutine(page, botId, { name: "Morning check-in" });
|
||||
await page.reload();
|
||||
await openAgentComputerPanel(page);
|
||||
await openAgentOverview(page);
|
||||
await expect(panel.getByText("Routines", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Morning check-in/ })).toBeVisible();
|
||||
|
||||
|
||||
@ -16,18 +16,16 @@ test("teach a task records interaction and saves a draft", async ({ page }, test
|
||||
await expect(sidePanel.getByRole("button", { name: "Update computer" })).toHaveCount(0);
|
||||
await expect(sidePanel.getByRole("button", { name: "Take control" })).toHaveCount(0);
|
||||
await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0);
|
||||
await expect(sidePanel.getByTestId("computer-preview")).toBeVisible();
|
||||
await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||
// Showing the panel never boots the computer; its Open action is visible without hover.
|
||||
const openButton = sidePanel.getByTestId("computer-preview-open");
|
||||
const openButton = sidePanel.getByTestId("computer-open");
|
||||
await expect(openButton).toBeVisible();
|
||||
const botId = activeBotId(page);
|
||||
expect((await rpc<{ state: string }>(page, "computer/status", { botId })).state).toBe("stopped");
|
||||
await captureScreenshot(page, testInfo, "teach-sidepanel-simple");
|
||||
|
||||
const preview = sidePanel.getByTestId("computer-preview");
|
||||
await preview.hover();
|
||||
const preview = sidePanel.getByTestId("oo-computer-browser-viewport");
|
||||
await expect(openButton).toBeVisible();
|
||||
await expect(openButton.getByText("Open", { exact: true })).toBeVisible();
|
||||
await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover");
|
||||
|
||||
await openButton.click();
|
||||
|
||||
@ -10,7 +10,7 @@ test("blocks the composer while a skill is being taught", async ({ page }) => {
|
||||
await page.getByTitle("Agent computer").click();
|
||||
const sidePanel = page.getByTestId("side-panel");
|
||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||
const openButton = sidePanel.getByTestId("computer-preview-open");
|
||||
const openButton = sidePanel.getByTestId("computer-open");
|
||||
await expect(openButton).toBeVisible();
|
||||
await openButton.click();
|
||||
const chrome = page.getByTestId("computer-chrome");
|
||||
|
||||
@ -51,7 +51,7 @@ test("Team Computer gives bots a home folder plus shared space while Private sta
|
||||
|
||||
const privateId = await createBot(page, "Private Writer", "dedicated");
|
||||
await openComputerPanel(page);
|
||||
await expect(page.getByText(/Private Writer['’]s screen/).last()).toBeVisible();
|
||||
await expect(page.getByText(/Private Writer['’]s browser/).last()).toBeVisible();
|
||||
await captureScreenshot(page, testInfo, "43-private-computer");
|
||||
await expect(readFileResponse(page, privateId, "notes/result.txt")).resolves.toMatchObject({
|
||||
ok: false,
|
||||
@ -94,8 +94,7 @@ test("user control leaves another Team bot's screen available", async ({ page },
|
||||
// The sidebar entry for the main bot opens the main conversation; its computer is in the 1:1.
|
||||
await openMainBotThread(page);
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await page.getByTestId("computer-preview").hover();
|
||||
await page.getByTestId("computer-preview-open").click();
|
||||
await page.getByTestId("computer-open").click();
|
||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Close computer" }).click();
|
||||
await expect(page.getByRole("button", { name: "Close computer" })).toHaveCount(0);
|
||||
@ -132,8 +131,7 @@ test("a failed control release keeps the computer open for retry", async ({ page
|
||||
await signup(page, `team-release-${Date.now()}@rakazo.test`, "password12", "Team Release");
|
||||
await completeOnboarding(page);
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await page.getByTestId("computer-preview").hover();
|
||||
await page.getByTestId("computer-preview-open").click();
|
||||
await page.getByTestId("computer-open").click();
|
||||
const chrome = page.getByTestId("computer-chrome");
|
||||
const release = chrome.getByRole("button", { name: "Release", exact: true });
|
||||
await expect(release).toBeVisible();
|
||||
@ -192,10 +190,8 @@ test("an active Team bot must be stopped before user takeover", async ({ page },
|
||||
await page.getByTitle("Agent computer").click();
|
||||
const sidePanel = page.getByTestId("side-panel");
|
||||
await expect(sidePanel.getByRole("button", { name: /Take control/i })).toHaveCount(0);
|
||||
await page.getByTestId("computer-preview").hover();
|
||||
const openBusy = sidePanel.getByTestId("computer-preview-open");
|
||||
const openBusy = sidePanel.getByTestId("computer-open");
|
||||
await expect(openBusy).toBeVisible();
|
||||
await expect(openBusy.getByText("Open", { exact: true })).toBeVisible();
|
||||
await openBusy.click();
|
||||
const chrome = page.getByTestId("computer-chrome");
|
||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||
@ -221,8 +217,7 @@ test("an active Team bot must be stopped before user takeover", async ({ page },
|
||||
.toBeNull();
|
||||
|
||||
// After stop, Open via hover is the takeover path (no Take control button).
|
||||
await page.getByTestId("computer-preview").hover();
|
||||
await page.getByTestId("computer-preview-open").click();
|
||||
await page.getByTestId("computer-open").click();
|
||||
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
|
||||
await expect(chrome.getByText("You have control", { exact: true })).toBeVisible();
|
||||
await expect(chrome.getByRole("button", { name: /Take control/i })).toHaveCount(0);
|
||||
@ -287,8 +282,8 @@ async function expectComposerReady(page: Page) {
|
||||
|
||||
async function openComputerPanel(page: Page) {
|
||||
await page.getByTitle("Agent computer").click();
|
||||
await expect(page.getByTestId("computer-preview")).toBeVisible();
|
||||
await expect(page.getByTestId("computer-preview-open")).toHaveCount(1);
|
||||
await expect(page.getByTestId("oo-computer-browser-viewport")).toBeVisible();
|
||||
await expect(page.getByTestId("computer-open")).toHaveCount(1);
|
||||
}
|
||||
|
||||
async function sendAndWait(page: Page, botId: string, text: string) {
|
||||
|
||||
@ -14,6 +14,13 @@ test("conversations and projects stay separate; + opens bot picker", async ({ pa
|
||||
await expect(unterhaltungen.getByText("Talk to your Oos", { exact: true })).toBeVisible();
|
||||
await expect(projekte.getByText("Projects", { exact: true })).toBeVisible();
|
||||
await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
||||
const [projekteBox, unterhaltungenBox] = await Promise.all([
|
||||
projekte.boundingBox(),
|
||||
unterhaltungen.boundingBox(),
|
||||
]);
|
||||
expect(projekteBox).not.toBeNull();
|
||||
expect(unterhaltungenBox).not.toBeNull();
|
||||
expect(projekteBox!.y).toBeLessThan(unterhaltungenBox!.y);
|
||||
|
||||
await unterhaltungen.getByTestId("conversation-picker-trigger").click();
|
||||
const picker = page.getByTestId("conversation-bot-picker");
|
||||
@ -40,14 +47,14 @@ test("conversations and projects stay separate; + opens bot picker", async ({ pa
|
||||
await expect(projekte.getByText("Side project", { exact: true })).toBeVisible();
|
||||
await expect(unterhaltungenOnly(page).getByText("Side project", { exact: true })).toHaveCount(0);
|
||||
|
||||
// The main agent leads the picker, even after other bots exist.
|
||||
// The new project's main agent leads the picker, even after other bots exist.
|
||||
await unterhaltungen.getByTestId("conversation-picker-trigger").click();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("conversation-bot-picker")
|
||||
.locator("[data-testid^='conversation-picker-bot-']")
|
||||
.first(),
|
||||
).toHaveText("Chief");
|
||||
).toHaveText("Projektmanager");
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
// Creating a project that already exists opens it instead of listing it twice.
|
||||
|
||||
@ -7,7 +7,15 @@ async function turnOnSpeaking(page: Page) {
|
||||
await expect(voice).toBeVisible();
|
||||
await expect(voice.getByRole("button", { name: "Other voice providers" })).toHaveCount(0);
|
||||
await expect(voice.getByLabel("API key", { exact: true })).toHaveCount(0);
|
||||
await voice.getByRole("switch", { name: "Speaking on or off" }).click();
|
||||
// The app turns the local voice on by itself the first time it starts, and the switch stays
|
||||
// disabled while that runs; only click once it settled off.
|
||||
const speaking = voice.getByRole("switch", { name: "Speaking on or off" });
|
||||
await expect(async () => {
|
||||
if ((await speaking.getAttribute("aria-checked")) !== "true" && (await speaking.isEnabled())) {
|
||||
await speaking.click();
|
||||
}
|
||||
await expect(speaking).toHaveAttribute("aria-checked", "true", { timeout: 3_000 });
|
||||
}).toPass({ timeout: 30_000 });
|
||||
await expect(voice.getByTestId("voice-carousel")).toBeVisible();
|
||||
return voice;
|
||||
}
|
||||
@ -25,11 +33,6 @@ test("voice settings pick a voice from the carousel, speak a reply, and open a c
|
||||
).toHaveCount(1);
|
||||
await captureScreenshot(page, testInfo, "voice-composer");
|
||||
|
||||
const preparedOff = await rpc<{ ready: boolean }>(page, "voice/prepare", {
|
||||
text: "Hello there.",
|
||||
});
|
||||
expect(preparedOff.ready).toBe(false);
|
||||
|
||||
const voice = await turnOnSpeaking(page);
|
||||
const cards = voice.getByTestId("voice-card");
|
||||
// English UI lists only the English voices of the local server.
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="description" content="Deine Oos — immer da, immer wach." />
|
||||
<meta name="theme-color" content="#07080a" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="evOo" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
{
|
||||
"id": "/",
|
||||
"name": "evOo",
|
||||
"short_name": "evOo",
|
||||
"description": "Deine Oos — immer da, immer wach.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f1218",
|
||||
"theme_color": "#0f1218",
|
||||
"orientation": "any",
|
||||
"background_color": "#07080a",
|
||||
"theme_color": "#07080a",
|
||||
"icons": [
|
||||
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
||||
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
|
||||
]
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ self.addEventListener("push", (event) => {
|
||||
// Same tag as the in-app notification, so one replaces the other.
|
||||
tag: typeof data.tag === "string" && data.tag ? data.tag : undefined,
|
||||
icon: "/icon-192.png",
|
||||
badge: "/icon-192.png",
|
||||
data: { url: appPath(data.url) },
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { ChatMarkdown } from "@rakazo/chat-ui/web";
|
||||
import { isPaneArtifactMimeType } from "@rakazo/core";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web";
|
||||
import { Download, FileText, X } from "lucide-react";
|
||||
import { type RefObject, useMemo, useRef, useState } from "react";
|
||||
import { Download, Eye, FileText, X } from "lucide-react";
|
||||
import type { RefObject } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useArtifactBytes } from "../hooks/useArtifactBytes";
|
||||
import { downloadArtifact } from "../lib/artifact-open";
|
||||
import { formatBytes } from "../lib/format-bytes";
|
||||
@ -16,80 +18,104 @@ type ArtifactFileCardProps = {
|
||||
onOpenBeside?: () => void;
|
||||
};
|
||||
|
||||
const INLINE_DIALOG_TEXT_MIME_TYPES = new Set(["text/markdown", "text/plain", "application/json"]);
|
||||
|
||||
function shortFileTypeLabel(name: string, mimeType: string): string {
|
||||
const dot = name.lastIndexOf(".");
|
||||
if (dot > 0 && dot < name.length - 1) {
|
||||
const ext = name
|
||||
.slice(dot + 1)
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
if (ext.length <= 6) return ext;
|
||||
}
|
||||
const sub = mimeType.split("/")[1]?.split("+")[0]?.toUpperCase();
|
||||
return sub && sub.length <= 8 ? sub : "FILE";
|
||||
}
|
||||
|
||||
export function ArtifactFileCard(props: ArtifactFileCardProps) {
|
||||
const { t } = useLingui();
|
||||
const markdown = props.mimeType === "text/markdown";
|
||||
const canOpenBeside = Boolean(props.onOpenBeside) && isPaneArtifactMimeType(props.mimeType);
|
||||
const canOpenDialog = INLINE_DIALOG_TEXT_MIME_TYPES.has(props.mimeType);
|
||||
const canPreview = canOpenBeside || canOpenDialog;
|
||||
const previewButton = useRef<HTMLButtonElement>(null);
|
||||
const closeButton = useRef<HTMLButtonElement>(null);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const startDownload = () => downloadArtifact(props.artifactId, props.name);
|
||||
|
||||
if (!markdown) {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startDownload}
|
||||
className="rounded-2xl border border-border bg-card px-4 py-3 text-left text-ui-body text-foreground hover:bg-accent"
|
||||
>
|
||||
<div className="font-medium">{props.name}</div>
|
||||
<div className="mt-1 text-muted-foreground">
|
||||
{props.mimeType} · {formatBytes(props.size)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const badgeLabel = shortFileTypeLabel(props.name, props.mimeType);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex min-w-[280px] overflow-hidden rounded-2xl border border-border bg-card text-left text-foreground">
|
||||
<div data-testid="artifact-file-card">
|
||||
<div className="flex min-w-[280px] max-w-md overflow-hidden rounded-2xl border border-border bg-card text-left text-foreground shadow-2xs transition-colors">
|
||||
<button
|
||||
ref={previewButton}
|
||||
type="button"
|
||||
aria-label={t`Preview ${props.name}`}
|
||||
onClick={() => (props.onOpenBeside ? props.onOpenBeside() : setPreviewOpen(true))}
|
||||
className="flex min-w-0 flex-1 items-center gap-3 px-4 py-3 text-left hover:bg-accent"
|
||||
aria-label={canPreview ? t`Preview ${props.name}` : t`Download ${props.name}`}
|
||||
onClick={() => {
|
||||
if (canOpenBeside && props.onOpenBeside) {
|
||||
props.onOpenBeside();
|
||||
} else if (canOpenDialog) {
|
||||
setPreviewOpen(true);
|
||||
} else {
|
||||
startDownload();
|
||||
}
|
||||
}}
|
||||
className="flex min-w-0 flex-1 items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent"
|
||||
>
|
||||
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-[10px] bg-muted text-foreground">
|
||||
<FileText size={21} strokeWidth={1.8} />
|
||||
<FileText size={20} strokeWidth={1.8} />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-ui-body font-medium">{props.name}</span>
|
||||
<span className="mt-0.5 block text-ui-body-sm text-muted-foreground">
|
||||
{formatBytes(props.size)}
|
||||
<span className="mt-0.5 flex items-center gap-1.5 text-ui-body-sm text-muted-foreground">
|
||||
<span className="rounded bg-muted px-1.5 py-0.2 font-mono text-ui-label text-foreground/80">
|
||||
{badgeLabel}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{formatBytes(props.size)}</span>
|
||||
</span>
|
||||
</span>
|
||||
{canPreview ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="shrink-0 text-muted-foreground/70 transition-colors"
|
||||
>
|
||||
<Eye size={16} strokeWidth={1.8} />
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t`Download ${props.name}`}
|
||||
title={t`Download ${props.name}`}
|
||||
onClick={startDownload}
|
||||
className="grid w-14 shrink-0 place-items-center border-l border-border text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="grid w-13 shrink-0 place-items-center border-l border-border text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<Download strokeWidth={1.8} className="size-icon-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
initialFocus={closeButton}
|
||||
finalFocus={previewButton}
|
||||
className="flex h-[min(88vh,900px)] w-[min(960px,94vw)] flex-col gap-0 overflow-hidden p-0 sm:max-w-none"
|
||||
>
|
||||
<MarkdownPreview {...props} closeButtonRef={closeButton} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{canOpenDialog ? (
|
||||
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
initialFocus={closeButton}
|
||||
finalFocus={previewButton}
|
||||
className="flex h-[min(88vh,900px)] w-[min(960px,94vw)] flex-col gap-0 overflow-hidden p-0 sm:max-w-none"
|
||||
>
|
||||
<TextFilePreview {...props} closeButtonRef={closeButton} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MarkdownPreview({
|
||||
function TextFilePreview({
|
||||
artifactId,
|
||||
name,
|
||||
mimeType,
|
||||
closeButtonRef,
|
||||
}: ArtifactFileCardProps & { closeButtonRef: RefObject<HTMLButtonElement | null> }) {
|
||||
const { t } = useLingui();
|
||||
@ -97,8 +123,8 @@ function MarkdownPreview({
|
||||
const state = useMemo(() => {
|
||||
if (loaded.status !== "ready") return loaded;
|
||||
try {
|
||||
const markdown = new TextDecoder("utf-8", { fatal: true }).decode(loaded.bytes);
|
||||
return { status: "ready" as const, markdown };
|
||||
const text = new TextDecoder("utf-8", { fatal: true }).decode(loaded.bytes);
|
||||
return { status: "ready" as const, text };
|
||||
} catch {
|
||||
return { status: "error" as const, message: t`This file is not valid UTF-8 Markdown.` };
|
||||
}
|
||||
@ -140,8 +166,12 @@ function MarkdownPreview({
|
||||
<div className="rounded-xl border border-destructive/40 bg-destructive/10 px-4 py-3 text-destructive">
|
||||
{state.message}
|
||||
</div>
|
||||
) : mimeType === "text/markdown" ? (
|
||||
<ChatMarkdown>{state.text}</ChatMarkdown>
|
||||
) : (
|
||||
<ChatMarkdown>{state.markdown}</ChatMarkdown>
|
||||
<pre className="whitespace-pre-wrap break-words font-mono text-ui-body-sm leading-6 text-foreground">
|
||||
{state.text}
|
||||
</pre>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@ -1,17 +1,115 @@
|
||||
import { selectedAskActionLabel } from "@rakazo/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
// @vitest-environment jsdom
|
||||
|
||||
describe("selectedAskActionLabel", () => {
|
||||
it("maps a choice answer id to its user-facing label", () => {
|
||||
expect(
|
||||
selectedAskActionLabel("choice-2", [
|
||||
{ id: "choice-1", label: "Berlin" },
|
||||
{ id: "choice-2", label: "Seoul" },
|
||||
]),
|
||||
).toBe("Seoul");
|
||||
});
|
||||
import type { ReactNode } from "react";
|
||||
import { act } from "react";
|
||||
import type { Root } from "react-dom/client";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
|
||||
it("falls back to the answer when an action is unavailable", () => {
|
||||
expect(selectedAskActionLabel("custom", undefined)).toBe("custom");
|
||||
});
|
||||
vi.mock("@lingui/core/macro", () => ({
|
||||
t: (parts: TemplateStringsArray, ...values: unknown[]) =>
|
||||
parts.reduce((acc, part, index) => acc + part + String(values[index] ?? ""), ""),
|
||||
}));
|
||||
|
||||
vi.mock("@lingui/react/macro", () => {
|
||||
const t = (parts: TemplateStringsArray, ...values: unknown[]) =>
|
||||
parts.reduce((acc, part, index) => acc + part + String(values[index] ?? ""), "");
|
||||
return { useLingui: () => ({ t }), Trans: ({ children }: { children: ReactNode }) => children };
|
||||
});
|
||||
|
||||
vi.mock("@rakazo/chat-ui/web", () => ({
|
||||
ChatMarkdown: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
import type { AskBlock } from "./AskCard";
|
||||
import { AskCard } from "./AskCard";
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("renders primary approval button text with text-primary-foreground for contrast", async () => {
|
||||
const onAnswer = vi.fn().mockResolvedValue(undefined);
|
||||
const block: AskBlock = {
|
||||
kind: "ask",
|
||||
text: "**Review before shell**\n\nThis run would push commits to a remote.",
|
||||
detail: "git push",
|
||||
status: "pending",
|
||||
approvalEffectId: "effect-1",
|
||||
actions: [
|
||||
{ id: "allow", label: "Allow once" },
|
||||
{ id: "always", label: "Always allow this tool" },
|
||||
{ id: "deny", label: "Deny" },
|
||||
],
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(<AskCard block={block} canAnswer={true} onAnswer={onAnswer} />);
|
||||
});
|
||||
|
||||
const buttons = Array.from(container.querySelectorAll("button"));
|
||||
expect(buttons).toHaveLength(3);
|
||||
|
||||
const [allowButton, alwaysButton, denyButton] = buttons;
|
||||
const allowSpan = allowButton?.querySelector("span");
|
||||
const alwaysSpan = alwaysButton?.querySelector("span");
|
||||
const denySpan = denyButton?.querySelector("span");
|
||||
const allowIcon = allowButton?.querySelector("svg");
|
||||
const alwaysIcon = alwaysButton?.querySelector("svg");
|
||||
|
||||
expect(allowSpan?.textContent).toBe("Allow once");
|
||||
expect(allowSpan?.className).toContain("text-primary-foreground");
|
||||
expect(allowSpan?.className).not.toContain("text-foreground");
|
||||
expect(allowIcon?.getAttribute("class")).toContain("text-primary-foreground/75");
|
||||
|
||||
expect(alwaysSpan?.textContent).toBe("Always allow this tool");
|
||||
expect(alwaysSpan?.className).toContain("text-foreground");
|
||||
expect(alwaysSpan?.className).not.toContain("text-primary-foreground");
|
||||
expect(alwaysIcon?.getAttribute("class")).toContain("text-muted-foreground");
|
||||
|
||||
expect(denySpan?.textContent).toBe("Deny");
|
||||
expect(denySpan?.className).toContain("text-foreground");
|
||||
expect(denySpan?.className).not.toContain("text-primary-foreground");
|
||||
|
||||
await act(async () => {
|
||||
allowButton?.click();
|
||||
});
|
||||
expect(onAnswer).toHaveBeenCalledWith("allow");
|
||||
});
|
||||
|
||||
it("uses text-foreground for non-approval action buttons", async () => {
|
||||
const onAnswer = vi.fn().mockResolvedValue(undefined);
|
||||
const block: AskBlock = {
|
||||
kind: "ask",
|
||||
text: "Which branch should we deploy?",
|
||||
status: "pending",
|
||||
actions: [
|
||||
{ id: "main", label: "main" },
|
||||
{ id: "staging", label: "staging" },
|
||||
],
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(<AskCard block={block} canAnswer={true} onAnswer={onAnswer} />);
|
||||
});
|
||||
|
||||
const actionSpans = Array.from(container.querySelectorAll("button span"));
|
||||
expect(actionSpans).toHaveLength(2);
|
||||
for (const span of actionSpans) {
|
||||
expect(span.className).toContain("text-foreground");
|
||||
expect(span.className).not.toContain("text-primary-foreground");
|
||||
}
|
||||
});
|
||||
|
||||
@ -174,34 +174,47 @@ export function AskCard({
|
||||
</form>
|
||||
) : askActions?.length ? (
|
||||
<div className="mt-3.5 space-y-1.5">
|
||||
{askActions.map((action) => (
|
||||
<Button
|
||||
key={action.id}
|
||||
variant={approvalActions && action.id === "allow" ? "default" : "outline"}
|
||||
className={cn(
|
||||
"h-auto w-full justify-between gap-3 whitespace-normal rounded-xl px-4 py-3 text-start font-medium transition-all group/ask-btn",
|
||||
approvalActions && action.id === "allow"
|
||||
? ""
|
||||
: "border-border/80 bg-muted/60 hover:border-foreground/30 hover:bg-muted active:scale-[0.99]",
|
||||
)}
|
||||
disabled={submitting}
|
||||
onClick={() => void submitAnswer(action.id)}
|
||||
>
|
||||
<span className="flex-1 text-ui-body font-medium leading-snug text-foreground">
|
||||
{pendingAction === action.id ? (
|
||||
<Trans>Sending…</Trans>
|
||||
) : approvalActions ? (
|
||||
approvalActionLabel(action.id, action.label, action.outcome)
|
||||
) : (
|
||||
action.label
|
||||
{askActions.map((action) => {
|
||||
const isPrimaryApproval = Boolean(approvalActions && action.id === "allow");
|
||||
return (
|
||||
<Button
|
||||
key={action.id}
|
||||
variant={isPrimaryApproval ? "default" : "outline"}
|
||||
className={cn(
|
||||
"h-auto w-full justify-between gap-3 whitespace-normal rounded-xl px-4 py-3 text-start font-medium transition-all group/ask-btn",
|
||||
isPrimaryApproval
|
||||
? ""
|
||||
: "border-border/80 bg-muted/60 hover:border-foreground/30 hover:bg-muted active:scale-[0.99]",
|
||||
)}
|
||||
</span>
|
||||
<ArrowRight
|
||||
strokeWidth={2}
|
||||
className="size-icon-sm shrink-0 text-muted-foreground transition-transform group-hover/ask-btn:translate-x-0.5 group-hover/ask-btn:text-foreground"
|
||||
/>
|
||||
</Button>
|
||||
))}
|
||||
disabled={submitting}
|
||||
onClick={() => void submitAnswer(action.id)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex-1 text-ui-body font-medium leading-snug",
|
||||
isPrimaryApproval ? "text-primary-foreground" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{pendingAction === action.id ? (
|
||||
<Trans>Sending…</Trans>
|
||||
) : approvalActions ? (
|
||||
approvalActionLabel(action.id, action.label, action.outcome)
|
||||
) : (
|
||||
action.label
|
||||
)}
|
||||
</span>
|
||||
<ArrowRight
|
||||
strokeWidth={2}
|
||||
className={cn(
|
||||
"size-icon-sm shrink-0 transition-transform group-hover/ask-btn:translate-x-0.5",
|
||||
isPrimaryApproval
|
||||
? "text-primary-foreground/75 group-hover/ask-btn:text-primary-foreground"
|
||||
: "text-muted-foreground group-hover/ask-btn:text-foreground",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{!approvalActions ? (
|
||||
<div className="pt-1">
|
||||
<Button
|
||||
@ -226,7 +239,11 @@ export function AskCard({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{error ? <p className="mt-3 text-ui-body-sm text-destructive">{error}</p> : null}
|
||||
{error ? (
|
||||
<p role="alert" className="mt-3 text-ui-body-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import type { MessageBlock } from "@rakazo/contracts";
|
||||
import { cloudAgentHttpsUrl } from "@rakazo/core";
|
||||
import { Badge } from "@rakazo/ui-web/components/ui/badge";
|
||||
import { Card, CardContent } from "@rakazo/ui-web/components/ui/card";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
export function CloudAgentCard({
|
||||
block,
|
||||
@ -15,8 +16,14 @@ export function CloudAgentCard({
|
||||
<Card size="sm" className="w-80 max-w-full" data-testid="cloud-agent-card">
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span className="font-medium" dir="auto">
|
||||
<span className="inline-flex items-center gap-1 font-medium" dir="auto">
|
||||
{block.title}
|
||||
{href ? (
|
||||
<ArrowUpRight
|
||||
aria-hidden="true"
|
||||
className="size-3.5 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
@ -52,7 +59,13 @@ export function CloudAgentCard({
|
||||
</Card>
|
||||
);
|
||||
return href ? (
|
||||
<a href={href} target="_blank" rel="noreferrer" className="block max-w-full no-underline">
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
data-external-link=""
|
||||
className="block max-w-full no-underline"
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
) : (
|
||||
|
||||
@ -74,7 +74,7 @@ export function DualSurfaceDivider({
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
"group relative flex w-3 md:w-4 shrink-0 cursor-col-resize select-none touch-none items-center justify-center transition-colors bg-border/0 hover:bg-accent/40 active:bg-primary/20 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||
"group relative flex w-3 md:w-3 shrink-0 cursor-col-resize select-none touch-none items-center justify-center transition-colors bg-transparent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||
disabled && "pointer-events-none cursor-default opacity-50",
|
||||
className,
|
||||
)}
|
||||
@ -82,8 +82,8 @@ export function DualSurfaceDivider({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full w-1 rounded-full bg-border transition-colors group-hover:bg-accent group-active:bg-primary",
|
||||
isDragging && "bg-primary",
|
||||
"h-12 w-1 rounded-full bg-border opacity-0 transition-opacity group-hover:opacity-100 group-active:bg-primary group-active:opacity-100",
|
||||
isDragging && "bg-primary opacity-100",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -4,7 +4,13 @@ import { Input, Switch } from "@rakazo/ui-web";
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import { rpc } from "../lib/rpc";
|
||||
import { disableWebPush, enableWebPush, webPushEnabled, webPushSupported } from "../lib/web-push";
|
||||
import {
|
||||
disableWebPush,
|
||||
enableWebPush,
|
||||
isIosBrowserNeedsHomeScreen,
|
||||
webPushEnabled,
|
||||
webPushSupported,
|
||||
} from "../lib/web-push";
|
||||
import { SettingsSection } from "./SettingsSection";
|
||||
|
||||
const DEFAULT_QUIET_HOURS = { start: "22:00", end: "07:00" };
|
||||
@ -51,6 +57,7 @@ function SettingRow({
|
||||
export function NotificationSettingsPanel() {
|
||||
const { t } = useLingui();
|
||||
const pushSupported = webPushSupported();
|
||||
const iosNeedsHomeScreen = isIosBrowserNeedsHomeScreen();
|
||||
const [settings, setSettings] = useState<NotificationSettings | null>(null);
|
||||
const [push, setPush] = useState<PushState>("loading");
|
||||
const [pushBusy, setPushBusy] = useState(false);
|
||||
@ -153,6 +160,15 @@ export function NotificationSettingsPanel() {
|
||||
</p>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
) : iosNeedsHomeScreen ? (
|
||||
<li data-testid="notification-ios-pwa-hint" className="py-3 first:pt-0 last:pb-0">
|
||||
<div className="text-ui-body text-foreground">
|
||||
<Trans>Push on this device</Trans>
|
||||
</div>
|
||||
<p className="mt-1 text-ui-body-sm text-muted-foreground">
|
||||
<Trans>Add evOo to your Home Screen via Share to enable push on iOS.</Trans>
|
||||
</p>
|
||||
</li>
|
||||
) : null}
|
||||
<SettingRow
|
||||
label={<Trans>Quiet hours</Trans>}
|
||||
|
||||
@ -254,10 +254,10 @@ export function OoAtelierDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className="max-h-[92vh] w-[420px] max-w-full gap-3 overflow-y-auto rounded-3xl p-5 sm:max-w-[420px]"
|
||||
className="flex max-h-[90dvh] w-[calc(100vw-2rem)] max-w-[420px] flex-col gap-3 overflow-hidden rounded-3xl p-5 sm:max-w-[420px]"
|
||||
data-testid="oo-atelier"
|
||||
>
|
||||
<DialogHeader className="flex-row items-center justify-between space-y-0">
|
||||
<DialogHeader className="shrink-0 flex-row items-center justify-between space-y-0">
|
||||
<DialogTitle className="text-ui-body font-semibold tracking-tight">
|
||||
<Trans>Atelier</Trans>
|
||||
</DialogTitle>
|
||||
@ -274,19 +274,19 @@ export function OoAtelierDialog({
|
||||
</button>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex justify-center py-1" data-testid="oo-atelier-preview">
|
||||
<div className="flex shrink-0 justify-center py-1" data-testid="oo-atelier-preview">
|
||||
<BotAvatar
|
||||
color={isPicture && !draft ? value : encoded}
|
||||
identity={identity}
|
||||
status={status}
|
||||
size={128}
|
||||
size={120}
|
||||
moment={preview}
|
||||
interactive
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="body">
|
||||
<TabsList className="w-full">
|
||||
<Tabs defaultValue="body" className="flex min-h-0 flex-1 flex-col">
|
||||
<TabsList className="w-full shrink-0">
|
||||
<TabsTrigger value="body">
|
||||
<Trans>Body</Trans>
|
||||
</TabsTrigger>
|
||||
@ -303,7 +303,11 @@ export function OoAtelierDialog({
|
||||
) : null}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="body" className="space-y-4 pt-2" data-testid="oo-atelier-body">
|
||||
<TabsContent
|
||||
value="body"
|
||||
className="rk-scroll min-h-0 flex-1 space-y-4 overflow-y-auto pt-2 pr-1"
|
||||
data-testid="oo-atelier-body"
|
||||
>
|
||||
<OptionGroup
|
||||
title={t`Species`}
|
||||
values={OO_SPECIES}
|
||||
@ -397,7 +401,11 @@ export function OoAtelierDialog({
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="eyes" className="space-y-4 pt-2" data-testid="oo-atelier-eyes">
|
||||
<TabsContent
|
||||
value="eyes"
|
||||
className="rk-scroll min-h-0 flex-1 space-y-4 overflow-y-auto pt-2 pr-1"
|
||||
data-testid="oo-atelier-eyes"
|
||||
>
|
||||
<OptionGroup
|
||||
title={t`Lashes`}
|
||||
values={OO_LASHES}
|
||||
@ -456,7 +464,11 @@ export function OoAtelierDialog({
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="extras" className="space-y-4 pt-2" data-testid="oo-atelier-extras">
|
||||
<TabsContent
|
||||
value="extras"
|
||||
className="rk-scroll min-h-0 flex-1 space-y-4 overflow-y-auto pt-2 pr-1"
|
||||
data-testid="oo-atelier-extras"
|
||||
>
|
||||
<OptionGroup
|
||||
title={t`Glasses`}
|
||||
values={OO_EYEWEAR}
|
||||
@ -492,7 +504,7 @@ export function OoAtelierDialog({
|
||||
</TabsContent>
|
||||
|
||||
{allowUpload ? (
|
||||
<TabsContent value="upload" className="pt-2">
|
||||
<TabsContent value="upload" className="min-h-0 flex-1 overflow-y-auto pt-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t`Image upload area`}
|
||||
@ -534,7 +546,7 @@ export function OoAtelierDialog({
|
||||
) : null}
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter className="flex-row items-center justify-between border-border sm:justify-between">
|
||||
<DialogFooter className="-mx-5 -mb-5 shrink-0 flex-row items-center justify-between border-border px-5 py-3.5 sm:justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -2,7 +2,8 @@ import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import type { Bot, ComputerStatus } from "@rakazo/contracts";
|
||||
import { Button, cn } from "@rakazo/ui-web";
|
||||
import { X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { agentPointerFeed } from "../../lib/agent-pointer";
|
||||
import { LocalBrowserPreview } from "./LocalBrowserPreview";
|
||||
import { OoComputerBrowser } from "./OoComputerBrowser";
|
||||
|
||||
@ -16,8 +17,9 @@ export function InAppBrowser({
|
||||
computerScreenError,
|
||||
sandboxProvider,
|
||||
booting,
|
||||
onBoot,
|
||||
onRefreshScreen,
|
||||
onConnectScreen,
|
||||
onOpenComputer,
|
||||
hideCloseButton = false,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
@ -27,13 +29,21 @@ export function InAppBrowser({
|
||||
computerScreenError: React.ReactNode;
|
||||
sandboxProvider: string | undefined;
|
||||
booting: boolean;
|
||||
onBoot: () => Promise<void>;
|
||||
onRefreshScreen: () => Promise<unknown>;
|
||||
onConnectScreen: () => Promise<unknown>;
|
||||
onOpenComputer?: () => void;
|
||||
hideCloseButton?: boolean;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const [mode, setMode] = useState<InAppBrowserMode>("oo");
|
||||
|
||||
useEffect(() => {
|
||||
if (!active?.id) return;
|
||||
return agentPointerFeed.subscribe(active.id, () => {
|
||||
setMode("oo");
|
||||
});
|
||||
}, [active?.id]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col" data-testid="in-app-browser">
|
||||
<div className="flex items-center justify-between gap-2 border-b px-2 py-1">
|
||||
@ -83,8 +93,9 @@ export function InAppBrowser({
|
||||
computerScreenError={computerScreenError}
|
||||
sandboxProvider={sandboxProvider}
|
||||
booting={booting}
|
||||
onBoot={onBoot}
|
||||
onRefreshScreen={onRefreshScreen}
|
||||
onConnectScreen={onConnectScreen}
|
||||
onOpenComputer={onOpenComputer}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid flex-1 place-items-center px-6 text-center text-ui-body-sm text-muted-foreground">
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import type { Bot, ComputerStatus } from "@rakazo/contracts";
|
||||
import { Button, cn, Input } from "@rakazo/ui-web";
|
||||
import { ArrowLeft, ArrowRight, ExternalLink, Loader2, RotateCw } from "lucide-react";
|
||||
import { ArrowLeft, ArrowRight, ExternalLink, Loader2, Maximize2, RotateCw } from "lucide-react";
|
||||
import { type FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { agentPointerFeed } from "../../lib/agent-pointer";
|
||||
import { normalizeBrowserUrl } from "../../lib/browser-url";
|
||||
import { rpc } from "../../lib/rpc";
|
||||
import { screenIframeSandbox } from "../../pages/shell/computer-screen";
|
||||
@ -33,8 +34,9 @@ export function OoComputerBrowser({
|
||||
computerScreenError,
|
||||
sandboxProvider,
|
||||
booting,
|
||||
onBoot,
|
||||
onRefreshScreen,
|
||||
onConnectScreen,
|
||||
onOpenComputer,
|
||||
}: {
|
||||
active: Bot;
|
||||
computer: ComputerStatus | null;
|
||||
@ -42,8 +44,11 @@ export function OoComputerBrowser({
|
||||
computerScreenError: React.ReactNode;
|
||||
sandboxProvider: string | undefined;
|
||||
booting: boolean;
|
||||
onBoot: () => Promise<void>;
|
||||
onRefreshScreen: () => Promise<unknown>;
|
||||
/** Fetches the screen of an already running computer, without touching the thread. */
|
||||
onConnectScreen: () => Promise<unknown>;
|
||||
/** Opens the full computer window, where the user can take control or teach. */
|
||||
onOpenComputer?: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const [chrome, setChrome] = useState<ChromeState>(EMPTY_CHROME);
|
||||
@ -73,10 +78,26 @@ export function OoComputerBrowser({
|
||||
}, [computer?.state, refreshChrome]);
|
||||
|
||||
useEffect(() => {
|
||||
if (computer?.state === "running" && (embeddedScreenUrl || computer?.kind === "desktop"))
|
||||
return;
|
||||
void onBoot().catch(() => undefined);
|
||||
}, [active.id, computer?.kind, computer?.state, embeddedScreenUrl, onBoot]);
|
||||
if (computer?.state !== "running") return;
|
||||
let settleTimer: number | undefined;
|
||||
const unsubscribe = agentPointerFeed.subscribe(active.id, () => {
|
||||
void refreshChrome();
|
||||
window.clearTimeout(settleTimer);
|
||||
settleTimer = window.setTimeout(() => void refreshChrome(), 600);
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
window.clearTimeout(settleTimer);
|
||||
};
|
||||
}, [active.id, computer?.state, refreshChrome]);
|
||||
|
||||
const connectScreen = useRef(onConnectScreen);
|
||||
connectScreen.current = onConnectScreen;
|
||||
// Showing the browser never starts a stopped computer; it only reconnects a running screen.
|
||||
useEffect(() => {
|
||||
if (computer?.state !== "running" || embeddedScreenUrl || computer?.kind === "desktop") return;
|
||||
void connectScreen.current().catch(() => undefined);
|
||||
}, [active.id, computer?.kind, computer?.state, embeddedScreenUrl]);
|
||||
|
||||
const runNav = useCallback(
|
||||
async (task: () => Promise<ChromeState>) => {
|
||||
@ -200,6 +221,18 @@ export function OoComputerBrowser({
|
||||
>
|
||||
<ExternalLink strokeWidth={1.7} className="size-icon-md" />
|
||||
</Button>
|
||||
{onOpenComputer && !unavailable && !computerScreenError ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={t`Open in full window`}
|
||||
data-testid="computer-open"
|
||||
onClick={onOpenComputer}
|
||||
>
|
||||
<Maximize2 strokeWidth={1.7} className="size-icon-md" />
|
||||
</Button>
|
||||
) : null}
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
|
||||
@ -15,14 +15,14 @@ type OnboardingDiscoveryShellProps = {
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
function sourceLabel(id: DiscoverySourceId, t: ReturnType<typeof useLingui>["t"]): string {
|
||||
function sourceLabel(id: DiscoverySourceId): ReactNode {
|
||||
switch (id) {
|
||||
case "profile-basics":
|
||||
return t`Your profile`;
|
||||
return <Trans>Your profile</Trans>;
|
||||
case "connected-tools":
|
||||
return t`Connected tools`;
|
||||
return <Trans>Connected tools</Trans>;
|
||||
case "specialist-crew":
|
||||
return t`Specialist crew`;
|
||||
return <Trans>Specialist crew</Trans>;
|
||||
default: {
|
||||
const never: never = id;
|
||||
return never;
|
||||
@ -83,7 +83,7 @@ export function OnboardingDiscoveryShell({
|
||||
>
|
||||
{active ? (
|
||||
<>
|
||||
{sourceLabel(active.id, t)}
|
||||
{sourceLabel(active.id)}
|
||||
{active.status === "found" && active.detail === DISCOVERY_MAIN_BOT_ROLE ? (
|
||||
<>
|
||||
{" · "}
|
||||
|
||||
@ -120,7 +120,7 @@ export function OnboardingOoDesigner({ config, onChange, onContinue }: Onboardin
|
||||
<Trans>What feels like you?</Trans>
|
||||
</p>
|
||||
<div
|
||||
className="mt-3 flex gap-2 overflow-x-auto pb-1 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
className="mt-3 flex flex-wrap gap-2 pb-1"
|
||||
role="tablist"
|
||||
aria-label={t`Look families`}
|
||||
>
|
||||
|
||||
@ -17,7 +17,7 @@ export function VoiceLiveText({ text, reducedMotion }: { text: string; reducedMo
|
||||
return (
|
||||
<p
|
||||
dir="auto"
|
||||
className="text-balance text-center text-ui-title font-medium leading-snug text-foreground"
|
||||
className="line-clamp-3 text-pretty text-start text-ui-body font-medium leading-snug text-foreground"
|
||||
>
|
||||
{captionWords(text).map((word, index) => (
|
||||
<span key={`${utterance}-${index}`} className={reducedMotion ? undefined : "voice-word-in"}>
|
||||
|
||||
@ -90,21 +90,52 @@ export function VoiceStage({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
<section
|
||||
data-testid="call-view"
|
||||
data-theme="dark"
|
||||
data-phase={phase}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
className="fixed inset-0 z-40 flex flex-col items-center justify-center gap-3 overflow-y-auto bg-background px-4 pt-[max(1rem,env(safe-area-inset-top))] pb-[max(1rem,env(safe-area-inset-bottom))] text-foreground"
|
||||
className="pointer-events-none fixed inset-x-0 bottom-0 z-40 flex flex-col items-center justify-end gap-2.5 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] text-foreground"
|
||||
>
|
||||
<h1 id={titleId} className="sr-only">
|
||||
{botName}
|
||||
</h1>
|
||||
|
||||
<div
|
||||
id="voice-transcript-panel"
|
||||
className={`pointer-events-auto w-full max-w-lg overflow-hidden transition-[max-height,opacity] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
||||
transcriptOpen ? "max-h-56 opacity-100" : "max-h-0 opacity-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<div className="rk-scroll max-h-52 overflow-y-auto rounded-3xl border border-border bg-card/95 px-5 py-4 text-start shadow-xl backdrop-blur-xl">
|
||||
{transcriptLines.length ? (
|
||||
<ul className="space-y-2.5">
|
||||
{transcriptLines.map((line, index) => (
|
||||
<li
|
||||
key={`${line.role}-${index}`}
|
||||
className={`text-ui-body-sm leading-relaxed ${
|
||||
line.role === "user" ? "text-foreground" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<span className="text-ui-label text-muted-foreground">
|
||||
{line.role === "user" ? t`You` : (line.speakerName ?? botName)}
|
||||
</span>
|
||||
<span dir="auto" className="mt-0.5 block">
|
||||
{line.text}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-ui-body-sm text-muted-foreground">
|
||||
<Trans>Your conversation will appear here.</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{awarenessLabel && awarenessEvent ? (
|
||||
<div className="w-full max-w-md">
|
||||
<div className="pointer-events-auto w-full max-w-lg">
|
||||
<VoiceAwarenessStrip
|
||||
event={awarenessEvent}
|
||||
label={awarenessLabel}
|
||||
@ -116,7 +147,7 @@ export function VoiceStage({
|
||||
|
||||
<section
|
||||
data-testid="voice-panel"
|
||||
className={`relative isolate flex min-h-[min(68vh,30rem)] w-full max-w-md flex-col overflow-hidden rounded-[2rem] border border-border bg-card shadow-2xl ${reducedMotion ? "" : "voice-panel-in"}`}
|
||||
className={`pointer-events-auto relative isolate flex w-full max-w-lg flex-col overflow-hidden rounded-3xl border border-border bg-card/95 shadow-2xl backdrop-blur-xl ${reducedMotion ? "" : "voice-panel-in"}`}
|
||||
>
|
||||
<VoiceAurora
|
||||
botColor={botColor}
|
||||
@ -125,28 +156,30 @@ export function VoiceStage({
|
||||
reducedMotion={reducedMotion}
|
||||
/>
|
||||
|
||||
<div className="relative flex flex-1 flex-col items-center justify-center gap-6 px-7 pt-10 pb-4">
|
||||
<div title={`${botName} · ${botTitle}`}>
|
||||
<div className="relative flex items-center gap-3.5 px-4 pt-3.5 pb-2.5">
|
||||
<div className="shrink-0" title={`${botName} · ${botTitle}`}>
|
||||
<BotAvatar
|
||||
color={botColor}
|
||||
identity={botId}
|
||||
status={botStatus}
|
||||
pose={mascotPose}
|
||||
size={88}
|
||||
size={44}
|
||||
/>
|
||||
</div>
|
||||
<VoiceLiveText text={subtitle} reducedMotion={reducedMotion} />
|
||||
{error ? (
|
||||
<p role="alert" className="text-center text-ui-body-sm font-medium text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="min-w-0 flex-1 text-start">
|
||||
<VoiceLiveText text={subtitle} reducedMotion={reducedMotion} />
|
||||
{error ? (
|
||||
<p role="alert" className="mt-1 text-ui-body-sm font-medium text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center gap-2 px-4 pb-4">
|
||||
<div className="relative flex items-center gap-2 px-4 pb-3">
|
||||
<span
|
||||
aria-live="polite"
|
||||
className="mr-auto truncate rounded-full bg-foreground/10 px-3.5 py-2 text-ui-body-sm font-medium text-foreground/85 backdrop-blur-md"
|
||||
className="mr-auto truncate rounded-full bg-foreground/10 px-3 py-1.5 text-ui-body-sm font-medium text-foreground/85 backdrop-blur-md"
|
||||
>
|
||||
{phaseLabel}
|
||||
</span>
|
||||
@ -187,39 +220,6 @@ export function VoiceStage({
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
id="voice-transcript-panel"
|
||||
className={`w-full max-w-md overflow-hidden transition-[max-height,opacity] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
||||
transcriptOpen ? "max-h-56 opacity-100" : "max-h-0 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div className="rk-scroll max-h-52 overflow-y-auto rounded-3xl border border-border bg-card px-5 py-4 text-start">
|
||||
{transcriptLines.length ? (
|
||||
<ul className="space-y-2.5">
|
||||
{transcriptLines.map((line, index) => (
|
||||
<li
|
||||
key={`${line.role}-${index}`}
|
||||
className={`text-ui-body-sm leading-relaxed ${
|
||||
line.role === "user" ? "text-foreground" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<span className="text-ui-label text-muted-foreground">
|
||||
{line.role === "user" ? t`You` : (line.speakerName ?? botName)}
|
||||
</span>
|
||||
<span dir="auto" className="mt-0.5 block">
|
||||
{line.text}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-ui-body-sm text-muted-foreground">
|
||||
<Trans>Your conversation will appear here.</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -42,4 +42,20 @@ describe("ChatMarkdown", () => {
|
||||
expect(html).toContain('aria-label="Copy code"');
|
||||
expect(html).toContain("rk-chat-markdown-copy");
|
||||
});
|
||||
|
||||
it("marks outbound links with target=_blank, external indicator, and renders inline hex color swatches", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<ChatMarkdown>
|
||||
{"See [Preview](https://example.com/review) and palette `#6B4226` or `#E8722A`."}
|
||||
</ChatMarkdown>,
|
||||
);
|
||||
|
||||
expect(html).toContain('href="https://example.com/review"');
|
||||
expect(html).toContain('target="_blank"');
|
||||
expect(html).toContain('rel="noreferrer noopener"');
|
||||
expect(html).toContain("rk-chat-markdown-external-link");
|
||||
expect(html).toContain("rk-chat-markdown-external-icon");
|
||||
expect(html).toContain('data-testid="markdown-color-swatch"');
|
||||
expect(html).toContain('data-color="#6B4226"');
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,6 +9,37 @@ import {
|
||||
StreamingVad,
|
||||
} from "@rakazo/core";
|
||||
import { selectedSpaceId, withSpaceHeaders } from "./rpc.js";
|
||||
import { readUiLocale } from "./ui-locale.js";
|
||||
|
||||
const SPEECH_LOCALE_TAGS: Record<string, string> = {
|
||||
de: "de-DE",
|
||||
en: "en-US",
|
||||
es: "es-ES",
|
||||
fr: "fr-FR",
|
||||
hi: "hi-IN",
|
||||
it: "it-IT",
|
||||
ja: "ja-JP",
|
||||
ko: "ko-KR",
|
||||
"pt-BR": "pt-BR",
|
||||
ru: "ru-RU",
|
||||
tr: "tr-TR",
|
||||
"zh-CN": "zh-CN",
|
||||
};
|
||||
|
||||
export function speechRecognitionLang(): string {
|
||||
const uiLocale = readUiLocale();
|
||||
const mapped = uiLocale ? SPEECH_LOCALE_TAGS[uiLocale] : undefined;
|
||||
if (uiLocale && mapped) {
|
||||
// Prefer browser's regional variant when it matches the UI language (e.g. de-AT / de-CH)
|
||||
const navLang = typeof navigator !== "undefined" ? navigator.language : "";
|
||||
const primary = uiLocale.split("-")[0]?.toLowerCase() ?? "";
|
||||
if (navLang?.toLowerCase().startsWith(`${primary}-`)) {
|
||||
return navLang;
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
return (typeof navigator !== "undefined" && navigator.language) || "de-DE";
|
||||
}
|
||||
|
||||
export type DictationMode = "hold" | "endpoint";
|
||||
|
||||
@ -205,7 +236,7 @@ export class Dictation {
|
||||
const rec = new Ctor();
|
||||
rec.continuous = true;
|
||||
rec.interimResults = true;
|
||||
rec.lang = navigator.language || "en-US";
|
||||
rec.lang = speechRecognitionLang();
|
||||
rec.onresult = (event) => {
|
||||
if (this.token !== mine) return;
|
||||
let transcript = "";
|
||||
|
||||
@ -98,13 +98,25 @@ describe("isToday", () => {
|
||||
});
|
||||
|
||||
describe("buildHeuteBrief", () => {
|
||||
it("collects needs-you, doing, and done buckets", () => {
|
||||
it("collects needs-you, doing, and done buckets, ignoring passive unread chats that do not need the user", () => {
|
||||
const brief = buildHeuteBrief({
|
||||
bots: [
|
||||
bot({ id: "bot-1", unread: true }),
|
||||
bot({ id: "bot-2", unread: true, updatedAt: "2026-09-22T08:00:00.000Z", name: "Steve" }),
|
||||
bot({ id: "bot-1", unread: true, status: "waiting_input" }),
|
||||
bot({
|
||||
id: "bot-2",
|
||||
unread: true,
|
||||
status: "waiting_takeover",
|
||||
updatedAt: "2026-09-22T08:00:00.000Z",
|
||||
name: "Steve",
|
||||
}),
|
||||
bot({
|
||||
id: "bot-passive",
|
||||
unread: true,
|
||||
status: "idle",
|
||||
name: "PassiveBot",
|
||||
}),
|
||||
],
|
||||
groups: [group({ updatedAt: "2026-09-22T09:30:00.000Z" })],
|
||||
groups: [group({ updatedAt: "2026-09-22T09:30:00.000Z", unread: true })],
|
||||
activeRuns: [
|
||||
run({ runId: "run-1", botId: "bot-1" }),
|
||||
run({ runId: "run-3", botId: "bot-4", botName: "Worker", status: "running" }),
|
||||
@ -157,7 +169,7 @@ describe("buildHeuteBrief", () => {
|
||||
|
||||
it("caps needs-you at three items", () => {
|
||||
const bots = Array.from({ length: 5 }, (_, index) =>
|
||||
bot({ id: `bot-${index}`, unread: true, name: `Bot ${index}` }),
|
||||
bot({ id: `bot-${index}`, unread: true, status: "waiting_input", name: `Bot ${index}` }),
|
||||
);
|
||||
const brief = buildHeuteBrief({
|
||||
bots,
|
||||
@ -169,6 +181,57 @@ describe("buildHeuteBrief", () => {
|
||||
expect(brief.needsYou).toHaveLength(HEUTE_NEEDS_YOU_CAP);
|
||||
expect(brief.actionCount).toBe(5);
|
||||
});
|
||||
|
||||
it("merges and deduplicates actionable unread bots and groups across spaces", () => {
|
||||
const brief = buildHeuteBrief({
|
||||
bots: [bot({ id: "bot-1", spaceId: "space-2", unread: false, status: "idle" })],
|
||||
groups: [],
|
||||
spaces: [
|
||||
{
|
||||
id: "space-1",
|
||||
bots: [
|
||||
bot({
|
||||
id: "bot-home-1",
|
||||
spaceId: "space-1",
|
||||
unread: true,
|
||||
status: "waiting_input",
|
||||
name: "Code Architect",
|
||||
}),
|
||||
bot({
|
||||
id: "bot-home-2",
|
||||
spaceId: "space-1",
|
||||
unread: true,
|
||||
status: "waiting_takeover",
|
||||
name: "UI Designer",
|
||||
}),
|
||||
],
|
||||
groups: [
|
||||
group({
|
||||
id: "group-home-1",
|
||||
spaceId: "space-1",
|
||||
unread: true,
|
||||
leadBotId: "bot-home-1",
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "space-2",
|
||||
// Current space's optimistic `bots` entry (unread: false) takes precedence over stale space snapshot
|
||||
bots: [bot({ id: "bot-1", spaceId: "space-2", unread: true, status: "waiting_input" })],
|
||||
groups: [],
|
||||
},
|
||||
],
|
||||
activeRuns: [],
|
||||
recentRuns: [],
|
||||
now,
|
||||
});
|
||||
expect(brief.actionCount).toBe(3);
|
||||
expect(brief.needsYou.map((item) => (item.kind === "unread" ? item.chat.id : ""))).toEqual([
|
||||
"bot-home-1",
|
||||
"bot-home-2",
|
||||
"group-home-1",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function habit(overrides: Partial<HabitToday> = {}): HabitToday {
|
||||
|
||||
@ -1,5 +1,18 @@
|
||||
import type { Bot, CoachToday, Group, RunActivityRow } from "@rakazo/contracts";
|
||||
import { isActionableRun, isDoingStatus, isFailedRoutine } from "./agent-activity";
|
||||
import type {
|
||||
Bot,
|
||||
CoachToday,
|
||||
Group,
|
||||
RunActivityRow,
|
||||
Space,
|
||||
SpaceBot,
|
||||
SpaceGroup,
|
||||
} from "@rakazo/contracts";
|
||||
import {
|
||||
isActionableRun,
|
||||
isDoingStatus,
|
||||
isFailedRoutine,
|
||||
isNeedsYouStatus,
|
||||
} from "./agent-activity";
|
||||
|
||||
export type JetztRunItem = {
|
||||
kind: "run";
|
||||
@ -9,7 +22,7 @@ export type JetztRunItem = {
|
||||
export type JetztUnreadItem = {
|
||||
kind: "unread";
|
||||
chatKind: "bot" | "group";
|
||||
chat: Bot | Group;
|
||||
chat: SpaceBot | (SpaceGroup & { leadBotId?: string | null });
|
||||
};
|
||||
|
||||
export type JetztItem = JetztRunItem | JetztUnreadItem;
|
||||
@ -76,16 +89,33 @@ function chatTargetKey(chatKind: "bot" | "group", chatId: string): string {
|
||||
}
|
||||
|
||||
export function buildHeuteBrief(params: {
|
||||
bots: Bot[];
|
||||
groups: Group[];
|
||||
bots: readonly SpaceBot[];
|
||||
groups: readonly (SpaceGroup & { leadBotId?: string | null })[];
|
||||
spaces?: readonly Pick<Space, "id" | "bots" | "groups">[];
|
||||
activeRuns: RunActivityRow[];
|
||||
recentRuns: RunActivityRow[];
|
||||
now?: Date;
|
||||
}): HeuteBriefData {
|
||||
const { bots, groups, activeRuns, recentRuns, now = new Date() } = params;
|
||||
const { bots, groups, spaces = [], activeRuns, recentRuns, now = new Date() } = params;
|
||||
const needsYouAll: JetztItem[] = [];
|
||||
const coveredTargets = new Set<string>();
|
||||
|
||||
const seenBotIds = new Set<string>();
|
||||
const allBots: SpaceBot[] = [];
|
||||
for (const bot of [...bots, ...spaces.flatMap((space) => space.bots)]) {
|
||||
if (seenBotIds.has(bot.id)) continue;
|
||||
seenBotIds.add(bot.id);
|
||||
allBots.push(bot);
|
||||
}
|
||||
|
||||
const seenGroupIds = new Set<string>();
|
||||
const allGroups: (SpaceGroup & { leadBotId?: string | null })[] = [];
|
||||
for (const group of [...groups, ...spaces.flatMap((space) => space.groups)]) {
|
||||
if (seenGroupIds.has(group.id)) continue;
|
||||
seenGroupIds.add(group.id);
|
||||
allGroups.push(group);
|
||||
}
|
||||
|
||||
const actionableRuns = [
|
||||
...activeRuns.filter(isActionableRun),
|
||||
...recentRuns.filter(isFailedRoutine),
|
||||
@ -98,12 +128,20 @@ export function buildHeuteBrief(params: {
|
||||
coveredTargets.add(runTargetKey(run));
|
||||
}
|
||||
|
||||
const botStatusById = new Map(allBots.map((bot) => [bot.id, bot.status]));
|
||||
const unreadChats: JetztUnreadItem[] = [
|
||||
...bots
|
||||
.filter((bot) => bot.unread)
|
||||
...allBots
|
||||
.filter((bot) => bot.unread && isNeedsYouStatus(bot.status))
|
||||
.map((bot) => ({ kind: "unread" as const, chatKind: "bot" as const, chat: bot })),
|
||||
...groups
|
||||
.filter((group) => group.unread)
|
||||
...allGroups
|
||||
.filter((group) => {
|
||||
if (!group.unread) return false;
|
||||
const memberBotIds = [
|
||||
...(group.leadBotId ? [group.leadBotId] : []),
|
||||
...(group.members?.map((m) => m.botId) ?? []),
|
||||
];
|
||||
return memberBotIds.some((id) => isNeedsYouStatus(botStatusById.get(id)));
|
||||
})
|
||||
.map((group) => ({
|
||||
kind: "unread" as const,
|
||||
chatKind: "group" as const,
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
|
||||
type AudioContextCtor = new () => AudioContext;
|
||||
|
||||
/** Master output ~−15 LUFS for short procedural peaks. */
|
||||
const SFX_MASTER_GAIN = 0.58;
|
||||
/** Master output ~−12 LUFS for clear, warm procedural peaks. */
|
||||
const SFX_MASTER_GAIN = 0.85;
|
||||
/** Duck depth when voice lane active (TTS or mic wash). */
|
||||
const SFX_DUCK_RATIO = 0.28;
|
||||
const DUCK_RAMP_S = 0.065;
|
||||
@ -22,8 +22,10 @@ class OoAudioBus {
|
||||
private ctx: AudioContext | null = null;
|
||||
private sfxGain: GainNode | null = null;
|
||||
private voiceHold = 0;
|
||||
private unlockBound = false;
|
||||
|
||||
ensureContext(): AudioContext | null {
|
||||
this.bindGestureUnlock();
|
||||
if (this.ctx && this.ctx.state !== "closed") return this.ctx;
|
||||
const Ctor = audioContextCtor();
|
||||
if (!Ctor) return null;
|
||||
@ -43,6 +45,34 @@ class OoAudioBus {
|
||||
}
|
||||
}
|
||||
|
||||
/** Unlocks Web Audio on the user's first click, key, or tap so async SSE events can play audio. */
|
||||
bindGestureUnlock(): void {
|
||||
if (
|
||||
this.unlockBound ||
|
||||
typeof window === "undefined" ||
|
||||
typeof window.addEventListener !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.unlockBound = true;
|
||||
const unlock = () => {
|
||||
const ctx = this.ensureContext();
|
||||
if (ctx && ctx.state === "suspended") {
|
||||
void ctx.resume().catch(() => undefined);
|
||||
}
|
||||
if (typeof window !== "undefined" && "speechSynthesis" in window) {
|
||||
try {
|
||||
window.speechSynthesis.getVoices?.();
|
||||
} catch {
|
||||
// Ignore voice preload errors.
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("pointerdown", unlock, { capture: true, passive: true });
|
||||
window.addEventListener("keydown", unlock, { capture: true, passive: true });
|
||||
window.addEventListener("touchstart", unlock, { capture: true, passive: true });
|
||||
}
|
||||
|
||||
/** Route for oo-sound / oo-vocals / oo-voice-mark oscillators. */
|
||||
sfxDestination(ctx: AudioContext): AudioNode {
|
||||
if (ctx !== this.ctx) {
|
||||
|
||||
@ -68,61 +68,62 @@ function playCueOnContext(ctx: AudioContext, cue: OoSoundCue) {
|
||||
|
||||
switch (cue) {
|
||||
case "app-start":
|
||||
sweep(ctx, master, 523.25, 659.25, t0, 0.14, { gain: 0.038, type: "sine" });
|
||||
sweep(ctx, master, 659.25, 493.88, t0 + 0.1, 0.18, { gain: 0.032, type: "triangle" });
|
||||
sweep(ctx, master, 523.25, 659.25, t0, 0.16, { gain: 0.11, type: "sine" });
|
||||
sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.2, { gain: 0.095, type: "triangle" });
|
||||
break;
|
||||
case "interact":
|
||||
tone(ctx, master, 1046.5, t0, 0.06, {
|
||||
gain: 0.018,
|
||||
attack: 0.003,
|
||||
release: 0.04,
|
||||
tone(ctx, master, 880, t0, 0.075, {
|
||||
gain: 0.065,
|
||||
attack: 0.004,
|
||||
release: 0.045,
|
||||
type: "triangle",
|
||||
});
|
||||
break;
|
||||
case "success-resolve":
|
||||
case "work-complete":
|
||||
// mint → gold resolve (~270 ms)
|
||||
sweep(ctx, master, 523.25, 659.25, t0, 0.12, { gain: 0.042, type: "sine" });
|
||||
sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.22, { gain: 0.036, type: "triangle" });
|
||||
tone(ctx, master, 880, t0 + 0.2, 0.28, { gain: 0.03, type: "sine" });
|
||||
sweep(ctx, master, 523.25, 659.25, t0, 0.13, { gain: 0.12, type: "sine" });
|
||||
sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.22, { gain: 0.105, type: "triangle" });
|
||||
tone(ctx, master, 880, t0 + 0.2, 0.28, { gain: 0.09, type: "sine" });
|
||||
break;
|
||||
case "need-you":
|
||||
sweep(ctx, master, 220, 330, t0, 0.1, { gain: 0.042, type: "triangle" });
|
||||
tone(ctx, master, 277.18, t0 + 0.09, 0.12, { gain: 0.035, release: 0.08 });
|
||||
sweep(ctx, master, 261.63, 392, t0, 0.12, { gain: 0.125, type: "triangle" });
|
||||
tone(ctx, master, 523.25, t0 + 0.1, 0.18, { gain: 0.11, release: 0.1 });
|
||||
break;
|
||||
case "listening-start":
|
||||
case "voice_listen":
|
||||
tone(ctx, master, 220, t0, 0.22, { gain: 0.045, type: "triangle" });
|
||||
tone(ctx, master, 330, t0 + 0.04, 0.28, { gain: 0.035 });
|
||||
tone(ctx, master, 293.66, t0, 0.2, { gain: 0.11, type: "triangle" });
|
||||
tone(ctx, master, 440, t0 + 0.05, 0.24, { gain: 0.095 });
|
||||
break;
|
||||
case "listening-stop":
|
||||
tone(ctx, master, 330, t0, 0.18, { gain: 0.03, release: 0.12 });
|
||||
tone(ctx, master, 392, t0, 0.16, { gain: 0.085, release: 0.1 });
|
||||
tone(ctx, master, 523.25, t0 + 0.06, 0.18, { gain: 0.075, release: 0.11 });
|
||||
break;
|
||||
case "speaking-start":
|
||||
tone(ctx, master, 440, t0, 0.2, { gain: 0.04, type: "sine" });
|
||||
sweep(ctx, master, 392, 523.25, t0, 0.14, { gain: 0.095, type: "sine" });
|
||||
break;
|
||||
case "speaking-done":
|
||||
tone(ctx, master, 523.25, t0, 0.24, { gain: 0.035 });
|
||||
tone(ctx, master, 392, t0 + 0.08, 0.3, { gain: 0.028, release: 0.14 });
|
||||
tone(ctx, master, 523.25, t0, 0.2, { gain: 0.085 });
|
||||
tone(ctx, master, 392, t0 + 0.08, 0.26, { gain: 0.075, release: 0.14 });
|
||||
break;
|
||||
case "voice_bargein":
|
||||
sweep(ctx, master, 523.25, 261.63, t0, 0.05, { gain: 0.032, type: "sine" });
|
||||
sweep(ctx, master, 523.25, 261.63, t0, 0.07, { gain: 0.09, type: "sine" });
|
||||
break;
|
||||
case "attention":
|
||||
tone(ctx, master, 277.18, t0, 0.16, { gain: 0.05, type: "triangle" });
|
||||
tone(ctx, master, 233.08, t0 + 0.12, 0.22, { gain: 0.045 });
|
||||
tone(ctx, master, 329.63, t0, 0.16, { gain: 0.13, type: "triangle" });
|
||||
tone(ctx, master, 261.63, t0 + 0.12, 0.22, { gain: 0.115 });
|
||||
break;
|
||||
case "message-sent":
|
||||
tone(ctx, master, 880, t0, 0.1, { gain: 0.022, attack: 0.004, release: 0.06 });
|
||||
sweep(ctx, master, 587.33, 880, t0, 0.11, { gain: 0.08, type: "sine" });
|
||||
break;
|
||||
case "handoff":
|
||||
tone(ctx, master, 349.23, t0, 0.2, { gain: 0.04 });
|
||||
tone(ctx, master, 466.16, t0 + 0.1, 0.24, { gain: 0.035, type: "triangle" });
|
||||
tone(ctx, master, 392, t0, 0.18, { gain: 0.11 });
|
||||
tone(ctx, master, 523.25, t0 + 0.09, 0.22, { gain: 0.1, type: "triangle" });
|
||||
break;
|
||||
case "group-spark":
|
||||
tone(ctx, master, 587.33, t0, 0.11, { gain: 0.032, attack: 0.004, type: "triangle" });
|
||||
tone(ctx, master, 739.99, t0 + 0.06, 0.14, { gain: 0.028, type: "sine" });
|
||||
tone(ctx, master, 880, t0 + 0.12, 0.1, { gain: 0.022, release: 0.05 });
|
||||
tone(ctx, master, 587.33, t0, 0.11, { gain: 0.095, attack: 0.004, type: "triangle" });
|
||||
tone(ctx, master, 739.99, t0 + 0.06, 0.14, { gain: 0.085, type: "sine" });
|
||||
tone(ctx, master, 880, t0 + 0.12, 0.12, { gain: 0.075, release: 0.06 });
|
||||
break;
|
||||
case "routine_start":
|
||||
tone(ctx, master, 392, t0, 0.08, { gain: 0.025, attack: 0.005, release: 0.03, type: "sine" });
|
||||
|
||||
@ -3,8 +3,8 @@ import { readOoSoundEnabled } from "./oo-sound-pref";
|
||||
|
||||
export type OoVocalReaction = "ouh" | "mhm" | "aaah" | "mm" | "nod-down" | "nod-up" | "hm";
|
||||
|
||||
const COOLDOWN_MIN_MS = 8_000;
|
||||
const COOLDOWN_MAX_MS = 12_000;
|
||||
const COOLDOWN_MIN_MS = 1_600;
|
||||
const COOLDOWN_MAX_MS = 2_800;
|
||||
|
||||
let lastPlayedAt = 0;
|
||||
let nextCooldownMs = COOLDOWN_MIN_MS;
|
||||
@ -28,17 +28,23 @@ function vocalFormant(
|
||||
) {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = "bandpass";
|
||||
filter.frequency.value = baseHz * 2.4;
|
||||
filter.Q.value = 4.5;
|
||||
osc.type = "triangle";
|
||||
const boostedPeak = Math.min(0.22, gainPeak * 3.2);
|
||||
osc.type = "sine";
|
||||
osc.frequency.setValueAtTime(baseHz, startAt);
|
||||
osc.frequency.exponentialRampToValueAtTime(baseHz * 1.12, startAt + duration * 0.75);
|
||||
gain.gain.setValueAtTime(0.0001, startAt);
|
||||
gain.gain.exponentialRampToValueAtTime(gainPeak, startAt + 0.025);
|
||||
gain.gain.exponentialRampToValueAtTime(boostedPeak, startAt + 0.02);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration);
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
if (typeof ctx.createBiquadFilter === "function") {
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = "bandpass";
|
||||
filter.frequency.value = baseHz * 1.6;
|
||||
filter.Q.value = 1.8;
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
} else {
|
||||
osc.connect(gain);
|
||||
}
|
||||
gain.connect(destination);
|
||||
osc.start(startAt);
|
||||
osc.stop(startAt + duration + 0.02);
|
||||
|
||||
@ -37,4 +37,10 @@ describe("queuedFollowUps", () => {
|
||||
const synthetic = userMessage("progress:1", "2026-01-01T10:00:05.000Z");
|
||||
expect(queuedFollowUps([synthetic], RUN_START, new Map())).toEqual([]);
|
||||
});
|
||||
|
||||
it("clears queued state once sendNow starts a newer run after the message", () => {
|
||||
const followUp = userMessage("queued-1", "2026-01-01T10:00:05.000Z");
|
||||
expect(queuedFollowUps([followUp], RUN_START, new Map())).toEqual([followUp]);
|
||||
expect(queuedFollowUps([followUp], "2026-01-01T10:00:05.001Z", new Map())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,25 +1 @@
|
||||
import type { ThreadMessage } from "@rakazo/contracts";
|
||||
|
||||
/** The bot marks a steering message with this reaction once it has picked it up. */
|
||||
const SEEN_REACTION = "👀";
|
||||
|
||||
/**
|
||||
* User messages sent after the active run started that the bot has not picked
|
||||
* up yet. The run's own trigger message predates the run, so it never counts.
|
||||
*/
|
||||
export function queuedFollowUps(
|
||||
messages: readonly ThreadMessage[],
|
||||
runCreatedAt: string | undefined,
|
||||
reactions: ReadonlyMap<string, ReadonlyMap<string, number>>,
|
||||
): ThreadMessage[] {
|
||||
if (!runCreatedAt) return [];
|
||||
const runStart = Date.parse(runCreatedAt);
|
||||
if (Number.isNaN(runStart)) return [];
|
||||
return messages.filter(
|
||||
(message) =>
|
||||
message.role === "user" &&
|
||||
!message.id.includes(":") &&
|
||||
Date.parse(message.createdAt) > runStart &&
|
||||
!reactions.get(message.id)?.has(SEEN_REACTION),
|
||||
);
|
||||
}
|
||||
export { queuedFollowUps } from "@rakazo/core";
|
||||
|
||||
@ -215,6 +215,53 @@ describe("Speaker", () => {
|
||||
expect(speaker.state.status).toBe("idle");
|
||||
expect(speaker.state.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it("filters out novelty voices and selects a natural German voice for German text", async () => {
|
||||
stubSelectedSpace("space-voice");
|
||||
const speak = vi.fn();
|
||||
const cancel = vi.fn();
|
||||
const eddy = { name: "Eddy (German (Germany))", lang: "de-DE", localService: true };
|
||||
const grandma = { name: "Grandma (German (Germany))", lang: "de-DE", localService: true };
|
||||
const anna = { name: "Anna", lang: "de-DE", localService: true };
|
||||
vi.stubGlobal(
|
||||
"SpeechSynthesisUtterance",
|
||||
class {
|
||||
onend: (() => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
voice: unknown = null;
|
||||
lang = "";
|
||||
rate = 1;
|
||||
pitch = 1;
|
||||
constructor(public text: string) {}
|
||||
},
|
||||
);
|
||||
vi.stubGlobal("window", {
|
||||
speechSynthesis: {
|
||||
cancel,
|
||||
getVoices: () => [eddy, grandma, anna],
|
||||
speak,
|
||||
},
|
||||
localStorage: globalThis.localStorage,
|
||||
});
|
||||
|
||||
const speaker = new Speaker();
|
||||
vi.spyOn(
|
||||
speaker as unknown as { prepare: () => Promise<string[]> },
|
||||
"prepare",
|
||||
).mockRejectedValue(new Error("offline"));
|
||||
|
||||
const pending = speaker.speak("Ich habe das Projekt für dich aktualisiert.");
|
||||
await vi.waitFor(() => expect(speak).toHaveBeenCalledOnce());
|
||||
const utterance = speak.mock.calls[0]?.[0] as {
|
||||
voice?: { name: string };
|
||||
lang?: string;
|
||||
onend?: () => void;
|
||||
};
|
||||
expect(utterance.lang).toBe("de-DE");
|
||||
expect(utterance.voice?.name).toBe("Anna");
|
||||
utterance.onend?.();
|
||||
await pending;
|
||||
});
|
||||
});
|
||||
|
||||
describe("UtteranceStreamer", () => {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { ClauseSegmenter, readBoundedResponseBytes, speakable } from "@rakazo/core";
|
||||
import { ooAudioBus } from "./oo-audio-bus.js";
|
||||
import { rpc, selectedSpaceId, withSpaceHeaders } from "./rpc.js";
|
||||
import { readUiLocale } from "./ui-locale.js";
|
||||
|
||||
export type SpeechStatus = "idle" | "preparing" | "speaking";
|
||||
|
||||
@ -76,7 +78,19 @@ export class Speaker {
|
||||
if (this.snapshot.status !== "idle" || this.snapshot.error) this.set(IDLE);
|
||||
}
|
||||
|
||||
private bufferSource: AudioBufferSourceNode | null = null;
|
||||
|
||||
private teardownAudio() {
|
||||
if (this.bufferSource) {
|
||||
try {
|
||||
this.bufferSource.onended = null;
|
||||
this.bufferSource.stop();
|
||||
this.bufferSource.disconnect();
|
||||
} catch {
|
||||
// Ignore already-stopped buffer source errors.
|
||||
}
|
||||
this.bufferSource = null;
|
||||
}
|
||||
if (this.audio) {
|
||||
this.audio.pause();
|
||||
this.audio.src = "";
|
||||
@ -225,16 +239,51 @@ export class Speaker {
|
||||
audio.onended = null;
|
||||
audio.onerror = null;
|
||||
if (this.settlePlayback === done) this.settlePlayback = null;
|
||||
if (this.audio === audio) this.teardownAudio();
|
||||
if (this.audio === audio || this.bufferSource) this.teardownAudio();
|
||||
resolve(ok);
|
||||
};
|
||||
this.settlePlayback = done;
|
||||
audio.onended = () => done(true);
|
||||
audio.onerror = () => done(false);
|
||||
audio.play().catch(() => done(false));
|
||||
audio.onerror = () => {
|
||||
void this.playViaWebAudio(blob, live).then(done);
|
||||
};
|
||||
audio.play().catch(() => {
|
||||
void this.playViaWebAudio(blob, live).then(done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async playViaWebAudio(blob: Blob, live: () => boolean): Promise<boolean> {
|
||||
if (!live()) return false;
|
||||
const ctx = ooAudioBus.ensureContext();
|
||||
if (
|
||||
!ctx ||
|
||||
typeof ctx.decodeAudioData !== "function" ||
|
||||
typeof ctx.createBufferSource !== "function"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
if (!live()) return false;
|
||||
const audioBuffer = await ctx.decodeAudioData(arrayBuffer.slice(0));
|
||||
if (!live()) return false;
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
const source = ctx.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(ctx.destination);
|
||||
this.bufferSource = source;
|
||||
source.onended = () => {
|
||||
if (this.bufferSource === source) this.bufferSource = null;
|
||||
resolve(true);
|
||||
};
|
||||
source.start(0);
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async tryBrowserFallback(
|
||||
text: string,
|
||||
opts: SpeakOptions,
|
||||
@ -255,17 +304,65 @@ export class Speaker {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
window.speechSynthesis.cancel();
|
||||
window.speechSynthesis.resume?.();
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
const voices = window.speechSynthesis.getVoices?.() ?? [];
|
||||
const isGerman = /[äöüß]|(?:und|der|die|das|ist|nicht|für|mit|den|dem|des)/i.test(text);
|
||||
const langPrefix = isGerman ? "de" : "en";
|
||||
const uiLocale = readUiLocale();
|
||||
const voiceId = opts.voiceId ?? "";
|
||||
const isGermanVoiceId = /^d[fm]_/i.test(voiceId);
|
||||
const isGermanText =
|
||||
/[äöüß]|(?:\b(?:und|der|die|das|ist|nicht|für|mit|den|dem|des|ein|eine|ich|wir|bitte)\b)/i.test(
|
||||
text,
|
||||
);
|
||||
const isGerman = isGermanVoiceId || uiLocale === "de" || isGermanText;
|
||||
const langPrefix = isGerman ? "de" : uiLocale.split("-")[0] || "en";
|
||||
utterance.lang = isGerman ? "de-DE" : langPrefix === "en" ? "en-US" : uiLocale;
|
||||
|
||||
const allLangVoices = voices.filter((v) =>
|
||||
(v.lang ?? "").toLowerCase().replace(/_/g, "-").startsWith(langPrefix),
|
||||
);
|
||||
const noveltyHint =
|
||||
/\b(?:eddy|flo|grandma|grandpa|reed|rocko|sandy|shelley|albert|bad news|bahh|bells|boing|bubbles|cellos|deranged|good news|hysterical|junior|organ|superstar|trinoids|whisper|zarvox|fred|kathy|ralph)\b/i;
|
||||
const naturalLangVoices = allLangVoices.filter((v) => !noveltyHint.test(v.name ?? ""));
|
||||
const langVoices = naturalLangVoices.length > 0 ? naturalLangVoices : allLangVoices;
|
||||
const wantsMale = /^dm_/i.test(voiceId);
|
||||
const wantsFemale = /^df_/i.test(voiceId) || (!wantsMale && isGerman);
|
||||
const maleHint =
|
||||
/yannick|martin|markus|stefan|viktor|hans|daniel|thomas|florian|conrad|male/i;
|
||||
const femaleHint = /anna|petra|helena|marlene|vicki|katja|seraphina|amala|siri|female/i;
|
||||
|
||||
const matchedVoice =
|
||||
voices.find((v) => v.lang.startsWith(langPrefix) && !v.localService) ||
|
||||
voices.find((v) => v.lang.startsWith(langPrefix)) ||
|
||||
(wantsMale &&
|
||||
(langVoices.find((v) => !v.localService && maleHint.test(v.name ?? "")) ||
|
||||
langVoices.find((v) => maleHint.test(v.name ?? "")))) ||
|
||||
(wantsFemale &&
|
||||
(langVoices.find((v) => !v.localService && femaleHint.test(v.name ?? "")) ||
|
||||
langVoices.find((v) => femaleHint.test(v.name ?? "")))) ||
|
||||
langVoices.find((v) => !v.localService) ||
|
||||
langVoices[0] ||
|
||||
voices[0];
|
||||
if (matchedVoice) utterance.voice = matchedVoice;
|
||||
utterance.rate = 1.05;
|
||||
utterance.pitch = 1.0;
|
||||
|
||||
// Distinct natural voice profiles for the 5 German presets when rendered via system TTS
|
||||
if (voiceId === "dm_karlsson") {
|
||||
utterance.rate = 0.98;
|
||||
utterance.pitch = 0.85;
|
||||
} else if (voiceId === "dm_thorsten") {
|
||||
utterance.rate = 1.03;
|
||||
utterance.pitch = 0.95;
|
||||
} else if (voiceId === "df_ramona") {
|
||||
utterance.rate = 0.97;
|
||||
utterance.pitch = 1.0;
|
||||
} else if (voiceId === "df_eva") {
|
||||
utterance.rate = 1.02;
|
||||
utterance.pitch = 1.04;
|
||||
} else if (voiceId === "df_kerstin") {
|
||||
utterance.rate = 1.05;
|
||||
utterance.pitch = 1.08;
|
||||
} else {
|
||||
utterance.rate = 1.04;
|
||||
utterance.pitch = 1.0;
|
||||
}
|
||||
|
||||
this.set({
|
||||
status: "speaking",
|
||||
|
||||
@ -100,3 +100,37 @@ export async function disableWebPush(): Promise<void> {
|
||||
await rpc.notifications.webPush.unsubscribe({ endpoint: subscription.endpoint });
|
||||
await subscription.unsubscribe().catch(() => undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* True on iOS/iPadOS when opened in a browser tab rather than as an installed Home-Screen PWA
|
||||
* (where Apple WebKit only exposes Web Push in standalone display mode).
|
||||
*/
|
||||
export function isIosBrowserNeedsHomeScreen(): boolean {
|
||||
if (typeof window === "undefined" || typeof navigator === "undefined" || desktopBridge()) {
|
||||
return false;
|
||||
}
|
||||
const ua = navigator.userAgent || "";
|
||||
const isIos =
|
||||
/iPad|iPhone|iPod/i.test(ua) ||
|
||||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
if (!isIos) return false;
|
||||
const standalone =
|
||||
(typeof window.matchMedia === "function" &&
|
||||
window.matchMedia("(display-mode: standalone)").matches) ||
|
||||
Boolean((navigator as Navigator & { standalone?: boolean }).standalone);
|
||||
return !standalone && !webPushSupported();
|
||||
}
|
||||
|
||||
/** Syncs the installed PWA / OS app icon badge with the actionable "Needs You" count. */
|
||||
export function syncAppBadge(count: number): void {
|
||||
if (typeof navigator === "undefined") return;
|
||||
const nav = navigator as Navigator & {
|
||||
setAppBadge?: (contents?: number) => Promise<void>;
|
||||
clearAppBadge?: () => Promise<void>;
|
||||
};
|
||||
if (count > 0 && typeof nav.setAppBadge === "function") {
|
||||
void nav.setAppBadge(count).catch(() => undefined);
|
||||
} else if (count <= 0 && typeof nav.clearAppBadge === "function") {
|
||||
void nav.clearAppBadge().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,11 +161,6 @@ msgstr "Browser von {0}"
|
||||
msgid "{0}'s browser appears here when the computer is running."
|
||||
msgstr "{0}s Browser erscheint hier, wenn der Computer läuft."
|
||||
|
||||
#. placeholder {0}: active.name
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "{0}'s screen"
|
||||
msgstr "{0}s Bildschirm"
|
||||
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
msgstr "{actionCount, plural, one {# Sache braucht dich} other {# Sachen brauchen dich}}"
|
||||
@ -182,10 +177,38 @@ msgstr "{botName} prüft die Architektur…"
|
||||
msgid "{botName} clears the path…"
|
||||
msgstr "{botName} räumt den Weg frei…"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is building"
|
||||
msgstr "{botName} baut & tüftelt"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is checking a problem"
|
||||
msgstr "{botName} prüft ein Problem"
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
msgid "{botName} is on it…"
|
||||
msgstr "{botName} ist dran…"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is replying"
|
||||
msgstr "{botName} antwortet"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is researching"
|
||||
msgstr "{botName} liest & recherchiert"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is thinking"
|
||||
msgstr "{botName} denkt nach"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is waiting for your answer"
|
||||
msgstr "{botName} wartet auf deine Antwort"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is working"
|
||||
msgstr "{botName} arbeitet"
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
#: src/pages/CallView.tsx
|
||||
msgid "{botName} is working…"
|
||||
@ -364,6 +387,7 @@ msgstr "Noch {remaining} · Bot beobachtet nur"
|
||||
msgid "{size} B"
|
||||
msgstr "{size} B"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||
msgstr "{stepCount, plural, one {# Schritt} other {# Schritte}}"
|
||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
msgstr "{toolCount, plural, one {# Tool} other {# Tools}}"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{workingBotName} is working"
|
||||
msgstr "{workingBotName} arbeitet"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "/Users/you/projects/acme"
|
||||
@ -433,7 +453,7 @@ msgstr "Über Gruppen"
|
||||
|
||||
#: src/pages/shell/bot-picker.tsx
|
||||
msgid "About spaces"
|
||||
msgstr "Über Spaces"
|
||||
msgstr "Über Projekte"
|
||||
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
msgid "About you"
|
||||
@ -544,11 +564,16 @@ msgstr "Weiteres hinzufügen"
|
||||
msgid "Add as projects"
|
||||
msgstr "Als Projekte hinzufügen"
|
||||
|
||||
#: src/components/NotificationSettingsPanel.tsx
|
||||
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
msgstr "Füge evOo über „Teilen“ zum Home-Bildschirm hinzu, um Push-Mitteilungen auf iOS zu aktivieren."
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Add Executor"
|
||||
msgstr "Executor hinzufügen"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Add goal"
|
||||
msgstr "Ziel hinzufügen"
|
||||
|
||||
@ -737,6 +762,14 @@ msgstr "Immer erlaubt"
|
||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||
msgstr "Analysiert die Aufgabe und führt Tools aus…"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Analyze the project and give me a brief overview."
|
||||
msgstr "Analysiere das Projekt und gib mir einen kurzen Überblick."
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Analyzing project…"
|
||||
msgstr "Projekt wird analysiert…"
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
msgid "Answer"
|
||||
msgstr "Antwort"
|
||||
@ -996,7 +1029,7 @@ msgstr "Körper"
|
||||
msgid "Booting live desktop…"
|
||||
msgstr "Live-Desktop wird gestartet…"
|
||||
|
||||
#. placeholder {0}: active?.name
|
||||
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Booting up {0}’s computer"
|
||||
msgstr "Computer von {0} wird gestartet"
|
||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
||||
msgid "Bot screen"
|
||||
msgstr "Bot-Bildschirm"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Bot screen preview"
|
||||
msgstr "Bot-Bildschirmvorschau"
|
||||
|
||||
#: src/pages/MessagingSettingsOverlay.tsx
|
||||
msgid "Bot to link"
|
||||
msgstr "Zu verknüpfender Bot"
|
||||
@ -1031,10 +1060,6 @@ msgstr "Zu verknüpfender Bot"
|
||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
msgstr "Bots handeln standardmäßig ohne Nachfrage. Füge nur dann eine Ausnahme hinzu, wenn du eine Aktionsart zuerst prüfen möchtest."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Bots are working"
|
||||
msgstr "Bots arbeiten"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Bow"
|
||||
msgstr "Schleife"
|
||||
@ -1064,7 +1089,6 @@ msgstr "Durchsuchen"
|
||||
msgid "Browse MCP servers"
|
||||
msgstr "MCP-Server durchsuchen"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Browser"
|
||||
msgstr "Browser"
|
||||
@ -1493,6 +1517,10 @@ msgstr "Computer"
|
||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
msgstr "Computer sind aus. Setze SANDBOX_PROVIDER=docker mit SANDBOX_SUPERVISOR_TOKEN, oder setze SANDBOX_PROVIDER auf e2b, daytona oder box mit dem passenden API-Key. Stack nach der Änderung von .env neu erstellen."
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Configure a plugin catalog on the server to connect apps."
|
||||
msgstr "Richte auf dem Server einen Plugin-Katalog ein, um Apps zu verbinden."
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Configured by deployment"
|
||||
msgstr "Durch die Bereitstellung konfiguriert"
|
||||
@ -1584,6 +1612,10 @@ msgstr "Verbunden, {0} wird verwendet."
|
||||
msgid "Connected app"
|
||||
msgstr "Verbundene App"
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Connected tools"
|
||||
msgstr "Verbundene Tools"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Connected. Its tools are available from your next message."
|
||||
msgstr "Verbunden. Die Tools sind ab deiner nächsten Nachricht verfügbar."
|
||||
@ -2077,7 +2109,7 @@ msgstr "Neue Gruppe erstellen"
|
||||
|
||||
#: src/pages/shell/bot-picker.tsx
|
||||
msgid "Create new Space"
|
||||
msgstr "Neuen Space erstellen"
|
||||
msgstr "Neues Projekt erstellen"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
msgid "Create project"
|
||||
@ -2086,7 +2118,7 @@ msgstr "Projekt erstellen"
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
msgid "Create space"
|
||||
msgstr "Space erstellen"
|
||||
msgstr "Projekt erstellen"
|
||||
|
||||
#: src/components/integrations/GithubConnect.tsx
|
||||
msgid "Create token on GitHub"
|
||||
@ -2241,7 +2273,7 @@ msgstr "Erinnerungen ebenfalls löschen"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Delete space"
|
||||
msgstr "Space löschen"
|
||||
msgstr "Projekt löschen"
|
||||
|
||||
#: src/pages/PrivacySettingsOverlay.tsx
|
||||
msgid "Delete your account?"
|
||||
@ -2322,6 +2354,7 @@ msgstr "Verbindung wird getrennt…"
|
||||
#: src/components/ComputerUpdateProgress.tsx
|
||||
#: src/components/teach/TeachComputerOverlay.tsx
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Dismiss"
|
||||
msgstr "Ausblenden"
|
||||
@ -2381,7 +2414,9 @@ msgstr "Kein Stress — wir sind jetzt da."
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Done"
|
||||
msgstr "Fertig"
|
||||
|
||||
@ -2394,8 +2429,11 @@ msgstr "Erledigt"
|
||||
msgid "Dot"
|
||||
msgstr "Punkt"
|
||||
|
||||
#. placeholder {0}: activeItem.name
|
||||
#. placeholder {0}: item.name
|
||||
#. placeholder {0}: props.name
|
||||
#: src/components/ArtifactFileCard.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {0}"
|
||||
msgstr "{0} herunterladen"
|
||||
|
||||
@ -2403,7 +2441,6 @@ msgstr "{0} herunterladen"
|
||||
#: src/components/ArtifactPane.tsx
|
||||
#: src/components/media/MediaCard.tsx
|
||||
#: src/components/media/MediaPlayer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {name}"
|
||||
msgstr "{name} herunterladen"
|
||||
|
||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
||||
msgstr "Erweitern"
|
||||
|
||||
#. placeholder {0}: group.title
|
||||
#. placeholder {0}: item.name
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/sidebar-group-header.tsx
|
||||
msgid "Expand {0}"
|
||||
msgstr "{0} ausklappen"
|
||||
@ -2660,6 +2699,10 @@ msgstr "Workspace-Dateien konnten nicht geladen werden"
|
||||
msgid "Failed to open file"
|
||||
msgstr "Datei konnte nicht geöffnet werden"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to remove message"
|
||||
msgstr "Nachricht konnte nicht entfernt werden"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to send message"
|
||||
msgstr "Nachricht konnte nicht gesendet werden"
|
||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
||||
msgstr "Das sehe ich in den Logs…"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgstr "Hallo, so klinge ich, wenn ich Antworten vorlese."
|
||||
msgid "Hi, this is how I'll sound when we talk."
|
||||
msgstr "Hallo, so klinge ich, wenn wir miteinander sprechen."
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/sidebar-header.tsx
|
||||
msgid "Hide bots"
|
||||
msgstr "Bots ausblenden"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Hide details"
|
||||
msgstr "Details ausblenden"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Hide password"
|
||||
msgstr "Passwort verbergen"
|
||||
@ -3080,6 +3128,7 @@ msgstr "Bot importieren"
|
||||
msgid "Import OpenAPI JSON"
|
||||
msgstr "OpenAPI-JSON importieren"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "In Bearbeitung"
|
||||
msgstr "In Bearbeitung"
|
||||
@ -3166,6 +3215,10 @@ msgstr "Isoliert"
|
||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
msgstr "Seine Unterhaltung, Dateien und Routinen werden dauerhaft gelöscht. Von ihm erstellte Bots bleiben in deiner Liste."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "joined"
|
||||
msgstr "dazugeschaltet"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Jump to latest"
|
||||
msgstr "Zur neuesten Nachricht springen"
|
||||
@ -3174,6 +3227,10 @@ msgstr "Zur neuesten Nachricht springen"
|
||||
msgid "Jump to replied message"
|
||||
msgstr "Zur beantworteten Nachricht springen"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Just ask in the chat whenever something should happen regularly."
|
||||
msgstr "Sag einfach im Chat Bescheid, wenn etwas regelmäßig erledigt werden soll."
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
msgid "just now"
|
||||
@ -3771,9 +3828,10 @@ msgstr "Neuer Skill"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
msgid "New space"
|
||||
msgstr "Neuer Space"
|
||||
msgstr "Neues Projekt"
|
||||
|
||||
#: src/components/CookingMode.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Next"
|
||||
msgstr "Weiter"
|
||||
|
||||
@ -3885,6 +3943,10 @@ msgstr "Noch keine Projekte."
|
||||
msgid "No providers found."
|
||||
msgstr "Keine Anbieter gefunden."
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
msgid "No recent activity"
|
||||
msgstr "Keine letzte Aktivität"
|
||||
|
||||
#: src/components/integrations/IntegrationSetup.tsx
|
||||
msgid "No remote MCP servers found"
|
||||
msgstr "Keine Remote-MCP-Server gefunden"
|
||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
||||
msgstr "Keine Ergebnisse"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "No runs yet"
|
||||
msgstr "Noch keine Ausführungen"
|
||||
|
||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
||||
msgstr "Benachrichtigungen"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Now"
|
||||
msgstr "Jetzt"
|
||||
|
||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
||||
msgstr "Name des Oo"
|
||||
|
||||
#: src/pages/ScratchpadSection.tsx
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Open"
|
||||
msgstr "Öffnen"
|
||||
|
||||
@ -4056,7 +4119,7 @@ msgstr "Unterhaltung öffnen"
|
||||
msgid "Open in browser"
|
||||
msgstr "Im Browser öffnen"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
#: src/components/browser/OoComputerBrowser.tsx
|
||||
msgid "Open in full window"
|
||||
msgstr "In vollständigem Fenster öffnen"
|
||||
|
||||
@ -4288,10 +4351,6 @@ msgstr "Privater Arbeitsbereich mit eigenen Bots und Gruppen."
|
||||
msgid "Project"
|
||||
msgstr "Projekt"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Project · sessions & folders"
|
||||
msgstr "Projekt · Sessions & Ordner"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "Project folder"
|
||||
@ -4399,6 +4458,10 @@ msgstr "Lese den Stacktrace…"
|
||||
msgid "Ready"
|
||||
msgstr "Bereit"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Ready"
|
||||
msgstr "Bereit"
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Reasoning effort"
|
||||
msgstr "Denkaufwand"
|
||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
||||
msgstr "Nur abrufen"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Recent"
|
||||
msgstr "Kürzlich"
|
||||
|
||||
@ -4528,6 +4592,10 @@ msgstr "{0} entfernen"
|
||||
msgid "Remove folder"
|
||||
msgstr "Ordner entfernen"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Remove from queue"
|
||||
msgstr "Aus der Warteschlange entfernen"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
msgid "Remove Git event"
|
||||
msgstr "Git-Ereignis entfernen"
|
||||
@ -4653,6 +4721,11 @@ msgstr "Stellt den zuletzt gespeicherten Arbeitsbereich wieder her. Nicht gespei
|
||||
msgid "Restoring your workspace"
|
||||
msgstr "Dein Arbeitsbereich wird wiederhergestellt"
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Resume"
|
||||
msgstr "Fortsetzen"
|
||||
|
||||
#: src/components/FolderPickerDialog.tsx
|
||||
msgid "Retry"
|
||||
msgstr "Wiederholen"
|
||||
@ -4700,11 +4773,13 @@ msgstr "Rund"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/routine-event-line.tsx
|
||||
msgid "Routine"
|
||||
msgstr "Routine"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Routines"
|
||||
msgstr "Routinen"
|
||||
|
||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
||||
msgstr "Ausführen um"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Run history"
|
||||
msgstr "Ausführungsverlauf"
|
||||
|
||||
@ -4756,10 +4832,6 @@ msgstr "Wird ausgeführt…"
|
||||
msgid "Runs when this bot receives a verified message from this provider."
|
||||
msgstr "Läuft, wenn dieser Bot eine verifizierte Nachricht von diesem Anbieter erhält."
|
||||
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Sandbox"
|
||||
msgstr "Sandbox"
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/components/MediaSettings.tsx
|
||||
#: src/components/teach/SkillDraftCard.tsx
|
||||
@ -4909,6 +4981,10 @@ msgstr "Antwort senden"
|
||||
msgid "Send it"
|
||||
msgstr "Senden"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Send now"
|
||||
msgstr "Jetzt gleich senden"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Send reset link"
|
||||
msgstr "Link zum Zurücksetzen senden"
|
||||
@ -5021,6 +5097,10 @@ msgstr "Bots anzeigen"
|
||||
msgid "Show computer"
|
||||
msgstr "Computer anzeigen"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Show details"
|
||||
msgstr "Details anzeigen"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Show less"
|
||||
msgstr "Weniger anzeigen"
|
||||
@ -5037,6 +5117,7 @@ msgstr "Mehr anzeigen"
|
||||
msgid "Show password"
|
||||
msgstr "Passwort anzeigen"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Show settings"
|
||||
msgstr "Einstellungen anzeigen"
|
||||
@ -5136,7 +5217,7 @@ msgstr "Space-Standard"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
msgid "Spaces"
|
||||
msgstr "Spaces"
|
||||
msgstr "Projekte"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Sparkle"
|
||||
@ -5172,6 +5253,7 @@ msgstr "Vorlesen an oder aus"
|
||||
msgid "Speaking with intent…"
|
||||
msgstr "Spreche mit Bedacht…"
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
#: src/lib/onboarding-copy.ts
|
||||
msgid "Specialist crew"
|
||||
msgstr "Spezialisten-Team"
|
||||
@ -5286,6 +5368,10 @@ msgstr "Anlernen beenden"
|
||||
msgid "Stop timer"
|
||||
msgstr "Timer stoppen"
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Stopped"
|
||||
msgstr "Gestoppt"
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/lib/browser-notifications.ts
|
||||
msgid "Stopped."
|
||||
@ -5398,6 +5484,7 @@ msgstr "Aufgabe wird angelernt. Beende das Anlernen, bevor du eine neue Nachrich
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Team"
|
||||
msgstr "Team"
|
||||
|
||||
@ -5512,10 +5599,6 @@ msgstr "Diese Abonnementanmeldung ist in Rakazo noch nicht verfügbar. Verwende
|
||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
msgstr "Dadurch werden dieses Projekt und alle zugehörigen Unterhaltungen, Bots und Erinnerungen dauerhaft gelöscht."
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Threads by topic · folders"
|
||||
msgstr "Unterhaltungen nach Thema · Ordner"
|
||||
|
||||
#: src/pages/ModelTierSettings.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
msgid "Tiers"
|
||||
@ -5701,6 +5784,7 @@ msgstr "Bis wann?"
|
||||
|
||||
#: src/components/DesktopUpdates.tsx
|
||||
#: src/components/SoftwareUpdateSection.tsx
|
||||
#: src/pages/AccountSettingsOverlay.tsx
|
||||
msgid "Up to date"
|
||||
msgstr "Aktuell"
|
||||
|
||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
||||
msgstr "Arbeitskanal offen — leg los."
|
||||
|
||||
#: src/components/stage/OoStage.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Working"
|
||||
msgstr "Arbeitet"
|
||||
|
||||
@ -6037,6 +6122,7 @@ msgstr "Gestern"
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
#: src/components/voice/VoiceStage.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "You"
|
||||
msgstr "Du"
|
||||
|
||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
||||
msgstr "Dein Name"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Your Oo reads replies aloud when this is on."
|
||||
msgstr "Wenn das an ist, liest dein Oo Antworten vor."
|
||||
msgid "Your Oo talks with you naturally when this is on."
|
||||
msgstr "Wenn das an ist, spricht dein Oo ganz natürlich mit dir."
|
||||
|
||||
#: src/pages/Onboarding.tsx
|
||||
msgid "Your Oo will communicate in this language. You can change it later."
|
||||
msgstr "Dein Oo kommuniziert in dieser Sprache. Du kannst sie später ändern."
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Your profile"
|
||||
msgstr "Dein Profil"
|
||||
|
||||
#: src/pages/Welcome.tsx
|
||||
msgid "Your specialists. One calm place to talk."
|
||||
msgstr "Deine Spezialisten. Ein ruhiger Ort zum Reden."
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom in"
|
||||
msgstr "Vergrößern"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom out"
|
||||
msgstr "Verkleinern"
|
||||
|
||||
@ -161,11 +161,6 @@ msgstr "{0}'s browser"
|
||||
msgid "{0}'s browser appears here when the computer is running."
|
||||
msgstr "{0}'s browser appears here when the computer is running."
|
||||
|
||||
#. placeholder {0}: active.name
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "{0}'s screen"
|
||||
msgstr "{0}'s screen"
|
||||
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
msgstr "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
@ -182,10 +177,38 @@ msgstr "{botName} checks the architecture…"
|
||||
msgid "{botName} clears the path…"
|
||||
msgstr "{botName} clears the path…"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is building"
|
||||
msgstr "{botName} is building"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is checking a problem"
|
||||
msgstr "{botName} is checking a problem"
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
msgid "{botName} is on it…"
|
||||
msgstr "{botName} is on it…"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is replying"
|
||||
msgstr "{botName} is replying"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is researching"
|
||||
msgstr "{botName} is researching"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is thinking"
|
||||
msgstr "{botName} is thinking"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is waiting for your answer"
|
||||
msgstr "{botName} is waiting for your answer"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is working"
|
||||
msgstr "{botName} is working"
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
#: src/pages/CallView.tsx
|
||||
msgid "{botName} is working…"
|
||||
@ -364,6 +387,7 @@ msgstr "{remaining} left · bot is watching, not acting"
|
||||
msgid "{size} B"
|
||||
msgstr "{size} B"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||
msgstr "{stepCount, plural, one {# step} other {# steps}}"
|
||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
msgstr "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{workingBotName} is working"
|
||||
msgstr "{workingBotName} is working"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "/Users/you/projects/acme"
|
||||
@ -544,11 +564,16 @@ msgstr "Add another"
|
||||
msgid "Add as projects"
|
||||
msgstr "Add as projects"
|
||||
|
||||
#: src/components/NotificationSettingsPanel.tsx
|
||||
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
msgstr "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Add Executor"
|
||||
msgstr "Add Executor"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Add goal"
|
||||
msgstr "Add goal"
|
||||
|
||||
@ -737,6 +762,14 @@ msgstr "Always allowed"
|
||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||
msgstr "Analysiert die Aufgabe und führt Tools aus…"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Analyze the project and give me a brief overview."
|
||||
msgstr "Analyze the project and give me a brief overview."
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Analyzing project…"
|
||||
msgstr "Analyzing project…"
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
msgid "Answer"
|
||||
msgstr "Answer"
|
||||
@ -996,7 +1029,7 @@ msgstr "Body"
|
||||
msgid "Booting live desktop…"
|
||||
msgstr "Booting live desktop…"
|
||||
|
||||
#. placeholder {0}: active?.name
|
||||
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Booting up {0}’s computer"
|
||||
msgstr "Booting up {0}’s computer"
|
||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
||||
msgid "Bot screen"
|
||||
msgstr "Bot screen"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Bot screen preview"
|
||||
msgstr "Bot screen preview"
|
||||
|
||||
#: src/pages/MessagingSettingsOverlay.tsx
|
||||
msgid "Bot to link"
|
||||
msgstr "Bot to link"
|
||||
@ -1031,10 +1060,6 @@ msgstr "Bot to link"
|
||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
msgstr "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Bots are working"
|
||||
msgstr "Bots are working"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Bow"
|
||||
msgstr "Bow"
|
||||
@ -1064,7 +1089,6 @@ msgstr "Browse"
|
||||
msgid "Browse MCP servers"
|
||||
msgstr "Browse MCP servers"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Browser"
|
||||
msgstr "Browser"
|
||||
@ -1493,6 +1517,10 @@ msgstr "Computers"
|
||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
msgstr "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Configure a plugin catalog on the server to connect apps."
|
||||
msgstr "Configure a plugin catalog on the server to connect apps."
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Configured by deployment"
|
||||
msgstr "Configured by deployment"
|
||||
@ -1584,6 +1612,10 @@ msgstr "Connected and using {0}."
|
||||
msgid "Connected app"
|
||||
msgstr "Connected app"
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Connected tools"
|
||||
msgstr "Connected tools"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Connected. Its tools are available from your next message."
|
||||
msgstr "Connected. Its tools are available from your next message."
|
||||
@ -2322,6 +2354,7 @@ msgstr "Disconnecting…"
|
||||
#: src/components/ComputerUpdateProgress.tsx
|
||||
#: src/components/teach/TeachComputerOverlay.tsx
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Dismiss"
|
||||
msgstr "Dismiss"
|
||||
@ -2381,7 +2414,9 @@ msgstr "Don't stress — we're here now."
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Done"
|
||||
msgstr "Done"
|
||||
|
||||
@ -2394,8 +2429,11 @@ msgstr "Done"
|
||||
msgid "Dot"
|
||||
msgstr "Dot"
|
||||
|
||||
#. placeholder {0}: activeItem.name
|
||||
#. placeholder {0}: item.name
|
||||
#. placeholder {0}: props.name
|
||||
#: src/components/ArtifactFileCard.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {0}"
|
||||
msgstr "Download {0}"
|
||||
|
||||
@ -2403,7 +2441,6 @@ msgstr "Download {0}"
|
||||
#: src/components/ArtifactPane.tsx
|
||||
#: src/components/media/MediaCard.tsx
|
||||
#: src/components/media/MediaPlayer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {name}"
|
||||
msgstr "Download {name}"
|
||||
|
||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
||||
msgstr "Expand"
|
||||
|
||||
#. placeholder {0}: group.title
|
||||
#. placeholder {0}: item.name
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/sidebar-group-header.tsx
|
||||
msgid "Expand {0}"
|
||||
msgstr "Expand {0}"
|
||||
@ -2660,6 +2699,10 @@ msgstr "Failed to load workspace files"
|
||||
msgid "Failed to open file"
|
||||
msgstr "Failed to open file"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to remove message"
|
||||
msgstr "Failed to remove message"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to send message"
|
||||
msgstr "Failed to send message"
|
||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
||||
msgstr "Here's what I see in the logs…"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgstr "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgid "Hi, this is how I'll sound when we talk."
|
||||
msgstr "Hi, this is how I'll sound when we talk."
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/sidebar-header.tsx
|
||||
msgid "Hide bots"
|
||||
msgstr "Hide bots"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Hide details"
|
||||
msgstr "Hide details"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Hide password"
|
||||
msgstr "Hide password"
|
||||
@ -3080,6 +3128,7 @@ msgstr "Import bot"
|
||||
msgid "Import OpenAPI JSON"
|
||||
msgstr "Import OpenAPI JSON"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "In Bearbeitung"
|
||||
msgstr "In Bearbeitung"
|
||||
@ -3166,6 +3215,10 @@ msgstr "Isolated"
|
||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
msgstr "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "joined"
|
||||
msgstr "joined"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Jump to latest"
|
||||
msgstr "Jump to latest"
|
||||
@ -3174,6 +3227,10 @@ msgstr "Jump to latest"
|
||||
msgid "Jump to replied message"
|
||||
msgstr "Jump to replied message"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Just ask in the chat whenever something should happen regularly."
|
||||
msgstr "Just ask in the chat whenever something should happen regularly."
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
msgid "just now"
|
||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
||||
msgstr "New space"
|
||||
|
||||
#: src/components/CookingMode.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Next"
|
||||
msgstr "Next"
|
||||
|
||||
@ -3885,6 +3943,10 @@ msgstr "No projects yet."
|
||||
msgid "No providers found."
|
||||
msgstr "No providers found."
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
msgid "No recent activity"
|
||||
msgstr "No recent activity"
|
||||
|
||||
#: src/components/integrations/IntegrationSetup.tsx
|
||||
msgid "No remote MCP servers found"
|
||||
msgstr "No remote MCP servers found"
|
||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
||||
msgstr "No results"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "No runs yet"
|
||||
msgstr "No runs yet"
|
||||
|
||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
||||
msgstr "Notifications"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Now"
|
||||
msgstr "Now"
|
||||
|
||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
||||
msgstr "Oo name"
|
||||
|
||||
#: src/pages/ScratchpadSection.tsx
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Open"
|
||||
msgstr "Open"
|
||||
|
||||
@ -4056,7 +4119,7 @@ msgstr "Open conversation"
|
||||
msgid "Open in browser"
|
||||
msgstr "Open in browser"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
#: src/components/browser/OoComputerBrowser.tsx
|
||||
msgid "Open in full window"
|
||||
msgstr "Open in full window"
|
||||
|
||||
@ -4288,10 +4351,6 @@ msgstr "Private workspace with its own bots and groups."
|
||||
msgid "Project"
|
||||
msgstr "Project"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Project · sessions & folders"
|
||||
msgstr "Project · sessions & folders"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "Project folder"
|
||||
@ -4399,6 +4458,10 @@ msgstr "Reading the stack trace…"
|
||||
msgid "Ready"
|
||||
msgstr "Ready"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Ready"
|
||||
msgstr "Ready"
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Reasoning effort"
|
||||
msgstr "Reasoning effort"
|
||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
||||
msgstr "Recall only"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Recent"
|
||||
msgstr "Recent"
|
||||
|
||||
@ -4528,6 +4592,10 @@ msgstr "Remove {0}"
|
||||
msgid "Remove folder"
|
||||
msgstr "Remove folder"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Remove from queue"
|
||||
msgstr "Remove from queue"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
msgid "Remove Git event"
|
||||
msgstr "Remove Git event"
|
||||
@ -4653,6 +4721,11 @@ msgstr "Restore the last saved workspace. Unsaved work on the computer is lost."
|
||||
msgid "Restoring your workspace"
|
||||
msgstr "Restoring your workspace"
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Resume"
|
||||
msgstr "Resume"
|
||||
|
||||
#: src/components/FolderPickerDialog.tsx
|
||||
msgid "Retry"
|
||||
msgstr "Retry"
|
||||
@ -4700,11 +4773,13 @@ msgstr "Round"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/routine-event-line.tsx
|
||||
msgid "Routine"
|
||||
msgstr "Routine"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Routines"
|
||||
msgstr "Routines"
|
||||
|
||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
||||
msgstr "Run at"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Run history"
|
||||
msgstr "Run history"
|
||||
|
||||
@ -4756,10 +4832,6 @@ msgstr "Running…"
|
||||
msgid "Runs when this bot receives a verified message from this provider."
|
||||
msgstr "Runs when this bot receives a verified message from this provider."
|
||||
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Sandbox"
|
||||
msgstr "Sandbox"
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/components/MediaSettings.tsx
|
||||
#: src/components/teach/SkillDraftCard.tsx
|
||||
@ -4909,6 +4981,10 @@ msgstr "Send answer"
|
||||
msgid "Send it"
|
||||
msgstr "Send it"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Send now"
|
||||
msgstr "Send now"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Send reset link"
|
||||
msgstr "Send reset link"
|
||||
@ -5021,6 +5097,10 @@ msgstr "Show bots"
|
||||
msgid "Show computer"
|
||||
msgstr "Show computer"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Show details"
|
||||
msgstr "Show details"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Show less"
|
||||
msgstr "Show less"
|
||||
@ -5037,6 +5117,7 @@ msgstr "Show more"
|
||||
msgid "Show password"
|
||||
msgstr "Show password"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Show settings"
|
||||
msgstr "Show settings"
|
||||
@ -5172,6 +5253,7 @@ msgstr "Speaking on or off"
|
||||
msgid "Speaking with intent…"
|
||||
msgstr "Speaking with intent…"
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
#: src/lib/onboarding-copy.ts
|
||||
msgid "Specialist crew"
|
||||
msgstr "Specialist crew"
|
||||
@ -5286,6 +5368,10 @@ msgstr "Stop teaching"
|
||||
msgid "Stop timer"
|
||||
msgstr "Stop timer"
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Stopped"
|
||||
msgstr "Stopped"
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/lib/browser-notifications.ts
|
||||
msgid "Stopped."
|
||||
@ -5398,6 +5484,7 @@ msgstr "Teaching in progress. Stop teaching before sending a new message."
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Team"
|
||||
msgstr "Team"
|
||||
|
||||
@ -5512,10 +5599,6 @@ msgstr "This subscription sign-in is not available in Rakazo yet. Use a deployme
|
||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
msgstr "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Threads by topic · folders"
|
||||
msgstr "Threads by topic · folders"
|
||||
|
||||
#: src/pages/ModelTierSettings.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
msgid "Tiers"
|
||||
@ -5701,6 +5784,7 @@ msgstr "Until when?"
|
||||
|
||||
#: src/components/DesktopUpdates.tsx
|
||||
#: src/components/SoftwareUpdateSection.tsx
|
||||
#: src/pages/AccountSettingsOverlay.tsx
|
||||
msgid "Up to date"
|
||||
msgstr "Up to date"
|
||||
|
||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
||||
msgstr "Work channel open — go ahead."
|
||||
|
||||
#: src/components/stage/OoStage.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Working"
|
||||
msgstr "Working"
|
||||
|
||||
@ -6037,6 +6122,7 @@ msgstr "Yesterday"
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
#: src/components/voice/VoiceStage.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "You"
|
||||
msgstr "You"
|
||||
|
||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
||||
msgstr "Your name"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Your Oo reads replies aloud when this is on."
|
||||
msgstr "Your Oo reads replies aloud when this is on."
|
||||
msgid "Your Oo talks with you naturally when this is on."
|
||||
msgstr "Your Oo talks with you naturally when this is on."
|
||||
|
||||
#: src/pages/Onboarding.tsx
|
||||
msgid "Your Oo will communicate in this language. You can change it later."
|
||||
msgstr "Your Oo will communicate in this language. You can change it later."
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Your profile"
|
||||
msgstr "Your profile"
|
||||
|
||||
#: src/pages/Welcome.tsx
|
||||
msgid "Your specialists. One calm place to talk."
|
||||
msgstr "Your specialists. One calm place to talk."
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom in"
|
||||
msgstr "Zoom in"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom out"
|
||||
msgstr "Zoom out"
|
||||
|
||||
@ -161,11 +161,6 @@ msgstr ""
|
||||
msgid "{0}'s browser appears here when the computer is running."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: active.name
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "{0}'s screen"
|
||||
msgstr "Pantalla de {0}"
|
||||
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
msgstr ""
|
||||
@ -182,10 +177,38 @@ msgstr ""
|
||||
msgid "{botName} clears the path…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is building"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is checking a problem"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
msgid "{botName} is on it…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is replying"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is researching"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is thinking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is waiting for your answer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is working"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
#: src/pages/CallView.tsx
|
||||
msgid "{botName} is working…"
|
||||
@ -364,6 +387,7 @@ msgstr "Quedan {remaining} · el bot observa, no actúa"
|
||||
msgid "{size} B"
|
||||
msgstr "{size} B"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||
msgstr ""
|
||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{workingBotName} is working"
|
||||
msgstr "{workingBotName} está trabajando"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "/Users/you/projects/acme"
|
||||
@ -544,11 +564,16 @@ msgstr ""
|
||||
msgid "Add as projects"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationSettingsPanel.tsx
|
||||
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Add Executor"
|
||||
msgstr "Agregar Executor"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Add goal"
|
||||
msgstr ""
|
||||
|
||||
@ -737,6 +762,14 @@ msgstr "Siempre permitido"
|
||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Analyze the project and give me a brief overview."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Analyzing project…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
msgid "Answer"
|
||||
msgstr "Responder"
|
||||
@ -996,7 +1029,7 @@ msgstr ""
|
||||
msgid "Booting live desktop…"
|
||||
msgstr "Iniciando escritorio en vivo…"
|
||||
|
||||
#. placeholder {0}: active?.name
|
||||
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Booting up {0}’s computer"
|
||||
msgstr "Iniciando la computadora de {0}"
|
||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
||||
msgid "Bot screen"
|
||||
msgstr "Pantalla del bot"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Bot screen preview"
|
||||
msgstr "Vista previa de la pantalla del bot"
|
||||
|
||||
#: src/pages/MessagingSettingsOverlay.tsx
|
||||
msgid "Bot to link"
|
||||
msgstr "Bot a vincular"
|
||||
@ -1031,10 +1060,6 @@ msgstr "Bot a vincular"
|
||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
msgstr "Los bots actúan sin preguntar de forma predeterminada. Agrega una excepción solo cuando quieras revisar primero un tipo de acción."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Bots are working"
|
||||
msgstr "Los bots están trabajando"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Bow"
|
||||
msgstr ""
|
||||
@ -1064,7 +1089,6 @@ msgstr ""
|
||||
msgid "Browse MCP servers"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Browser"
|
||||
msgstr ""
|
||||
@ -1493,6 +1517,10 @@ msgstr "Computadoras"
|
||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
msgstr "Las computadoras están apagadas. Configura SANDBOX_PROVIDER=docker con SANDBOX_SUPERVISOR_TOKEN, o establece SANDBOX_PROVIDER en e2b, daytona o box con su clave de API. Recrea el stack después de cambiar .env."
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Configure a plugin catalog on the server to connect apps."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Configured by deployment"
|
||||
msgstr "Configurado por el despliegue"
|
||||
@ -1584,6 +1612,10 @@ msgstr "Conectado y usando {0}."
|
||||
msgid "Connected app"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Connected tools"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Connected. Its tools are available from your next message."
|
||||
msgstr "Conectado: sus herramientas estarán disponibles desde tu próximo mensaje."
|
||||
@ -2322,6 +2354,7 @@ msgstr "Desconectando…"
|
||||
#: src/components/ComputerUpdateProgress.tsx
|
||||
#: src/components/teach/TeachComputerOverlay.tsx
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Dismiss"
|
||||
msgstr "Descartar"
|
||||
@ -2381,7 +2414,9 @@ msgstr ""
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Done"
|
||||
msgstr "Listo"
|
||||
|
||||
@ -2394,8 +2429,11 @@ msgstr ""
|
||||
msgid "Dot"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: activeItem.name
|
||||
#. placeholder {0}: item.name
|
||||
#. placeholder {0}: props.name
|
||||
#: src/components/ArtifactFileCard.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {0}"
|
||||
msgstr "Descargar {0}"
|
||||
|
||||
@ -2403,7 +2441,6 @@ msgstr "Descargar {0}"
|
||||
#: src/components/ArtifactPane.tsx
|
||||
#: src/components/media/MediaCard.tsx
|
||||
#: src/components/media/MediaPlayer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {name}"
|
||||
msgstr "Descargar {name}"
|
||||
|
||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
||||
msgstr "Expandir"
|
||||
|
||||
#. placeholder {0}: group.title
|
||||
#. placeholder {0}: item.name
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/sidebar-group-header.tsx
|
||||
msgid "Expand {0}"
|
||||
msgstr "Expandir {0}"
|
||||
@ -2660,6 +2699,10 @@ msgstr ""
|
||||
msgid "Failed to open file"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to remove message"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to send message"
|
||||
msgstr "No se pudo enviar el mensaje"
|
||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgstr "Hola, así sonaré cuando lea las respuestas en voz alta."
|
||||
msgid "Hi, this is how I'll sound when we talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/sidebar-header.tsx
|
||||
msgid "Hide bots"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Hide details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Hide password"
|
||||
msgstr "Ocultar contraseña"
|
||||
@ -3080,6 +3128,7 @@ msgstr ""
|
||||
msgid "Import OpenAPI JSON"
|
||||
msgstr "Importar JSON de OpenAPI"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "In Bearbeitung"
|
||||
msgstr ""
|
||||
@ -3166,6 +3215,10 @@ msgstr "Aislado"
|
||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
msgstr "Su conversación, archivos y rutinas se eliminarán de forma permanente. Los bots que creó permanecerán en tu lista."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Jump to latest"
|
||||
msgstr "Ir al más reciente"
|
||||
@ -3174,6 +3227,10 @@ msgstr "Ir al más reciente"
|
||||
msgid "Jump to replied message"
|
||||
msgstr "Ir al mensaje respondido"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Just ask in the chat whenever something should happen regularly."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
msgid "just now"
|
||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
||||
msgstr "Nuevo space"
|
||||
|
||||
#: src/components/CookingMode.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
@ -3885,6 +3943,10 @@ msgstr ""
|
||||
msgid "No providers found."
|
||||
msgstr "No se encontraron proveedores."
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
msgid "No recent activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/integrations/IntegrationSetup.tsx
|
||||
msgid "No remote MCP servers found"
|
||||
msgstr ""
|
||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
||||
msgstr "Sin resultados"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "No runs yet"
|
||||
msgstr "Aún no hay ejecuciones"
|
||||
|
||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
||||
msgstr "Notificaciones"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Now"
|
||||
msgstr "Ahora"
|
||||
|
||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ScratchpadSection.tsx
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Open"
|
||||
msgstr "Abrir"
|
||||
|
||||
@ -4056,7 +4119,7 @@ msgstr ""
|
||||
msgid "Open in browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
#: src/components/browser/OoComputerBrowser.tsx
|
||||
msgid "Open in full window"
|
||||
msgstr "Abrir en ventana completa"
|
||||
|
||||
@ -4288,10 +4351,6 @@ msgstr ""
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Project · sessions & folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "Project folder"
|
||||
@ -4399,6 +4458,10 @@ msgstr ""
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Reasoning effort"
|
||||
msgstr "Esfuerzo de razonamiento"
|
||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Recent"
|
||||
msgstr "Recientes"
|
||||
|
||||
@ -4528,6 +4592,10 @@ msgstr "Quitar {0}"
|
||||
msgid "Remove folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Remove from queue"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
msgid "Remove Git event"
|
||||
msgstr ""
|
||||
@ -4653,6 +4721,11 @@ msgstr "Restaura el último espacio de trabajo guardado. Se pierde el trabajo si
|
||||
msgid "Restoring your workspace"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FolderPickerDialog.tsx
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
@ -4700,11 +4773,13 @@ msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/routine-event-line.tsx
|
||||
msgid "Routine"
|
||||
msgstr "Rutina"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Routines"
|
||||
msgstr "Rutinas"
|
||||
|
||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
||||
msgstr "Ejecutar a las"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Run history"
|
||||
msgstr "Historial de ejecuciones"
|
||||
|
||||
@ -4756,10 +4832,6 @@ msgstr "Ejecutando…"
|
||||
msgid "Runs when this bot receives a verified message from this provider."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Sandbox"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/components/MediaSettings.tsx
|
||||
#: src/components/teach/SkillDraftCard.tsx
|
||||
@ -4909,6 +4981,10 @@ msgstr "Enviar respuesta"
|
||||
msgid "Send it"
|
||||
msgstr "Enviarlo"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Send now"
|
||||
msgstr "Enviar ahora"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Send reset link"
|
||||
msgstr "Enviar enlace de restablecimiento"
|
||||
@ -5021,6 +5097,10 @@ msgstr ""
|
||||
msgid "Show computer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Show details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Show less"
|
||||
msgstr ""
|
||||
@ -5037,6 +5117,7 @@ msgstr "Mostrar más"
|
||||
msgid "Show password"
|
||||
msgstr "Mostrar contraseña"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Show settings"
|
||||
msgstr "Mostrar configuración"
|
||||
@ -5172,6 +5253,7 @@ msgstr ""
|
||||
msgid "Speaking with intent…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
#: src/lib/onboarding-copy.ts
|
||||
msgid "Specialist crew"
|
||||
msgstr ""
|
||||
@ -5286,6 +5368,10 @@ msgstr "Detener enseñanza"
|
||||
msgid "Stop timer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Stopped"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/lib/browser-notifications.ts
|
||||
msgid "Stopped."
|
||||
@ -5398,6 +5484,7 @@ msgstr "Enseñanza en curso: detén la enseñanza antes de enviar un nuevo mensa
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Team"
|
||||
msgstr "Equipo"
|
||||
|
||||
@ -5512,10 +5599,6 @@ msgstr "Este inicio de sesión por suscripción aún no está disponible en Raka
|
||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Threads by topic · folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelTierSettings.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
msgid "Tiers"
|
||||
@ -5701,6 +5784,7 @@ msgstr ""
|
||||
|
||||
#: src/components/DesktopUpdates.tsx
|
||||
#: src/components/SoftwareUpdateSection.tsx
|
||||
#: src/pages/AccountSettingsOverlay.tsx
|
||||
msgid "Up to date"
|
||||
msgstr "Actualizado"
|
||||
|
||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/OoStage.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Working"
|
||||
msgstr ""
|
||||
|
||||
@ -6037,6 +6122,7 @@ msgstr "Ayer"
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
#: src/components/voice/VoiceStage.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "You"
|
||||
msgstr "Tú"
|
||||
|
||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
||||
msgstr "Tu nombre"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Your Oo reads replies aloud when this is on."
|
||||
msgid "Your Oo talks with you naturally when this is on."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Onboarding.tsx
|
||||
msgid "Your Oo will communicate in this language. You can change it later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Your profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Welcome.tsx
|
||||
msgid "Your specialists. One calm place to talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom in"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom out"
|
||||
msgstr ""
|
||||
|
||||
@ -161,11 +161,6 @@ msgstr ""
|
||||
msgid "{0}'s browser appears here when the computer is running."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: active.name
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "{0}'s screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
msgstr ""
|
||||
@ -182,10 +177,38 @@ msgstr ""
|
||||
msgid "{botName} clears the path…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is building"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is checking a problem"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
msgid "{botName} is on it…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is replying"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is researching"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is thinking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is waiting for your answer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is working"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
#: src/pages/CallView.tsx
|
||||
msgid "{botName} is working…"
|
||||
@ -364,6 +387,7 @@ msgstr "{remaining} शेष · बॉट देख रहा है, कार
|
||||
msgid "{size} B"
|
||||
msgstr "{size} B"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||
msgstr ""
|
||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{workingBotName} is working"
|
||||
msgstr "{workingBotName} काम कर रहा है"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "/Users/you/projects/acme"
|
||||
@ -544,11 +564,16 @@ msgstr ""
|
||||
msgid "Add as projects"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationSettingsPanel.tsx
|
||||
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Add Executor"
|
||||
msgstr "Executor जोड़ें"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Add goal"
|
||||
msgstr ""
|
||||
|
||||
@ -737,6 +762,14 @@ msgstr "हमेशा अनुमत"
|
||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Analyze the project and give me a brief overview."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Analyzing project…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
msgid "Answer"
|
||||
msgstr "उत्तर"
|
||||
@ -996,7 +1029,7 @@ msgstr ""
|
||||
msgid "Booting live desktop…"
|
||||
msgstr "लाइव डेस्कटॉप शुरू हो रहा है…"
|
||||
|
||||
#. placeholder {0}: active?.name
|
||||
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Booting up {0}’s computer"
|
||||
msgstr "{0} का कंप्यूटर शुरू हो रहा है"
|
||||
@ -1019,10 +1052,6 @@ msgstr "बॉट"
|
||||
msgid "Bot screen"
|
||||
msgstr "बॉट स्क्रीन"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Bot screen preview"
|
||||
msgstr "बॉट स्क्रीन प्रीव्यू"
|
||||
|
||||
#: src/pages/MessagingSettingsOverlay.tsx
|
||||
msgid "Bot to link"
|
||||
msgstr ""
|
||||
@ -1031,10 +1060,6 @@ msgstr ""
|
||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
msgstr "बॉट डिफ़ॉल्ट रूप से बिना पूछे कार्रवाई करते हैं। अपवाद तभी जोड़ें जब आप किसी प्रकार की कार्रवाई की पहले समीक्षा करना चाहते हों।"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Bots are working"
|
||||
msgstr "बॉट काम कर रहे हैं"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Bow"
|
||||
msgstr ""
|
||||
@ -1064,7 +1089,6 @@ msgstr ""
|
||||
msgid "Browse MCP servers"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Browser"
|
||||
msgstr ""
|
||||
@ -1493,6 +1517,10 @@ msgstr "कंप्यूटर"
|
||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
msgstr "कंप्यूटर बंद हैं। SANDBOX_PROVIDER=docker और SANDBOX_SUPERVISOR_TOKEN सेट करें, या SANDBOX_PROVIDER को e2b, daytona, या box पर सेट कर उसका API key दें। .env बदलने के बाद स्टैक फिर बनाएँ।"
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Configure a plugin catalog on the server to connect apps."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Configured by deployment"
|
||||
msgstr "डिप्लॉयमेंट द्वारा कॉन्फ़िगर"
|
||||
@ -1584,6 +1612,10 @@ msgstr "{0} कनेक्ट किया गया और उपयोग म
|
||||
msgid "Connected app"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Connected tools"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Connected. Its tools are available from your next message."
|
||||
msgstr "कनेक्टेड। इसके टूल आपके अगले संदेश से उपलब्ध हैं।"
|
||||
@ -2322,6 +2354,7 @@ msgstr "डिस्कनेक्ट हो रहा है…"
|
||||
#: src/components/ComputerUpdateProgress.tsx
|
||||
#: src/components/teach/TeachComputerOverlay.tsx
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Dismiss"
|
||||
msgstr ""
|
||||
@ -2381,7 +2414,9 @@ msgstr ""
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Done"
|
||||
msgstr "पूर्ण"
|
||||
|
||||
@ -2394,8 +2429,11 @@ msgstr ""
|
||||
msgid "Dot"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: activeItem.name
|
||||
#. placeholder {0}: item.name
|
||||
#. placeholder {0}: props.name
|
||||
#: src/components/ArtifactFileCard.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {0}"
|
||||
msgstr "{0} डाउनलोड करें"
|
||||
|
||||
@ -2403,7 +2441,6 @@ msgstr "{0} डाउनलोड करें"
|
||||
#: src/components/ArtifactPane.tsx
|
||||
#: src/components/media/MediaCard.tsx
|
||||
#: src/components/media/MediaPlayer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {name}"
|
||||
msgstr "{name} डाउनलोड करें"
|
||||
|
||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
||||
msgstr "विस्तृत करें"
|
||||
|
||||
#. placeholder {0}: group.title
|
||||
#. placeholder {0}: item.name
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/sidebar-group-header.tsx
|
||||
msgid "Expand {0}"
|
||||
msgstr ""
|
||||
@ -2660,6 +2699,10 @@ msgstr ""
|
||||
msgid "Failed to open file"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to remove message"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to send message"
|
||||
msgstr "संदेश भेजने में विफल"
|
||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgstr "नमस्ते, उत्तर पढ़कर सुनाते समय मेरी आवाज़ ऐसी होगी।"
|
||||
msgid "Hi, this is how I'll sound when we talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/sidebar-header.tsx
|
||||
msgid "Hide bots"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Hide details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Hide password"
|
||||
msgstr ""
|
||||
@ -3080,6 +3128,7 @@ msgstr ""
|
||||
msgid "Import OpenAPI JSON"
|
||||
msgstr "OpenAPI JSON इम्पोर्ट करें"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "In Bearbeitung"
|
||||
msgstr ""
|
||||
@ -3166,6 +3215,10 @@ msgstr "पृथक"
|
||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
msgstr "इसकी बातचीत, फ़ाइलें और रूटीन स्थायी रूप से डिलीट कर दिए जाएंगे। इसके बनाए बॉट आपकी सूची में बने रहेंगे।"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Jump to latest"
|
||||
msgstr "नवीनतम संदेश पर जाएँ"
|
||||
@ -3174,6 +3227,10 @@ msgstr "नवीनतम संदेश पर जाएँ"
|
||||
msgid "Jump to replied message"
|
||||
msgstr "जिस संदेश का उत्तर दिया गया उस पर जाएं"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Just ask in the chat whenever something should happen regularly."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
msgid "just now"
|
||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/CookingMode.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
@ -3885,6 +3943,10 @@ msgstr ""
|
||||
msgid "No providers found."
|
||||
msgstr "कोई प्रोवाइडर नहीं मिला।"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
msgid "No recent activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/integrations/IntegrationSetup.tsx
|
||||
msgid "No remote MCP servers found"
|
||||
msgstr ""
|
||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
||||
msgstr "कोई परिणाम नहीं"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "No runs yet"
|
||||
msgstr ""
|
||||
|
||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
||||
msgstr "सूचनाएं"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Now"
|
||||
msgstr "अभी"
|
||||
|
||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ScratchpadSection.tsx
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Open"
|
||||
msgstr "खोलें"
|
||||
|
||||
@ -4056,7 +4119,7 @@ msgstr ""
|
||||
msgid "Open in browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
#: src/components/browser/OoComputerBrowser.tsx
|
||||
msgid "Open in full window"
|
||||
msgstr "पूरी विंडो में खोलें"
|
||||
|
||||
@ -4288,10 +4351,6 @@ msgstr ""
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Project · sessions & folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "Project folder"
|
||||
@ -4399,6 +4458,10 @@ msgstr ""
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Reasoning effort"
|
||||
msgstr "रीज़निंग प्रयास"
|
||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Recent"
|
||||
msgstr "हाल के"
|
||||
|
||||
@ -4528,6 +4592,10 @@ msgstr "{0} हटाएं"
|
||||
msgid "Remove folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Remove from queue"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
msgid "Remove Git event"
|
||||
msgstr ""
|
||||
@ -4653,6 +4721,11 @@ msgstr "अंतिम सहेजे गए वर्कस्पेस क
|
||||
msgid "Restoring your workspace"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FolderPickerDialog.tsx
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
@ -4700,11 +4773,13 @@ msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/routine-event-line.tsx
|
||||
msgid "Routine"
|
||||
msgstr "रूटीन"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Routines"
|
||||
msgstr "रूटीन्स"
|
||||
|
||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Run history"
|
||||
msgstr ""
|
||||
|
||||
@ -4756,10 +4832,6 @@ msgstr "चल रहा है…"
|
||||
msgid "Runs when this bot receives a verified message from this provider."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Sandbox"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/components/MediaSettings.tsx
|
||||
#: src/components/teach/SkillDraftCard.tsx
|
||||
@ -4909,6 +4981,10 @@ msgstr "उत्तर भेजें"
|
||||
msgid "Send it"
|
||||
msgstr "इसे भेजें"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Send now"
|
||||
msgstr "अभी भेजें"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Send reset link"
|
||||
msgstr "रीसेट लिंक भेजें"
|
||||
@ -5021,6 +5097,10 @@ msgstr ""
|
||||
msgid "Show computer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Show details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Show less"
|
||||
msgstr ""
|
||||
@ -5037,6 +5117,7 @@ msgstr "और दिखाएँ"
|
||||
msgid "Show password"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Show settings"
|
||||
msgstr "सेटिंग्स दिखाएं"
|
||||
@ -5172,6 +5253,7 @@ msgstr ""
|
||||
msgid "Speaking with intent…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
#: src/lib/onboarding-copy.ts
|
||||
msgid "Specialist crew"
|
||||
msgstr ""
|
||||
@ -5286,6 +5368,10 @@ msgstr "सिखाना रोकें"
|
||||
msgid "Stop timer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Stopped"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/lib/browser-notifications.ts
|
||||
msgid "Stopped."
|
||||
@ -5398,6 +5484,7 @@ msgstr "सिखाना जारी है। नया संदेश भ
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Team"
|
||||
msgstr "टीम"
|
||||
|
||||
@ -5512,10 +5599,6 @@ msgstr "यह सब्सक्रिप्शन साइन-इन अभ
|
||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Threads by topic · folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelTierSettings.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
msgid "Tiers"
|
||||
@ -5701,6 +5784,7 @@ msgstr ""
|
||||
|
||||
#: src/components/DesktopUpdates.tsx
|
||||
#: src/components/SoftwareUpdateSection.tsx
|
||||
#: src/pages/AccountSettingsOverlay.tsx
|
||||
msgid "Up to date"
|
||||
msgstr ""
|
||||
|
||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/OoStage.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Working"
|
||||
msgstr ""
|
||||
|
||||
@ -6037,6 +6122,7 @@ msgstr "कल"
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
#: src/components/voice/VoiceStage.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "You"
|
||||
msgstr "आप"
|
||||
|
||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
||||
msgstr "आपका नाम"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Your Oo reads replies aloud when this is on."
|
||||
msgid "Your Oo talks with you naturally when this is on."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Onboarding.tsx
|
||||
msgid "Your Oo will communicate in this language. You can change it later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Your profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Welcome.tsx
|
||||
msgid "Your specialists. One calm place to talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom in"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom out"
|
||||
msgstr ""
|
||||
|
||||
@ -161,11 +161,6 @@ msgstr ""
|
||||
msgid "{0}'s browser appears here when the computer is running."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: active.name
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "{0}'s screen"
|
||||
msgstr "{0}의 화면"
|
||||
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
msgstr ""
|
||||
@ -182,10 +177,38 @@ msgstr ""
|
||||
msgid "{botName} clears the path…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is building"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is checking a problem"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
msgid "{botName} is on it…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is replying"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is researching"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is thinking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is waiting for your answer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is working"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
#: src/pages/CallView.tsx
|
||||
msgid "{botName} is working…"
|
||||
@ -364,6 +387,7 @@ msgstr "남은 시간 {remaining} · Bot은 화면만 보고 직접 조작하지
|
||||
msgid "{size} B"
|
||||
msgstr "{size} B"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||
msgstr ""
|
||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{workingBotName} is working"
|
||||
msgstr "{workingBotName} 작업 중"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "/Users/you/projects/acme"
|
||||
@ -544,11 +564,16 @@ msgstr ""
|
||||
msgid "Add as projects"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationSettingsPanel.tsx
|
||||
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Add Executor"
|
||||
msgstr "Executor 추가"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Add goal"
|
||||
msgstr ""
|
||||
|
||||
@ -737,6 +762,14 @@ msgstr "항상 허용됨"
|
||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Analyze the project and give me a brief overview."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Analyzing project…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
msgid "Answer"
|
||||
msgstr "답변"
|
||||
@ -996,7 +1029,7 @@ msgstr ""
|
||||
msgid "Booting live desktop…"
|
||||
msgstr "라이브 데스크톱 시작 중…"
|
||||
|
||||
#. placeholder {0}: active?.name
|
||||
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Booting up {0}’s computer"
|
||||
msgstr "{0}의 컴퓨터를 켜는 중"
|
||||
@ -1019,10 +1052,6 @@ msgstr "봇"
|
||||
msgid "Bot screen"
|
||||
msgstr "Bot 화면"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Bot screen preview"
|
||||
msgstr "Bot 화면 미리보기"
|
||||
|
||||
#: src/pages/MessagingSettingsOverlay.tsx
|
||||
msgid "Bot to link"
|
||||
msgstr "연결할 Bot"
|
||||
@ -1031,10 +1060,6 @@ msgstr "연결할 Bot"
|
||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
msgstr "Bot은 기본적으로 묻지 않고 작업합니다. 먼저 확인하고 싶은 작업만 예외로 추가하세요."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Bots are working"
|
||||
msgstr "봇 작업 중"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Bow"
|
||||
msgstr ""
|
||||
@ -1064,7 +1089,6 @@ msgstr ""
|
||||
msgid "Browse MCP servers"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Browser"
|
||||
msgstr ""
|
||||
@ -1493,6 +1517,10 @@ msgstr "컴퓨터"
|
||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
msgstr "컴퓨터가 꺼져 있습니다. SANDBOX_PROVIDER=docker와 SANDBOX_SUPERVISOR_TOKEN을 설정하거나, SANDBOX_PROVIDER를 e2b, daytona, box 중 하나로 두고 해당 API 키를 넣으세요. .env 변경 후 스택을 다시 만드세요."
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Configure a plugin catalog on the server to connect apps."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Configured by deployment"
|
||||
msgstr "서버에서 설정됨"
|
||||
@ -1584,6 +1612,10 @@ msgstr "{0}에 연결하고 기본 모델로 설정했습니다."
|
||||
msgid "Connected app"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Connected tools"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Connected. Its tools are available from your next message."
|
||||
msgstr "연결되었습니다. 다음 메시지부터 이 도구를 사용할 수 있습니다."
|
||||
@ -2322,6 +2354,7 @@ msgstr "연결 해제 중…"
|
||||
#: src/components/ComputerUpdateProgress.tsx
|
||||
#: src/components/teach/TeachComputerOverlay.tsx
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Dismiss"
|
||||
msgstr "닫기"
|
||||
@ -2381,7 +2414,9 @@ msgstr ""
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Done"
|
||||
msgstr "완료"
|
||||
|
||||
@ -2394,8 +2429,11 @@ msgstr ""
|
||||
msgid "Dot"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: activeItem.name
|
||||
#. placeholder {0}: item.name
|
||||
#. placeholder {0}: props.name
|
||||
#: src/components/ArtifactFileCard.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {0}"
|
||||
msgstr "{0} 다운로드"
|
||||
|
||||
@ -2403,7 +2441,6 @@ msgstr "{0} 다운로드"
|
||||
#: src/components/ArtifactPane.tsx
|
||||
#: src/components/media/MediaCard.tsx
|
||||
#: src/components/media/MediaPlayer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {name}"
|
||||
msgstr "{name} 다운로드"
|
||||
|
||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
||||
msgstr "펼치기"
|
||||
|
||||
#. placeholder {0}: group.title
|
||||
#. placeholder {0}: item.name
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/sidebar-group-header.tsx
|
||||
msgid "Expand {0}"
|
||||
msgstr "{0} 펼치기"
|
||||
@ -2660,6 +2699,10 @@ msgstr ""
|
||||
msgid "Failed to open file"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to remove message"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to send message"
|
||||
msgstr "메시지를 보내지 못했습니다."
|
||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgstr "안녕하세요. 답변을 소리 내어 읽을 때 이런 목소리로 들립니다."
|
||||
msgid "Hi, this is how I'll sound when we talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/sidebar-header.tsx
|
||||
msgid "Hide bots"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Hide details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Hide password"
|
||||
msgstr "비밀번호 숨기기"
|
||||
@ -3080,6 +3128,7 @@ msgstr ""
|
||||
msgid "Import OpenAPI JSON"
|
||||
msgstr "OpenAPI JSON 가져오기"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "In Bearbeitung"
|
||||
msgstr ""
|
||||
@ -3166,6 +3215,10 @@ msgstr "이 Bot만 사용"
|
||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
msgstr "대화, 파일, 자동 실행이 영구 삭제됩니다. 이 Bot이 만든 다른 Bot은 목록에 남습니다."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Jump to latest"
|
||||
msgstr "최신 메시지로 이동"
|
||||
@ -3174,6 +3227,10 @@ msgstr "최신 메시지로 이동"
|
||||
msgid "Jump to replied message"
|
||||
msgstr "답장한 메시지로 이동"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Just ask in the chat whenever something should happen regularly."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
msgid "just now"
|
||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
||||
msgstr "새 스페이스"
|
||||
|
||||
#: src/components/CookingMode.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
@ -3885,6 +3943,10 @@ msgstr ""
|
||||
msgid "No providers found."
|
||||
msgstr "검색된 AI 서비스가 없습니다."
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
msgid "No recent activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/integrations/IntegrationSetup.tsx
|
||||
msgid "No remote MCP servers found"
|
||||
msgstr ""
|
||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
||||
msgstr "검색 결과가 없습니다"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "No runs yet"
|
||||
msgstr "아직 실행 기록이 없습니다"
|
||||
|
||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
||||
msgstr "알림"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Now"
|
||||
msgstr "지금"
|
||||
|
||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ScratchpadSection.tsx
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Open"
|
||||
msgstr "열기"
|
||||
|
||||
@ -4056,7 +4119,7 @@ msgstr ""
|
||||
msgid "Open in browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
#: src/components/browser/OoComputerBrowser.tsx
|
||||
msgid "Open in full window"
|
||||
msgstr "전체 창으로 열기"
|
||||
|
||||
@ -4288,10 +4351,6 @@ msgstr ""
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Project · sessions & folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "Project folder"
|
||||
@ -4399,6 +4458,10 @@ msgstr ""
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Reasoning effort"
|
||||
msgstr "추론 강도"
|
||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Recent"
|
||||
msgstr "최근"
|
||||
|
||||
@ -4528,6 +4592,10 @@ msgstr "{0} 삭제"
|
||||
msgid "Remove folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Remove from queue"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
msgid "Remove Git event"
|
||||
msgstr ""
|
||||
@ -4653,6 +4721,11 @@ msgstr "마지막으로 저장된 작업 공간을 복원합니다. 컴퓨터에
|
||||
msgid "Restoring your workspace"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FolderPickerDialog.tsx
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
@ -4700,11 +4773,13 @@ msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/routine-event-line.tsx
|
||||
msgid "Routine"
|
||||
msgstr "자동 실행"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Routines"
|
||||
msgstr "자동 실행"
|
||||
|
||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
||||
msgstr "실행 시간"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Run history"
|
||||
msgstr "실행 기록"
|
||||
|
||||
@ -4756,10 +4832,6 @@ msgstr "실행 중…"
|
||||
msgid "Runs when this bot receives a verified message from this provider."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Sandbox"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/components/MediaSettings.tsx
|
||||
#: src/components/teach/SkillDraftCard.tsx
|
||||
@ -4909,6 +4981,10 @@ msgstr "답변 보내기"
|
||||
msgid "Send it"
|
||||
msgstr "보내기"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Send now"
|
||||
msgstr "지금 보내기"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Send reset link"
|
||||
msgstr "재설정 링크 보내기"
|
||||
@ -5021,6 +5097,10 @@ msgstr ""
|
||||
msgid "Show computer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Show details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Show less"
|
||||
msgstr ""
|
||||
@ -5037,6 +5117,7 @@ msgstr "더 보기"
|
||||
msgid "Show password"
|
||||
msgstr "비밀번호 표시"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Show settings"
|
||||
msgstr "설정 보기"
|
||||
@ -5172,6 +5253,7 @@ msgstr ""
|
||||
msgid "Speaking with intent…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
#: src/lib/onboarding-copy.ts
|
||||
msgid "Specialist crew"
|
||||
msgstr ""
|
||||
@ -5286,6 +5368,10 @@ msgstr "가르치기 종료"
|
||||
msgid "Stop timer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Stopped"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/lib/browser-notifications.ts
|
||||
msgid "Stopped."
|
||||
@ -5398,6 +5484,7 @@ msgstr "작업을 가르치는 중입니다. 새 메시지를 보내려면 먼
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Team"
|
||||
msgstr "팀 공용"
|
||||
|
||||
@ -5512,10 +5599,6 @@ msgstr "이 구독 로그인 방식은 아직 Rakazo에서 지원되지 않습
|
||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Threads by topic · folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelTierSettings.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
msgid "Tiers"
|
||||
@ -5701,6 +5784,7 @@ msgstr ""
|
||||
|
||||
#: src/components/DesktopUpdates.tsx
|
||||
#: src/components/SoftwareUpdateSection.tsx
|
||||
#: src/pages/AccountSettingsOverlay.tsx
|
||||
msgid "Up to date"
|
||||
msgstr "최신 상태"
|
||||
|
||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/OoStage.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Working"
|
||||
msgstr ""
|
||||
|
||||
@ -6037,6 +6122,7 @@ msgstr "어제"
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
#: src/components/voice/VoiceStage.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "You"
|
||||
msgstr "나"
|
||||
|
||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
||||
msgstr "이름"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Your Oo reads replies aloud when this is on."
|
||||
msgid "Your Oo talks with you naturally when this is on."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Onboarding.tsx
|
||||
msgid "Your Oo will communicate in this language. You can change it later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Your profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Welcome.tsx
|
||||
msgid "Your specialists. One calm place to talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom in"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom out"
|
||||
msgstr ""
|
||||
|
||||
@ -161,11 +161,6 @@ msgstr ""
|
||||
msgid "{0}'s browser appears here when the computer is running."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: active.name
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "{0}'s screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
msgstr ""
|
||||
@ -182,10 +177,38 @@ msgstr ""
|
||||
msgid "{botName} clears the path…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is building"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is checking a problem"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
msgid "{botName} is on it…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is replying"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is researching"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is thinking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is waiting for your answer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is working"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
#: src/pages/CallView.tsx
|
||||
msgid "{botName} is working…"
|
||||
@ -364,6 +387,7 @@ msgstr "{remaining} restantes · o bot está observando, não agindo"
|
||||
msgid "{size} B"
|
||||
msgstr "{size} B"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||
msgstr ""
|
||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{workingBotName} is working"
|
||||
msgstr "{workingBotName} está trabalhando"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "/Users/you/projects/acme"
|
||||
@ -544,11 +564,16 @@ msgstr ""
|
||||
msgid "Add as projects"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationSettingsPanel.tsx
|
||||
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Add Executor"
|
||||
msgstr "Adicionar Executor"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Add goal"
|
||||
msgstr ""
|
||||
|
||||
@ -737,6 +762,14 @@ msgstr "Sempre permitido"
|
||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Analyze the project and give me a brief overview."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Analyzing project…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
msgid "Answer"
|
||||
msgstr "Resposta"
|
||||
@ -996,7 +1029,7 @@ msgstr ""
|
||||
msgid "Booting live desktop…"
|
||||
msgstr "Iniciando a área de trabalho ao vivo..."
|
||||
|
||||
#. placeholder {0}: active?.name
|
||||
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Booting up {0}’s computer"
|
||||
msgstr "Inicializando o computador do {0}"
|
||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
||||
msgid "Bot screen"
|
||||
msgstr "Tela do bot"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Bot screen preview"
|
||||
msgstr "Pré-visualização da tela"
|
||||
|
||||
#: src/pages/MessagingSettingsOverlay.tsx
|
||||
msgid "Bot to link"
|
||||
msgstr ""
|
||||
@ -1031,10 +1060,6 @@ msgstr ""
|
||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
msgstr "Os bots agem sem perguntar por padrão. Adicione uma exceção apenas quando quiser analisar um tipo de ação primeiro."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Bots are working"
|
||||
msgstr "Bots estão trabalhando"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Bow"
|
||||
msgstr ""
|
||||
@ -1064,7 +1089,6 @@ msgstr ""
|
||||
msgid "Browse MCP servers"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Browser"
|
||||
msgstr ""
|
||||
@ -1493,6 +1517,10 @@ msgstr "Computadores"
|
||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
msgstr "Computadores estão desligados. Defina SANDBOX_PROVIDER=docker com SANDBOX_SUPERVISOR_TOKEN, ou SANDBOX_PROVIDER como e2b, daytona ou box com a chave de API. Recrie o stack após alterar o .env."
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Configure a plugin catalog on the server to connect apps."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Configured by deployment"
|
||||
msgstr "Configurado por implantação"
|
||||
@ -1584,6 +1612,10 @@ msgstr "Conectado e usando {0}."
|
||||
msgid "Connected app"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Connected tools"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Connected. Its tools are available from your next message."
|
||||
msgstr "Conectado. Suas ferramentas estão disponíveis na sua próxima mensagem."
|
||||
@ -2322,6 +2354,7 @@ msgstr "Desconectando…"
|
||||
#: src/components/ComputerUpdateProgress.tsx
|
||||
#: src/components/teach/TeachComputerOverlay.tsx
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Dismiss"
|
||||
msgstr ""
|
||||
@ -2381,7 +2414,9 @@ msgstr ""
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Done"
|
||||
msgstr "Concluído"
|
||||
|
||||
@ -2394,8 +2429,11 @@ msgstr ""
|
||||
msgid "Dot"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: activeItem.name
|
||||
#. placeholder {0}: item.name
|
||||
#. placeholder {0}: props.name
|
||||
#: src/components/ArtifactFileCard.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {0}"
|
||||
msgstr "Baixar {0}"
|
||||
|
||||
@ -2403,7 +2441,6 @@ msgstr "Baixar {0}"
|
||||
#: src/components/ArtifactPane.tsx
|
||||
#: src/components/media/MediaCard.tsx
|
||||
#: src/components/media/MediaPlayer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {name}"
|
||||
msgstr "Baixar {name}"
|
||||
|
||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
||||
msgstr "Expandir"
|
||||
|
||||
#. placeholder {0}: group.title
|
||||
#. placeholder {0}: item.name
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/sidebar-group-header.tsx
|
||||
msgid "Expand {0}"
|
||||
msgstr ""
|
||||
@ -2660,6 +2699,10 @@ msgstr ""
|
||||
msgid "Failed to open file"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to remove message"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to send message"
|
||||
msgstr "Ocorreu um erro ao enviar a mensagem"
|
||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgstr "Olá, é assim que vou soar quando ler as respostas em voz alta."
|
||||
msgid "Hi, this is how I'll sound when we talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/sidebar-header.tsx
|
||||
msgid "Hide bots"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Hide details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Hide password"
|
||||
msgstr ""
|
||||
@ -3080,6 +3128,7 @@ msgstr ""
|
||||
msgid "Import OpenAPI JSON"
|
||||
msgstr "Importar OpenAPI JSON"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "In Bearbeitung"
|
||||
msgstr ""
|
||||
@ -3166,6 +3215,10 @@ msgstr "Isolado"
|
||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
msgstr "Suas conversas, arquivos e rotinas serão excluídos permanentemente. Os bots criados ficam na sua lista."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Jump to latest"
|
||||
msgstr "Ir para a mensagem mais recente"
|
||||
@ -3174,6 +3227,10 @@ msgstr "Ir para a mensagem mais recente"
|
||||
msgid "Jump to replied message"
|
||||
msgstr "Ir para a mensagem respondida"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Just ask in the chat whenever something should happen regularly."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
msgid "just now"
|
||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/CookingMode.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
@ -3885,6 +3943,10 @@ msgstr ""
|
||||
msgid "No providers found."
|
||||
msgstr "Nenhum provedor encontrado"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
msgid "No recent activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/integrations/IntegrationSetup.tsx
|
||||
msgid "No remote MCP servers found"
|
||||
msgstr ""
|
||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
||||
msgstr "Nenhum resultado"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "No runs yet"
|
||||
msgstr ""
|
||||
|
||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
||||
msgstr "Notificações"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Now"
|
||||
msgstr "Agora"
|
||||
|
||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ScratchpadSection.tsx
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Open"
|
||||
msgstr "Abrir"
|
||||
|
||||
@ -4056,7 +4119,7 @@ msgstr ""
|
||||
msgid "Open in browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
#: src/components/browser/OoComputerBrowser.tsx
|
||||
msgid "Open in full window"
|
||||
msgstr "Abrir em janela cheia"
|
||||
|
||||
@ -4288,10 +4351,6 @@ msgstr ""
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Project · sessions & folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "Project folder"
|
||||
@ -4399,6 +4458,10 @@ msgstr ""
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Reasoning effort"
|
||||
msgstr "Esforço de raciocínio"
|
||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Recent"
|
||||
msgstr "Recente"
|
||||
|
||||
@ -4528,6 +4592,10 @@ msgstr "Remover {0}"
|
||||
msgid "Remove folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Remove from queue"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
msgid "Remove Git event"
|
||||
msgstr ""
|
||||
@ -4653,6 +4721,11 @@ msgstr "Restaure o último espaço de trabalho salvo. O trabalho não salvo no c
|
||||
msgid "Restoring your workspace"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FolderPickerDialog.tsx
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
@ -4700,11 +4773,13 @@ msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/routine-event-line.tsx
|
||||
msgid "Routine"
|
||||
msgstr "Rotina"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Routines"
|
||||
msgstr "Rotinas"
|
||||
|
||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Run history"
|
||||
msgstr ""
|
||||
|
||||
@ -4756,10 +4832,6 @@ msgstr "Executando..."
|
||||
msgid "Runs when this bot receives a verified message from this provider."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Sandbox"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/components/MediaSettings.tsx
|
||||
#: src/components/teach/SkillDraftCard.tsx
|
||||
@ -4909,6 +4981,10 @@ msgstr "Enviar resposta"
|
||||
msgid "Send it"
|
||||
msgstr "Enviar"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Send now"
|
||||
msgstr "Enviar agora"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Send reset link"
|
||||
msgstr "Enviar link de redefinição"
|
||||
@ -5021,6 +5097,10 @@ msgstr ""
|
||||
msgid "Show computer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Show details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Show less"
|
||||
msgstr ""
|
||||
@ -5037,6 +5117,7 @@ msgstr "Mostrar mais"
|
||||
msgid "Show password"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Show settings"
|
||||
msgstr "Mostrar configurações"
|
||||
@ -5172,6 +5253,7 @@ msgstr ""
|
||||
msgid "Speaking with intent…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
#: src/lib/onboarding-copy.ts
|
||||
msgid "Specialist crew"
|
||||
msgstr ""
|
||||
@ -5286,6 +5368,10 @@ msgstr "Parar de ensinar"
|
||||
msgid "Stop timer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Stopped"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/lib/browser-notifications.ts
|
||||
msgid "Stopped."
|
||||
@ -5398,6 +5484,7 @@ msgstr "Ensino em andamento. Pare de ensinar antes de enviar uma nova mensagem."
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Team"
|
||||
msgstr "Equipe"
|
||||
|
||||
@ -5512,10 +5599,6 @@ msgstr "Este login de assinatura ainda não está disponível no Rakazo. Use uma
|
||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Threads by topic · folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelTierSettings.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
msgid "Tiers"
|
||||
@ -5701,6 +5784,7 @@ msgstr ""
|
||||
|
||||
#: src/components/DesktopUpdates.tsx
|
||||
#: src/components/SoftwareUpdateSection.tsx
|
||||
#: src/pages/AccountSettingsOverlay.tsx
|
||||
msgid "Up to date"
|
||||
msgstr ""
|
||||
|
||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/OoStage.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Working"
|
||||
msgstr ""
|
||||
|
||||
@ -6037,6 +6122,7 @@ msgstr "Ontem"
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
#: src/components/voice/VoiceStage.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "You"
|
||||
msgstr "Você"
|
||||
|
||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
||||
msgstr "O seu nome"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Your Oo reads replies aloud when this is on."
|
||||
msgid "Your Oo talks with you naturally when this is on."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Onboarding.tsx
|
||||
msgid "Your Oo will communicate in this language. You can change it later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Your profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Welcome.tsx
|
||||
msgid "Your specialists. One calm place to talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom in"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom out"
|
||||
msgstr ""
|
||||
|
||||
@ -161,11 +161,6 @@ msgstr ""
|
||||
msgid "{0}'s browser appears here when the computer is running."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: active.name
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "{0}'s screen"
|
||||
msgstr "Экран {0}"
|
||||
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
msgstr ""
|
||||
@ -182,10 +177,38 @@ msgstr ""
|
||||
msgid "{botName} clears the path…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is building"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is checking a problem"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
msgid "{botName} is on it…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is replying"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is researching"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is thinking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is waiting for your answer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is working"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
#: src/pages/CallView.tsx
|
||||
msgid "{botName} is working…"
|
||||
@ -364,6 +387,7 @@ msgstr "Осталось: {remaining} · бот наблюдает, но не д
|
||||
msgid "{size} B"
|
||||
msgstr "{size} Б"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||
msgstr ""
|
||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
msgstr "{toolCount, plural, one {# инструмент} few {# инструмента} many {# инструментов} other {# инструмента}}"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{workingBotName} is working"
|
||||
msgstr "{workingBotName} работает"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "/Users/you/projects/acme"
|
||||
@ -544,11 +564,16 @@ msgstr "Добавить еще"
|
||||
msgid "Add as projects"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationSettingsPanel.tsx
|
||||
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Add Executor"
|
||||
msgstr "Добавить Executor"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Add goal"
|
||||
msgstr ""
|
||||
|
||||
@ -737,6 +762,14 @@ msgstr "Всегда разрешено"
|
||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Analyze the project and give me a brief overview."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Analyzing project…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
msgid "Answer"
|
||||
msgstr "Ответ"
|
||||
@ -996,7 +1029,7 @@ msgstr ""
|
||||
msgid "Booting live desktop…"
|
||||
msgstr "Загрузка рабочего стола…"
|
||||
|
||||
#. placeholder {0}: active?.name
|
||||
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Booting up {0}’s computer"
|
||||
msgstr "Загрузка компьютера {0}"
|
||||
@ -1019,10 +1052,6 @@ msgstr "Бот"
|
||||
msgid "Bot screen"
|
||||
msgstr "Экран бота"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Bot screen preview"
|
||||
msgstr "Предварительный просмотр экрана бота"
|
||||
|
||||
#: src/pages/MessagingSettingsOverlay.tsx
|
||||
msgid "Bot to link"
|
||||
msgstr "Бот для ссылки"
|
||||
@ -1031,10 +1060,6 @@ msgstr "Бот для ссылки"
|
||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
msgstr "По умолчанию боты действуют, не спрашивая. Добавляйте исключение только в том случае, если вы хотите сначала просмотреть тип действия."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Bots are working"
|
||||
msgstr "Боты работают"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Bow"
|
||||
msgstr ""
|
||||
@ -1064,7 +1089,6 @@ msgstr ""
|
||||
msgid "Browse MCP servers"
|
||||
msgstr "Просмотреть MCP-серверы"
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Browser"
|
||||
msgstr ""
|
||||
@ -1493,6 +1517,10 @@ msgstr "Компьютеры"
|
||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
msgstr "Компьютеры выключены. Установите SANDBOX_PROVIDER=docker с помощью SANDBOX_SUPERVISOR_TOKEN или установите для SANDBOX_PROVIDER значение e2b, Daytona или Box с его ключом API. Воссоздайте стек после изменения .env."
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Configure a plugin catalog on the server to connect apps."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Configured by deployment"
|
||||
msgstr "Настраивается путем развертывания"
|
||||
@ -1584,6 +1612,10 @@ msgstr "Подключено и используется {0}."
|
||||
msgid "Connected app"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Connected tools"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Connected. Its tools are available from your next message."
|
||||
msgstr "Подключено. Его инструменты доступны из вашего следующего сообщения."
|
||||
@ -2322,6 +2354,7 @@ msgstr "Отключение…"
|
||||
#: src/components/ComputerUpdateProgress.tsx
|
||||
#: src/components/teach/TeachComputerOverlay.tsx
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Dismiss"
|
||||
msgstr "Закрыть"
|
||||
@ -2381,7 +2414,9 @@ msgstr ""
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Done"
|
||||
msgstr "Готово"
|
||||
|
||||
@ -2394,8 +2429,11 @@ msgstr ""
|
||||
msgid "Dot"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: activeItem.name
|
||||
#. placeholder {0}: item.name
|
||||
#. placeholder {0}: props.name
|
||||
#: src/components/ArtifactFileCard.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {0}"
|
||||
msgstr "Скачать {0}"
|
||||
|
||||
@ -2403,7 +2441,6 @@ msgstr "Скачать {0}"
|
||||
#: src/components/ArtifactPane.tsx
|
||||
#: src/components/media/MediaCard.tsx
|
||||
#: src/components/media/MediaPlayer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {name}"
|
||||
msgstr "Скачать {name}"
|
||||
|
||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
||||
msgstr "Развернуть"
|
||||
|
||||
#. placeholder {0}: group.title
|
||||
#. placeholder {0}: item.name
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/sidebar-group-header.tsx
|
||||
msgid "Expand {0}"
|
||||
msgstr "Развернуть {0}"
|
||||
@ -2660,6 +2699,10 @@ msgstr ""
|
||||
msgid "Failed to open file"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to remove message"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to send message"
|
||||
msgstr "Не удалось отправить сообщение"
|
||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgstr "Привет, вот как я буду звучать, когда буду читать ответы вслух."
|
||||
msgid "Hi, this is how I'll sound when we talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/sidebar-header.tsx
|
||||
msgid "Hide bots"
|
||||
msgstr "Скрыть ботов"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Hide details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Hide password"
|
||||
msgstr "Скрыть пароль"
|
||||
@ -3080,6 +3128,7 @@ msgstr ""
|
||||
msgid "Import OpenAPI JSON"
|
||||
msgstr "Импортировать OpenAPI JSON"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "In Bearbeitung"
|
||||
msgstr ""
|
||||
@ -3166,6 +3215,10 @@ msgstr "изолированный"
|
||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
msgstr "Его диалог, файлы и задачи будут безвозвратно удалены. Созданные им боты останутся в вашем списке."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Jump to latest"
|
||||
msgstr "Перейти к последнему"
|
||||
@ -3174,6 +3227,10 @@ msgstr "Перейти к последнему"
|
||||
msgid "Jump to replied message"
|
||||
msgstr "Перейти к ответному сообщению"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Just ask in the chat whenever something should happen regularly."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
msgid "just now"
|
||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
||||
msgstr "Новое пространство"
|
||||
|
||||
#: src/components/CookingMode.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
@ -3885,6 +3943,10 @@ msgstr ""
|
||||
msgid "No providers found."
|
||||
msgstr "Поставщики не найдены."
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
msgid "No recent activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/integrations/IntegrationSetup.tsx
|
||||
msgid "No remote MCP servers found"
|
||||
msgstr "Удаленные MCP-серверы не найдены"
|
||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
||||
msgstr "Нет результатов"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "No runs yet"
|
||||
msgstr "Запусков пока нет"
|
||||
|
||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
||||
msgstr "Уведомления"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Now"
|
||||
msgstr "Сейчас"
|
||||
|
||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ScratchpadSection.tsx
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Open"
|
||||
msgstr "Открыть"
|
||||
|
||||
@ -4056,7 +4119,7 @@ msgstr ""
|
||||
msgid "Open in browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
#: src/components/browser/OoComputerBrowser.tsx
|
||||
msgid "Open in full window"
|
||||
msgstr "Открыть в полном окне"
|
||||
|
||||
@ -4288,10 +4351,6 @@ msgstr "Приватное пространство со своими ботам
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Project · sessions & folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "Project folder"
|
||||
@ -4399,6 +4458,10 @@ msgstr ""
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Reasoning effort"
|
||||
msgstr ""
|
||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Recent"
|
||||
msgstr "Недавние"
|
||||
|
||||
@ -4528,6 +4592,10 @@ msgstr "Удалить {0}"
|
||||
msgid "Remove folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Remove from queue"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
msgid "Remove Git event"
|
||||
msgstr "Удалить событие Git"
|
||||
@ -4653,6 +4721,11 @@ msgstr "Восстановите последнее сохраненное ра
|
||||
msgid "Restoring your workspace"
|
||||
msgstr "Восстановление вашего рабочего пространства"
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FolderPickerDialog.tsx
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
@ -4700,11 +4773,13 @@ msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/routine-event-line.tsx
|
||||
msgid "Routine"
|
||||
msgstr "Задача"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Routines"
|
||||
msgstr "Задачи"
|
||||
|
||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
||||
msgstr "Запустить в"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Run history"
|
||||
msgstr "История запуска"
|
||||
|
||||
@ -4756,10 +4832,6 @@ msgstr "Выполняется…"
|
||||
msgid "Runs when this bot receives a verified message from this provider."
|
||||
msgstr "Запускается, когда этот бот получает проверенное сообщение от этого провайдера."
|
||||
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Sandbox"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/components/MediaSettings.tsx
|
||||
#: src/components/teach/SkillDraftCard.tsx
|
||||
@ -4909,6 +4981,10 @@ msgstr "Отправить ответ"
|
||||
msgid "Send it"
|
||||
msgstr "Отправить"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Send now"
|
||||
msgstr "Отправить сейчас"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Send reset link"
|
||||
msgstr "Отправить ссылку для сброса"
|
||||
@ -5021,6 +5097,10 @@ msgstr "Показать ботов"
|
||||
msgid "Show computer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Show details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Show less"
|
||||
msgstr ""
|
||||
@ -5037,6 +5117,7 @@ msgstr "Показать ещё"
|
||||
msgid "Show password"
|
||||
msgstr "Показать пароль"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Show settings"
|
||||
msgstr "Показать настройки"
|
||||
@ -5172,6 +5253,7 @@ msgstr ""
|
||||
msgid "Speaking with intent…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
#: src/lib/onboarding-copy.ts
|
||||
msgid "Specialist crew"
|
||||
msgstr ""
|
||||
@ -5286,6 +5368,10 @@ msgstr "Остановить обучение"
|
||||
msgid "Stop timer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Stopped"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/lib/browser-notifications.ts
|
||||
msgid "Stopped."
|
||||
@ -5398,6 +5484,7 @@ msgstr "Обучение в процессе. Прекратите обучен
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Team"
|
||||
msgstr "Команда"
|
||||
|
||||
@ -5512,10 +5599,6 @@ msgstr "Вход по этой подписке пока недоступен в
|
||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Threads by topic · folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelTierSettings.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
msgid "Tiers"
|
||||
@ -5701,6 +5784,7 @@ msgstr ""
|
||||
|
||||
#: src/components/DesktopUpdates.tsx
|
||||
#: src/components/SoftwareUpdateSection.tsx
|
||||
#: src/pages/AccountSettingsOverlay.tsx
|
||||
msgid "Up to date"
|
||||
msgstr "Актуально"
|
||||
|
||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/OoStage.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Working"
|
||||
msgstr ""
|
||||
|
||||
@ -6037,6 +6122,7 @@ msgstr "Вчера"
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
#: src/components/voice/VoiceStage.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "You"
|
||||
msgstr "Вы"
|
||||
|
||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
||||
msgstr "Ваше имя"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Your Oo reads replies aloud when this is on."
|
||||
msgid "Your Oo talks with you naturally when this is on."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Onboarding.tsx
|
||||
msgid "Your Oo will communicate in this language. You can change it later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Your profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Welcome.tsx
|
||||
msgid "Your specialists. One calm place to talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom in"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom out"
|
||||
msgstr ""
|
||||
|
||||
@ -161,11 +161,6 @@ msgstr ""
|
||||
msgid "{0}'s browser appears here when the computer is running."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: active.name
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "{0}'s screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
msgstr ""
|
||||
@ -182,10 +177,38 @@ msgstr ""
|
||||
msgid "{botName} clears the path…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is building"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is checking a problem"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
msgid "{botName} is on it…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is replying"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is researching"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is thinking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is waiting for your answer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is working"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
#: src/pages/CallView.tsx
|
||||
msgid "{botName} is working…"
|
||||
@ -364,6 +387,7 @@ msgstr "{remaining} kaldı · bot yalnızca izliyor"
|
||||
msgid "{size} B"
|
||||
msgstr "{size} B"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||
msgstr ""
|
||||
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
|
||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{workingBotName} is working"
|
||||
msgstr "{workingBotName} çalışıyor"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "/Users/you/projects/acme"
|
||||
@ -544,11 +564,16 @@ msgstr ""
|
||||
msgid "Add as projects"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationSettingsPanel.tsx
|
||||
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Add Executor"
|
||||
msgstr "Executor ekle"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Add goal"
|
||||
msgstr ""
|
||||
|
||||
@ -737,6 +762,14 @@ msgstr "Her zaman izinli"
|
||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Analyze the project and give me a brief overview."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Analyzing project…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
msgid "Answer"
|
||||
msgstr "Yanıtla"
|
||||
@ -996,7 +1029,7 @@ msgstr ""
|
||||
msgid "Booting live desktop…"
|
||||
msgstr "Canlı masaüstü başlatılıyor…"
|
||||
|
||||
#. placeholder {0}: active?.name
|
||||
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Booting up {0}’s computer"
|
||||
msgstr "{0} adlı botun bilgisayarı başlatılıyor"
|
||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
||||
msgid "Bot screen"
|
||||
msgstr "Bot ekranı"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Bot screen preview"
|
||||
msgstr "Bot ekranı önizlemesi"
|
||||
|
||||
#: src/pages/MessagingSettingsOverlay.tsx
|
||||
msgid "Bot to link"
|
||||
msgstr ""
|
||||
@ -1031,10 +1060,6 @@ msgstr ""
|
||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
msgstr "Botlar varsayılan olarak sormadan hareket eder. Yalnızca belirli bir işlem türünü önce incelemek istediğinde istisna ekle."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Bots are working"
|
||||
msgstr "Botlar çalışıyor"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Bow"
|
||||
msgstr ""
|
||||
@ -1064,7 +1089,6 @@ msgstr ""
|
||||
msgid "Browse MCP servers"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Browser"
|
||||
msgstr ""
|
||||
@ -1493,6 +1517,10 @@ msgstr "Bilgisayarlar"
|
||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
msgstr "Bilgisayarlar kapalı. SANDBOX_PROVIDER=docker ve SANDBOX_SUPERVISOR_TOKEN ayarlayın; ya da SANDBOX_PROVIDER değerini e2b, daytona veya box yapıp API anahtarını ekleyin. .env değişince stacki yeniden oluşturun."
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Configure a plugin catalog on the server to connect apps."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Configured by deployment"
|
||||
msgstr "Dağıtım tarafından yapılandırıldı"
|
||||
@ -1584,6 +1612,10 @@ msgstr "Bağlandı, {0} kullanılıyor."
|
||||
msgid "Connected app"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Connected tools"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Connected. Its tools are available from your next message."
|
||||
msgstr "Bağlandı. Araçları bir sonraki mesajından itibaren kullanılabilir."
|
||||
@ -2322,6 +2354,7 @@ msgstr "Bağlantı kesiliyor…"
|
||||
#: src/components/ComputerUpdateProgress.tsx
|
||||
#: src/components/teach/TeachComputerOverlay.tsx
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Dismiss"
|
||||
msgstr ""
|
||||
@ -2381,7 +2414,9 @@ msgstr ""
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Done"
|
||||
msgstr "Bitti"
|
||||
|
||||
@ -2394,8 +2429,11 @@ msgstr ""
|
||||
msgid "Dot"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: activeItem.name
|
||||
#. placeholder {0}: item.name
|
||||
#. placeholder {0}: props.name
|
||||
#: src/components/ArtifactFileCard.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {0}"
|
||||
msgstr "{0} indir"
|
||||
|
||||
@ -2403,7 +2441,6 @@ msgstr "{0} indir"
|
||||
#: src/components/ArtifactPane.tsx
|
||||
#: src/components/media/MediaCard.tsx
|
||||
#: src/components/media/MediaPlayer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {name}"
|
||||
msgstr "{name} indir"
|
||||
|
||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
||||
msgstr "Genişlet"
|
||||
|
||||
#. placeholder {0}: group.title
|
||||
#. placeholder {0}: item.name
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/sidebar-group-header.tsx
|
||||
msgid "Expand {0}"
|
||||
msgstr ""
|
||||
@ -2660,6 +2699,10 @@ msgstr ""
|
||||
msgid "Failed to open file"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to remove message"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to send message"
|
||||
msgstr "Mesaj gönderilemedi"
|
||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgstr "Merhaba, yanıtları sesli okuduğumda böyle duyulacağım."
|
||||
msgid "Hi, this is how I'll sound when we talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/sidebar-header.tsx
|
||||
msgid "Hide bots"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Hide details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Hide password"
|
||||
msgstr ""
|
||||
@ -3080,6 +3128,7 @@ msgstr ""
|
||||
msgid "Import OpenAPI JSON"
|
||||
msgstr "OpenAPI JSON içe aktar"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "In Bearbeitung"
|
||||
msgstr ""
|
||||
@ -3166,6 +3215,10 @@ msgstr "Yalıtılmış"
|
||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
msgstr "Sohbeti, dosyaları ve rutinleri kalıcı olarak silinecek. Oluşturduğu botlar listende kalır."
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Jump to latest"
|
||||
msgstr "En son mesaja git"
|
||||
@ -3174,6 +3227,10 @@ msgstr "En son mesaja git"
|
||||
msgid "Jump to replied message"
|
||||
msgstr "Yanıtlanan mesaja git"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Just ask in the chat whenever something should happen regularly."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
msgid "just now"
|
||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/CookingMode.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
@ -3885,6 +3943,10 @@ msgstr ""
|
||||
msgid "No providers found."
|
||||
msgstr "Sağlayıcı bulunamadı."
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
msgid "No recent activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/integrations/IntegrationSetup.tsx
|
||||
msgid "No remote MCP servers found"
|
||||
msgstr ""
|
||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
||||
msgstr "Sonuç yok"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "No runs yet"
|
||||
msgstr ""
|
||||
|
||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
||||
msgstr "Bildirimler"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Now"
|
||||
msgstr "Şimdi"
|
||||
|
||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ScratchpadSection.tsx
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Open"
|
||||
msgstr "Aç"
|
||||
|
||||
@ -4056,7 +4119,7 @@ msgstr ""
|
||||
msgid "Open in browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
#: src/components/browser/OoComputerBrowser.tsx
|
||||
msgid "Open in full window"
|
||||
msgstr "Tam pencerede aç"
|
||||
|
||||
@ -4288,10 +4351,6 @@ msgstr ""
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Project · sessions & folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "Project folder"
|
||||
@ -4399,6 +4458,10 @@ msgstr ""
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Reasoning effort"
|
||||
msgstr "Akıl yürütme düzeyi"
|
||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Recent"
|
||||
msgstr "Son"
|
||||
|
||||
@ -4528,6 +4592,10 @@ msgstr "{0} kaldır"
|
||||
msgid "Remove folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Remove from queue"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
msgid "Remove Git event"
|
||||
msgstr ""
|
||||
@ -4653,6 +4721,11 @@ msgstr ""
|
||||
msgid "Restoring your workspace"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FolderPickerDialog.tsx
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
@ -4700,11 +4773,13 @@ msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/routine-event-line.tsx
|
||||
msgid "Routine"
|
||||
msgstr "Rutin"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Routines"
|
||||
msgstr "Rutinler"
|
||||
|
||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Run history"
|
||||
msgstr ""
|
||||
|
||||
@ -4756,10 +4832,6 @@ msgstr "Çalışıyor…"
|
||||
msgid "Runs when this bot receives a verified message from this provider."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Sandbox"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/components/MediaSettings.tsx
|
||||
#: src/components/teach/SkillDraftCard.tsx
|
||||
@ -4909,6 +4981,10 @@ msgstr "Yanıtı gönder"
|
||||
msgid "Send it"
|
||||
msgstr "Gönder"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Send now"
|
||||
msgstr "Şimdi gönder"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Send reset link"
|
||||
msgstr "Sıfırlama bağlantısı gönder"
|
||||
@ -5021,6 +5097,10 @@ msgstr ""
|
||||
msgid "Show computer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Show details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Show less"
|
||||
msgstr ""
|
||||
@ -5037,6 +5117,7 @@ msgstr "Daha fazla göster"
|
||||
msgid "Show password"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Show settings"
|
||||
msgstr "Ayarları göster"
|
||||
@ -5172,6 +5253,7 @@ msgstr ""
|
||||
msgid "Speaking with intent…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
#: src/lib/onboarding-copy.ts
|
||||
msgid "Specialist crew"
|
||||
msgstr ""
|
||||
@ -5286,6 +5368,10 @@ msgstr "Öğretmeyi durdur"
|
||||
msgid "Stop timer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Stopped"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/lib/browser-notifications.ts
|
||||
msgid "Stopped."
|
||||
@ -5398,6 +5484,7 @@ msgstr "Öğretme sürüyor. Yeni mesaj göndermeden önce öğretmeyi durdur."
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Team"
|
||||
msgstr "Takım"
|
||||
|
||||
@ -5512,10 +5599,6 @@ msgstr "Bu abonelik girişi Rakazo'da henüz kullanılamıyor. Bir dağıtım ki
|
||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Threads by topic · folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelTierSettings.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
msgid "Tiers"
|
||||
@ -5701,6 +5784,7 @@ msgstr ""
|
||||
|
||||
#: src/components/DesktopUpdates.tsx
|
||||
#: src/components/SoftwareUpdateSection.tsx
|
||||
#: src/pages/AccountSettingsOverlay.tsx
|
||||
msgid "Up to date"
|
||||
msgstr ""
|
||||
|
||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/OoStage.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Working"
|
||||
msgstr ""
|
||||
|
||||
@ -6037,6 +6122,7 @@ msgstr "Dün"
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
#: src/components/voice/VoiceStage.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "You"
|
||||
msgstr "Sen"
|
||||
|
||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
||||
msgstr "Adın"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Your Oo reads replies aloud when this is on."
|
||||
msgid "Your Oo talks with you naturally when this is on."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Onboarding.tsx
|
||||
msgid "Your Oo will communicate in this language. You can change it later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Your profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Welcome.tsx
|
||||
msgid "Your specialists. One calm place to talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom in"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom out"
|
||||
msgstr ""
|
||||
|
||||
@ -161,11 +161,6 @@ msgstr ""
|
||||
msgid "{0}'s browser appears here when the computer is running."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: active.name
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "{0}'s screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
|
||||
msgstr ""
|
||||
@ -182,10 +177,38 @@ msgstr ""
|
||||
msgid "{botName} clears the path…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is building"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is checking a problem"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
msgid "{botName} is on it…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is replying"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is researching"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is thinking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is waiting for your answer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{botName} is working"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/voice-persona-copy.ts
|
||||
#: src/pages/CallView.tsx
|
||||
msgid "{botName} is working…"
|
||||
@ -364,6 +387,7 @@ msgstr "剩余 {remaining} · Bot 只看不操作"
|
||||
msgid "{size} B"
|
||||
msgstr "{size} B"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "{stepCount, plural, one {# step} other {# steps}}"
|
||||
msgstr ""
|
||||
@ -386,10 +410,6 @@ msgstr "{title},{label}"
|
||||
msgid "{toolCount, plural, one {# tool} other {# tools}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "{workingBotName} is working"
|
||||
msgstr "{workingBotName} 正在工作"
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "/Users/you/projects/acme"
|
||||
@ -544,11 +564,16 @@ msgstr ""
|
||||
msgid "Add as projects"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationSettingsPanel.tsx
|
||||
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Add Executor"
|
||||
msgstr "添加 Executor"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Add goal"
|
||||
msgstr ""
|
||||
|
||||
@ -737,6 +762,14 @@ msgstr "始终允许"
|
||||
msgid "Analysiert die Aufgabe und führt Tools aus…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Analyze the project and give me a brief overview."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Analyzing project…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
msgid "Answer"
|
||||
msgstr "回答"
|
||||
@ -996,7 +1029,7 @@ msgstr ""
|
||||
msgid "Booting live desktop…"
|
||||
msgstr "正在启动实时桌面…"
|
||||
|
||||
#. placeholder {0}: active?.name
|
||||
#. placeholder {0}: (active ?? coordinatorBot)?.name
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Booting up {0}’s computer"
|
||||
msgstr "正在启动 {0} 的电脑"
|
||||
@ -1019,10 +1052,6 @@ msgstr "Bot"
|
||||
msgid "Bot screen"
|
||||
msgstr "Bot 屏幕"
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Bot screen preview"
|
||||
msgstr "Bot 屏幕预览"
|
||||
|
||||
#: src/pages/MessagingSettingsOverlay.tsx
|
||||
msgid "Bot to link"
|
||||
msgstr "要关联的 Bot"
|
||||
@ -1031,10 +1060,6 @@ msgstr "要关联的 Bot"
|
||||
msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
|
||||
msgstr "默认情况下 Bot 会直接执行。只有当你想先审核某类操作时,才添加例外。"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Bots are working"
|
||||
msgstr "Bot 正在工作"
|
||||
|
||||
#: src/components/OoAtelier.tsx
|
||||
msgid "Bow"
|
||||
msgstr ""
|
||||
@ -1064,7 +1089,6 @@ msgstr ""
|
||||
msgid "Browse MCP servers"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Browser"
|
||||
msgstr ""
|
||||
@ -1493,6 +1517,10 @@ msgstr "电脑"
|
||||
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
|
||||
msgstr "电脑未开启。请设置 SANDBOX_PROVIDER=docker 并提供 SANDBOX_SUPERVISOR_TOKEN,或将 SANDBOX_PROVIDER 设为 e2b、daytona 或 box 并提供对应 API 密钥。更改 .env 后请重建环境。"
|
||||
|
||||
#: src/pages/PluginsOverlay.tsx
|
||||
msgid "Configure a plugin catalog on the server to connect apps."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Configured by deployment"
|
||||
msgstr "由部署配置"
|
||||
@ -1584,6 +1612,10 @@ msgstr "已连接并使用 {0}。"
|
||||
msgid "Connected app"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Connected tools"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Connected. Its tools are available from your next message."
|
||||
msgstr "已连接,下一条消息起即可使用其工具。"
|
||||
@ -2322,6 +2354,7 @@ msgstr "正在断开…"
|
||||
#: src/components/ComputerUpdateProgress.tsx
|
||||
#: src/components/teach/TeachComputerOverlay.tsx
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Dismiss"
|
||||
msgstr ""
|
||||
@ -2381,7 +2414,9 @@ msgstr ""
|
||||
#: src/components/ThreadCard.tsx
|
||||
#: src/pages/HeuteBrief.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Done"
|
||||
msgstr "完成"
|
||||
|
||||
@ -2394,8 +2429,11 @@ msgstr ""
|
||||
msgid "Dot"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: activeItem.name
|
||||
#. placeholder {0}: item.name
|
||||
#. placeholder {0}: props.name
|
||||
#: src/components/ArtifactFileCard.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {0}"
|
||||
msgstr "下载 {0}"
|
||||
|
||||
@ -2403,7 +2441,6 @@ msgstr "下载 {0}"
|
||||
#: src/components/ArtifactPane.tsx
|
||||
#: src/components/media/MediaCard.tsx
|
||||
#: src/components/media/MediaPlayer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Download {name}"
|
||||
msgstr "下载 {name}"
|
||||
|
||||
@ -2589,6 +2626,8 @@ msgid "Expand"
|
||||
msgstr "展开"
|
||||
|
||||
#. placeholder {0}: group.title
|
||||
#. placeholder {0}: item.name
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/sidebar-group-header.tsx
|
||||
msgid "Expand {0}"
|
||||
msgstr "展开 {0}"
|
||||
@ -2660,6 +2699,10 @@ msgstr ""
|
||||
msgid "Failed to open file"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to remove message"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Failed to send message"
|
||||
msgstr "发送消息失败"
|
||||
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Hi, this is how I'll sound when I read replies out loud."
|
||||
msgstr "你好,这是我朗读回复时的声音。"
|
||||
msgid "Hi, this is how I'll sound when we talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/sidebar-header.tsx
|
||||
msgid "Hide bots"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Hide details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Hide password"
|
||||
msgstr "隐藏密码"
|
||||
@ -3080,6 +3128,7 @@ msgstr ""
|
||||
msgid "Import OpenAPI JSON"
|
||||
msgstr "导入 OpenAPI JSON"
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
#: src/pages/shell/run-steps.tsx
|
||||
msgid "In Bearbeitung"
|
||||
msgstr ""
|
||||
@ -3166,6 +3215,10 @@ msgstr "隔离"
|
||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||
msgstr "它的对话、文件和例行任务将被永久删除。它创建的 Bot 仍留在列表中。"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Jump to latest"
|
||||
msgstr "跳到最新"
|
||||
@ -3174,6 +3227,10 @@ msgstr "跳到最新"
|
||||
msgid "Jump to replied message"
|
||||
msgstr "跳到被回复的消息"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Just ask in the chat whenever something should happen regularly."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
msgid "just now"
|
||||
@ -3774,6 +3831,7 @@ msgid "New space"
|
||||
msgstr "新建工作区"
|
||||
|
||||
#: src/components/CookingMode.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
@ -3885,6 +3943,10 @@ msgstr ""
|
||||
msgid "No providers found."
|
||||
msgstr "未找到提供方。"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
msgid "No recent activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/integrations/IntegrationSetup.tsx
|
||||
msgid "No remote MCP servers found"
|
||||
msgstr ""
|
||||
@ -3899,6 +3961,7 @@ msgid "No results"
|
||||
msgstr "无结果"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "No runs yet"
|
||||
msgstr "还没有运行记录"
|
||||
|
||||
@ -3957,6 +4020,7 @@ msgid "Notifications"
|
||||
msgstr "通知"
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Now"
|
||||
msgstr "现在"
|
||||
|
||||
@ -4027,7 +4091,6 @@ msgid "Oo name"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ScratchpadSection.tsx
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
msgid "Open"
|
||||
msgstr "打开"
|
||||
|
||||
@ -4056,7 +4119,7 @@ msgstr ""
|
||||
msgid "Open in browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/computer-panel-preview.tsx
|
||||
#: src/components/browser/OoComputerBrowser.tsx
|
||||
msgid "Open in full window"
|
||||
msgstr "在完整窗口中打开"
|
||||
|
||||
@ -4288,10 +4351,6 @@ msgstr ""
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Project · sessions & folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/dialogs.tsx
|
||||
#: src/pages/shell/project-look-dialog.tsx
|
||||
msgid "Project folder"
|
||||
@ -4399,6 +4458,10 @@ msgstr ""
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelSettingsOverlay.tsx
|
||||
msgid "Reasoning effort"
|
||||
msgstr "推理强度"
|
||||
@ -4412,6 +4475,7 @@ msgid "Recall only"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ActivityList.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Recent"
|
||||
msgstr "最近"
|
||||
|
||||
@ -4528,6 +4592,10 @@ msgstr "移除 {0}"
|
||||
msgid "Remove folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Remove from queue"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
msgid "Remove Git event"
|
||||
msgstr ""
|
||||
@ -4653,6 +4721,11 @@ msgstr "还原上次保存的工作区。电脑上未保存的工作会丢失。
|
||||
msgid "Restoring your workspace"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FolderPickerDialog.tsx
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
@ -4700,11 +4773,13 @@ msgstr ""
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/routine-event-line.tsx
|
||||
msgid "Routine"
|
||||
msgstr "例行任务"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Routines"
|
||||
msgstr "例行任务"
|
||||
|
||||
@ -4718,6 +4793,7 @@ msgid "Run at"
|
||||
msgstr "运行时间"
|
||||
|
||||
#: src/pages/RoutineEditor.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Run history"
|
||||
msgstr "运行记录"
|
||||
|
||||
@ -4756,10 +4832,6 @@ msgstr "正在运行…"
|
||||
msgid "Runs when this bot receives a verified message from this provider."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Sandbox"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AskCard.tsx
|
||||
#: src/components/MediaSettings.tsx
|
||||
#: src/components/teach/SkillDraftCard.tsx
|
||||
@ -4909,6 +4981,10 @@ msgstr "发送回答"
|
||||
msgid "Send it"
|
||||
msgstr "发送"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Send now"
|
||||
msgstr "立即发送"
|
||||
|
||||
#: src/pages/Auth.tsx
|
||||
msgid "Send reset link"
|
||||
msgstr "发送重置链接"
|
||||
@ -5021,6 +5097,10 @@ msgstr ""
|
||||
msgid "Show computer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Show details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Show less"
|
||||
msgstr ""
|
||||
@ -5037,6 +5117,7 @@ msgstr "显示更多"
|
||||
msgid "Show password"
|
||||
msgstr "显示密码"
|
||||
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
#: src/pages/shell/side-panel-header.tsx
|
||||
msgid "Show settings"
|
||||
msgstr "显示设置"
|
||||
@ -5172,6 +5253,7 @@ msgstr ""
|
||||
msgid "Speaking with intent…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
#: src/lib/onboarding-copy.ts
|
||||
msgid "Specialist crew"
|
||||
msgstr ""
|
||||
@ -5286,6 +5368,10 @@ msgstr "停止示范"
|
||||
msgid "Stop timer"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Stopped"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/lib/browser-notifications.ts
|
||||
msgid "Stopped."
|
||||
@ -5398,6 +5484,7 @@ msgstr "正在示范,发送新消息前请先停止示范。"
|
||||
|
||||
#: src/pages/ProjectSessionTimeline.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Team"
|
||||
msgstr "团队"
|
||||
|
||||
@ -5512,10 +5599,6 @@ msgstr "此订阅登录在 Rakazo 中尚不可用。请使用部署凭据或选
|
||||
msgid "This will permanently delete this project and all its conversations, bots, and memory."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Shell.tsx
|
||||
msgid "Threads by topic · folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ModelTierSettings.tsx
|
||||
#: src/pages/shell/bot-panel.tsx
|
||||
msgid "Tiers"
|
||||
@ -5701,6 +5784,7 @@ msgstr ""
|
||||
|
||||
#: src/components/DesktopUpdates.tsx
|
||||
#: src/components/SoftwareUpdateSection.tsx
|
||||
#: src/pages/AccountSettingsOverlay.tsx
|
||||
msgid "Up to date"
|
||||
msgstr "已是最新"
|
||||
|
||||
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/OoStage.tsx
|
||||
#: src/pages/shell/project-overview.tsx
|
||||
msgid "Working"
|
||||
msgstr ""
|
||||
|
||||
@ -6037,6 +6122,7 @@ msgstr "昨天"
|
||||
#: src/components/UserProfileSettings.tsx
|
||||
#: src/components/voice/VoiceStage.tsx
|
||||
#: src/pages/Shell.tsx
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "You"
|
||||
msgstr "你"
|
||||
|
||||
@ -6077,13 +6163,25 @@ msgid "Your name"
|
||||
msgstr "你的名字"
|
||||
|
||||
#: src/pages/VoiceSettingsOverlay.tsx
|
||||
msgid "Your Oo reads replies aloud when this is on."
|
||||
msgid "Your Oo talks with you naturally when this is on."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Onboarding.tsx
|
||||
msgid "Your Oo will communicate in this language. You can change it later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
|
||||
msgid "Your profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Welcome.tsx
|
||||
msgid "Your specialists. One calm place to talk."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom in"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/message-cards.tsx
|
||||
msgid "Zoom out"
|
||||
msgstr ""
|
||||
|
||||
@ -188,6 +188,11 @@ export function UpdatesSettingsPanel({
|
||||
<div className="space-y-5">
|
||||
<DesktopUpdateSection />
|
||||
<SoftwareUpdateSection isDeploymentOwner={isDeploymentOwner} />
|
||||
<section className="hidden rounded-xl border border-border px-4 py-4 only:block">
|
||||
<p className="text-ui-body-sm text-muted-foreground">
|
||||
<Trans>Up to date</Trans>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -30,7 +30,13 @@ export function ActivityList({ onOpenRun }: ActivityListProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (activeRuns.length === 0 && recentRuns.length === 0) return null;
|
||||
if (activeRuns.length === 0 && recentRuns.length === 0) {
|
||||
return (
|
||||
<div className="mb-2 border-b border-border px-2.5 py-2 text-ui-body-sm text-muted-foreground/80">
|
||||
<Trans>No recent activity</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-2 border-b border-border pb-2">
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import type { ThreadMessage, ThreadSnapshot } from "@rakazo/contracts";
|
||||
import { isSecretAskBlock, speechFromBlocks, spokenDecision } from "@rakazo/core";
|
||||
import {
|
||||
isSecretAskBlock,
|
||||
speechFromBlocks,
|
||||
spokenDecision,
|
||||
spokenSummaryFromBlocks,
|
||||
} from "@rakazo/core";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { VoicePhase, VoiceTranscriptLine } from "../components/voice/VoiceStage";
|
||||
import { VoiceStage } from "../components/voice/VoiceStage";
|
||||
@ -417,7 +422,7 @@ export function CallView({
|
||||
(block) => block.kind === "ask" && block.status !== "answered",
|
||||
);
|
||||
const secretAsk = ask && isSecretAskBlock(ask);
|
||||
const rawText = speechFromBlocks(lastBot.blocks);
|
||||
const rawText = spokenSummaryFromBlocks(lastBot.blocks);
|
||||
const text = rawText
|
||||
? secretAsk
|
||||
? `${rawText}. ${secretPromptRef.current}`
|
||||
@ -477,7 +482,7 @@ export function CallView({
|
||||
hasUnspokenBotMessage: Boolean(
|
||||
lastBot &&
|
||||
lastBot.id !== spokenMessage.current &&
|
||||
speechFromBlocks(lastBot.blocks).trim(),
|
||||
spokenSummaryFromBlocks(lastBot.blocks).trim(),
|
||||
),
|
||||
runActive: isActiveRunStatus(snapshot?.run?.status),
|
||||
userTurnPending: userTurnPendingRef.current,
|
||||
|
||||
@ -7,6 +7,9 @@ import type {
|
||||
Group,
|
||||
HabitToday,
|
||||
RunActivityRow,
|
||||
Space,
|
||||
SpaceBot,
|
||||
SpaceGroup,
|
||||
} from "@rakazo/contracts";
|
||||
import { runChatTitle } from "@rakazo/core";
|
||||
import { botColors } from "@rakazo/ui-tokens";
|
||||
@ -30,6 +33,7 @@ import { rpc } from "../lib/rpc";
|
||||
type HeuteBriefProps = {
|
||||
bots: Bot[];
|
||||
groups: Group[];
|
||||
spaces?: Space[];
|
||||
onOpenBot: (botId: string, spaceId: string) => void;
|
||||
onOpenGroup: (groupId: string, spaceId: string) => void;
|
||||
onOpenRun: (run: RunActivityRow) => void;
|
||||
@ -41,6 +45,7 @@ const DATE_LABEL: Intl.DateTimeFormatOptions = { weekday: "long", day: "numeric"
|
||||
export function HeuteBrief({
|
||||
bots,
|
||||
groups,
|
||||
spaces = [],
|
||||
onOpenBot,
|
||||
onOpenGroup,
|
||||
onOpenRun,
|
||||
@ -135,13 +140,21 @@ export function HeuteBrief({
|
||||
buildHeuteBrief({
|
||||
bots,
|
||||
groups,
|
||||
spaces,
|
||||
activeRuns,
|
||||
recentRuns,
|
||||
}),
|
||||
[activeRuns, bots, groups, recentRuns],
|
||||
[activeRuns, bots, groups, recentRuns, spaces],
|
||||
);
|
||||
|
||||
const botColorById = useMemo(() => new Map(bots.map((bot) => [bot.id, bot.color])), [bots]);
|
||||
const botColorById = useMemo(
|
||||
() =>
|
||||
new Map([
|
||||
...spaces.flatMap((space) => space.bots).map((bot) => [bot.id, bot.color] as const),
|
||||
...bots.map((bot) => [bot.id, bot.color] as const),
|
||||
]),
|
||||
[bots, spaces],
|
||||
);
|
||||
|
||||
const locale = i18n.locale || "de";
|
||||
const dateLabel = coach
|
||||
@ -391,13 +404,13 @@ function NeedsYouRow({
|
||||
>
|
||||
{chatKind === "bot" ? (
|
||||
<BotAvatar
|
||||
color={(chat as Bot).color}
|
||||
color={(chat as SpaceBot).color}
|
||||
identity={chat.id}
|
||||
size={40}
|
||||
status={(chat as Bot).status}
|
||||
status={(chat as SpaceBot).status}
|
||||
/>
|
||||
) : (
|
||||
<GroupAvatar members={(chat as Group).members} size={40} />
|
||||
<GroupAvatar members={(chat as SpaceGroup).members} size={40} />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-ui-body font-semibold text-foreground">{title}</div>
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
abortableDelay,
|
||||
buildFeaturedConnectorTiles,
|
||||
CONNECTION_CATALOG_PAGE_SIZE,
|
||||
EMPTY_PLUGIN_CATALOG_MESSAGE,
|
||||
filterConnectionCatalogItems,
|
||||
humanizeToolName,
|
||||
isGithubInstall,
|
||||
@ -730,7 +729,7 @@ export function PluginsOverlay({
|
||||
<div className="mb-6" data-testid="featured-connectors">
|
||||
{!loading && catalog.length === 0 ? (
|
||||
<p className="text-ui-body leading-6 text-muted-foreground/80">
|
||||
{EMPTY_PLUGIN_CATALOG_MESSAGE}
|
||||
<Trans>Configure a plugin catalog on the server to connect apps.</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
|
||||
@ -176,7 +176,7 @@ export function SettingsOverlay({
|
||||
<nav
|
||||
data-testid="settings-nav"
|
||||
aria-label={t`Settings`}
|
||||
className="flex shrink-0 flex-row gap-1 overflow-x-auto border-b border-border px-3 py-3 md:w-[200px] md:flex-col md:overflow-y-auto md:border-b-0 md:border-e md:px-3 md:py-4"
|
||||
className="flex shrink-0 flex-row gap-1 overflow-x-auto border-b border-border px-3 py-3 md:w-[220px] md:flex-col md:overflow-y-auto md:border-b-0 md:border-e md:px-3 md:py-4"
|
||||
>
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import type { VoiceCredential, VoiceInfo, VoiceStatus } from "@rakazo/contracts";
|
||||
import { voiceIdForLocale, voicesForLocale } from "@rakazo/core";
|
||||
import { GERMAN_VOICE_PRESETS, voiceIdForLocale, voicesForLocale } from "@rakazo/core";
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
@ -46,7 +46,7 @@ export function VoiceSettingsOverlay({
|
||||
const [voiceId, setVoiceId] = useState("");
|
||||
const [autoSpeak, setAutoSpeak] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pending, setPending] = useState<"connect" | "voice" | "speak" | null>(null);
|
||||
const [pending, setPending] = useState<"voice" | "speak" | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
const [soundEnabled, setSoundEnabled] = useState(() => readOoSoundEnabled());
|
||||
@ -74,7 +74,7 @@ export function VoiceSettingsOverlay({
|
||||
};
|
||||
}, []);
|
||||
|
||||
function markPending(next: "connect" | "voice" | "speak" | null) {
|
||||
function markPending(next: "voice" | "speak" | null) {
|
||||
setPending(next);
|
||||
onBusyChange?.(next !== null);
|
||||
}
|
||||
@ -113,17 +113,22 @@ export function VoiceSettingsOverlay({
|
||||
}, [activeBotId]);
|
||||
|
||||
const voiceOptions = useMemo(() => {
|
||||
const listed = voicesForLocale(voices, uiLocale);
|
||||
const isGermanLocale = uiLocale.toLowerCase().startsWith("de");
|
||||
const hasGermanVoices = voices.some((v) => /^d[fm]_/i.test(v.id));
|
||||
const combined =
|
||||
isGermanLocale && !hasGermanVoices ? [...GERMAN_VOICE_PRESETS, ...voices] : voices;
|
||||
const listed = voicesForLocale(combined, uiLocale);
|
||||
if (listed.length) return listed;
|
||||
if (isGermanLocale) return GERMAN_VOICE_PRESETS;
|
||||
return voiceId ? [{ id: voiceId, label: voiceId }] : [];
|
||||
}, [voices, voiceId, uiLocale]);
|
||||
|
||||
const speakingReady = Boolean(localConnected && voiceId && status?.ready);
|
||||
const speakingReady = Boolean(
|
||||
(localConnected && voiceId && status?.ready) || (autoSpeak && voiceOptions.length > 0),
|
||||
);
|
||||
|
||||
async function ensureLocalVoice(): Promise<boolean> {
|
||||
if (localConnected) return true;
|
||||
markPending("connect");
|
||||
setError(null);
|
||||
try {
|
||||
await rpc.voice.connect({
|
||||
provider: LOCAL_PROVIDER_ID,
|
||||
@ -133,16 +138,22 @@ export function VoiceSettingsOverlay({
|
||||
await refresh();
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (typeof window !== "undefined" && "speechSynthesis" in window) {
|
||||
return true;
|
||||
}
|
||||
setError(err instanceof Error ? err.message : t`Could not enable speaking`);
|
||||
return false;
|
||||
} finally {
|
||||
markPending(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseVoice(nextVoiceId: string) {
|
||||
setVoiceId(nextVoiceId);
|
||||
if (!localConnected) return;
|
||||
if (!localConnected) {
|
||||
if (activeBotId) {
|
||||
await rpc.bots.update({ botId: activeBotId, voiceId: nextVoiceId }).catch(() => undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
markPending("voice");
|
||||
setError(null);
|
||||
try {
|
||||
@ -161,6 +172,7 @@ export function VoiceSettingsOverlay({
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
try {
|
||||
let chosenVoice = voiceId;
|
||||
if (next) {
|
||||
const connected = await ensureLocalVoice();
|
||||
if (!connected) {
|
||||
@ -168,16 +180,29 @@ export function VoiceSettingsOverlay({
|
||||
return;
|
||||
}
|
||||
const listed = await rpc.voice.voices({ provider: LOCAL_PROVIDER_ID }).catch(() => []);
|
||||
const nextVoice = voiceIdForLocale(listed, uiLocale, voiceId);
|
||||
const isGermanLocale = uiLocale.toLowerCase().startsWith("de");
|
||||
const pool =
|
||||
isGermanLocale && !listed.some((v) => /^d[fm]_/i.test(v.id))
|
||||
? [...GERMAN_VOICE_PRESETS, ...listed]
|
||||
: listed;
|
||||
const nextVoice = voiceIdForLocale(pool, uiLocale, voiceId);
|
||||
if (nextVoice) {
|
||||
await rpc.voice.setVoice({ voiceId: nextVoice, provider: LOCAL_PROVIDER_ID });
|
||||
chosenVoice = nextVoice;
|
||||
setVoiceId(nextVoice);
|
||||
await rpc.voice
|
||||
.setVoice({ voiceId: nextVoice, provider: LOCAL_PROVIDER_ID })
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
if (activeBotId) {
|
||||
await rpc.bots.update({ botId: activeBotId, autoSpeak: next });
|
||||
await rpc.bots.update({
|
||||
botId: activeBotId,
|
||||
autoSpeak: next,
|
||||
...(chosenVoice ? { voiceId: chosenVoice } : {}),
|
||||
});
|
||||
}
|
||||
await refresh();
|
||||
if (!voiceId && chosenVoice) setVoiceId(chosenVoice);
|
||||
setNotice(next ? t`Speaking is on.` : t`Speaking is off.`);
|
||||
} catch (err) {
|
||||
setAutoSpeak(!next);
|
||||
@ -193,7 +218,11 @@ export function VoiceSettingsOverlay({
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
void speaker.speak(t`Hi, this is how I'll sound when I read replies out loud.`, {
|
||||
const isGermanVoice = /^d[fm]_/i.test(voice.id) || uiLocale.toLowerCase().startsWith("de");
|
||||
const sampleText = isGermanVoice
|
||||
? `Hallo, ich bin ${voice.label}. Lass uns direkt miteinander sprechen.`
|
||||
: t`Hi, this is how I'll sound when we talk.`;
|
||||
void speaker.speak(sampleText, {
|
||||
voiceId: voice.id,
|
||||
messageId: `${PREVIEW_PREFIX}${voice.id}`,
|
||||
});
|
||||
@ -234,7 +263,7 @@ export function VoiceSettingsOverlay({
|
||||
{autoSpeak ? <Trans>Can speak</Trans> : <Trans>Does not speak</Trans>}
|
||||
</p>
|
||||
<p className="mt-1 text-ui-body-sm text-muted-foreground">
|
||||
<Trans>Your Oo reads replies aloud when this is on.</Trans>
|
||||
<Trans>Your Oo talks with you naturally when this is on.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
ModelTierSchema,
|
||||
normalizeBotTags,
|
||||
} from "@rakazo/contracts";
|
||||
import { GERMAN_VOICE_PRESETS, germanVoiceForBot, voicesForLocale } from "@rakazo/core";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@ -37,6 +38,7 @@ import { connectedModelOptions } from "../../lib/connected-models";
|
||||
import { lazyWithRecovery } from "../../lib/lazy-recovery";
|
||||
import { modelTierLabel } from "../../lib/model-tier-label";
|
||||
import { rpc } from "../../lib/rpc";
|
||||
import { readUiLocale } from "../../lib/ui-locale";
|
||||
import { BotCapabilitiesFields } from "./bot-capabilities";
|
||||
|
||||
const ScratchpadSection = lazyWithRecovery("ScratchpadSection", () =>
|
||||
@ -280,8 +282,8 @@ export function BotSettings({
|
||||
const [computerMode, setComputerMode] = useState(bot.computerMode);
|
||||
const [memoryScope, setMemoryScope] = useState(bot.memoryScope);
|
||||
const [autoSpeak, setAutoSpeak] = useState(bot.autoSpeak);
|
||||
const [voiceId, setVoiceId] = useState(bot.voiceId ?? "");
|
||||
const [voices, setVoices] = useState<VoiceInfo[]>([]);
|
||||
const [voiceId, setVoiceId] = useState(bot.voiceId || germanVoiceForBot(bot.id, bot.voiceId));
|
||||
const [voices, setVoices] = useState<VoiceInfo[]>(GERMAN_VOICE_PRESETS);
|
||||
const [modelKey, setModelKey] = useState(
|
||||
bot.modelTier
|
||||
? tierOptionKey(bot.modelTier)
|
||||
@ -314,8 +316,15 @@ export function BotSettings({
|
||||
useEffect(() => {
|
||||
void rpc.voice
|
||||
.voices({})
|
||||
.then(setVoices)
|
||||
.catch(() => setVoices([]));
|
||||
.then((listed) => {
|
||||
const uiLocale = readUiLocale();
|
||||
const isGerman = uiLocale.toLowerCase().startsWith("de");
|
||||
const hasGerman = listed.some((v) => /^d[fm]_/i.test(v.id));
|
||||
const combined = isGerman && !hasGerman ? [...GERMAN_VOICE_PRESETS, ...listed] : listed;
|
||||
const filtered = voicesForLocale(combined, uiLocale);
|
||||
setVoices(filtered.length > 0 ? filtered : GERMAN_VOICE_PRESETS);
|
||||
})
|
||||
.catch(() => setVoices(GERMAN_VOICE_PRESETS));
|
||||
void rpc.bots
|
||||
.builtinTools({ botId: bot.id })
|
||||
.then(setToolNames)
|
||||
@ -675,7 +684,7 @@ export function BotSettings({
|
||||
<NativeSelectOption value="">{t`Account default`}</NativeSelectOption>
|
||||
{voices.map((voice) => (
|
||||
<NativeSelectOption key={voice.id} value={voice.id}>
|
||||
{voice.label}
|
||||
{voice.description ? `${voice.label} (${voice.description})` : voice.label}
|
||||
</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
|
||||
249
apps/web/src/pages/shell/composer-task-pill.test.tsx
Normal file
249
apps/web/src/pages/shell/composer-task-pill.test.tsx
Normal file
@ -0,0 +1,249 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import type { ThreadMessage } from "@rakazo/contracts";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
function formatTemplate(parts: TemplateStringsArray, ...values: unknown[]) {
|
||||
return parts.reduce(
|
||||
(text, part, index) => text + part + (index < values.length ? values[index] : ""),
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
vi.mock("@lingui/core/macro", () => ({
|
||||
t: formatTemplate,
|
||||
msg: (parts: TemplateStringsArray, ...values: unknown[]) => ({
|
||||
id: parts.join(""),
|
||||
message: formatTemplate(parts, ...values),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@lingui/react/macro", () => {
|
||||
return {
|
||||
useLingui: () => ({ t: formatTemplate }),
|
||||
Trans: ({ children }: { children: React.ReactNode }) => children,
|
||||
Plural: ({ value, one, other }: { value: number; one: string; other: string }) =>
|
||||
(value === 1 ? one : other).replace("#", String(value)),
|
||||
};
|
||||
});
|
||||
|
||||
import { ComposerTaskPill, extractActivePlanState, PlanCard } from "./message-cards";
|
||||
|
||||
// @ts-expect-error React act environment flag
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
function buildSampleMessages(): ThreadMessage[] {
|
||||
return [
|
||||
{
|
||||
id: "u1",
|
||||
threadId: "t1",
|
||||
seq: 1,
|
||||
role: "user",
|
||||
runId: "r1",
|
||||
createdAt: "2026-10-22T12:00:00Z",
|
||||
blocks: [{ kind: "text", text: "Build brand concept" }],
|
||||
},
|
||||
{
|
||||
id: "b-plan",
|
||||
threadId: "t1",
|
||||
seq: 2,
|
||||
role: "bot",
|
||||
botId: "bot-1",
|
||||
runId: "r1",
|
||||
createdAt: "2026-10-22T12:00:05Z",
|
||||
blocks: [
|
||||
{
|
||||
kind: "plan",
|
||||
planId: "p1",
|
||||
title: "Branding-Konzept finalisieren",
|
||||
status: "active",
|
||||
steps: [
|
||||
{ id: "s1", label: "Slogans erstellen", status: "completed" },
|
||||
{
|
||||
id: "s2",
|
||||
label: "Brand Guide HTML-Seite erstellen",
|
||||
status: "in_progress",
|
||||
note: "Komplette Zusammenfassung aller Brand-Elemente",
|
||||
},
|
||||
{ id: "s3", label: "Alles auf Server kopieren", status: "pending" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "b-step-1",
|
||||
threadId: "t1",
|
||||
seq: 3,
|
||||
role: "bot",
|
||||
botId: "bot-1",
|
||||
runId: "r1",
|
||||
createdAt: "2026-10-22T12:00:10Z",
|
||||
blocks: [{ kind: "text", text: "running: pnpm build" }],
|
||||
},
|
||||
{
|
||||
id: "b-step-2",
|
||||
threadId: "t1",
|
||||
seq: 4,
|
||||
role: "bot",
|
||||
botId: "bot-1",
|
||||
runId: "r1",
|
||||
createdAt: "2026-10-22T12:00:15Z",
|
||||
blocks: [{ kind: "text", text: "writing brand-guide.html" }],
|
||||
},
|
||||
{
|
||||
id: "progress:r1",
|
||||
threadId: "t1",
|
||||
seq: 5,
|
||||
role: "bot",
|
||||
botId: "bot-1",
|
||||
runId: "r1",
|
||||
createdAt: "2026-10-22T12:00:20Z",
|
||||
blocks: [{ kind: "progress", text: "Working..." }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
describe("ComposerTaskPill & PlanCard integration", () => {
|
||||
beforeEach(() => {
|
||||
i18n.load("en", {});
|
||||
i18n.activate("en");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("extractActivePlanState collects the active plan and folded narration steps from the current turn", () => {
|
||||
const messages = buildSampleMessages();
|
||||
const state = extractActivePlanState(messages, true, "2026-10-22T12:00:00Z");
|
||||
expect(state).not.toBeNull();
|
||||
expect(state?.block.planId).toBe("p1");
|
||||
expect(state?.steps.map((s) => s.id)).toEqual(["b-step-1", "b-step-2"]);
|
||||
expect(state?.running).toBe(true);
|
||||
});
|
||||
|
||||
it("renders compact active step + progress badge and expands to reveal full checklist & intermediate steps", async () => {
|
||||
const state = extractActivePlanState(buildSampleMessages(), true, "2026-10-22T12:00:00Z")!;
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<ComposerTaskPill activePlan={state} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
const pill = container.querySelector('[data-testid="composer-task-pill"]');
|
||||
expect(pill).not.toBeNull();
|
||||
expect(pill?.getAttribute("data-expanded")).toBe("false");
|
||||
expect(container.querySelector('[data-testid="composer-task-pill-label"]')?.textContent).toBe(
|
||||
"Brand Guide HTML-Seite erstellen",
|
||||
);
|
||||
expect(
|
||||
container.querySelector('[data-testid="composer-task-pill-progress"]')?.textContent,
|
||||
).toBe("1/3");
|
||||
|
||||
const toggle = container.querySelector(
|
||||
'[data-testid="composer-task-pill-toggle"]',
|
||||
) as HTMLButtonElement;
|
||||
await act(async () => {
|
||||
toggle.click();
|
||||
});
|
||||
|
||||
expect(pill?.getAttribute("data-expanded")).toBe("true");
|
||||
const intermediate = container.querySelector(
|
||||
'[data-testid="composer-plan-intermediate-steps"]',
|
||||
);
|
||||
expect(intermediate).not.toBeNull();
|
||||
|
||||
const stepToggle = intermediate?.querySelector("button") as HTMLButtonElement;
|
||||
await act(async () => {
|
||||
stepToggle.click();
|
||||
});
|
||||
|
||||
expect(intermediate?.textContent).toContain("running: pnpm build");
|
||||
expect(intermediate?.textContent).toContain("writing brand-guide.html");
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("auto-collapses and hides after all steps complete", async () => {
|
||||
vi.useFakeTimers();
|
||||
const completedPlan = {
|
||||
block: {
|
||||
kind: "plan" as const,
|
||||
planId: "p-done",
|
||||
title: "Branding-Konzept finalisieren",
|
||||
status: "completed" as const,
|
||||
steps: [
|
||||
{ id: "s1", label: "Slogans erstellen", status: "completed" as const },
|
||||
{ id: "s2", label: "Brand Guide erstellen", status: "completed" as const },
|
||||
],
|
||||
},
|
||||
steps: [],
|
||||
running: false,
|
||||
};
|
||||
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<ComposerTaskPill activePlan={completedPlan} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-testid="composer-task-pill"]')).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="composer-task-pill-progress"]')?.textContent,
|
||||
).toBe("Done");
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(3600);
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-testid="composer-task-pill"]')).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("PlanCard embeds intermediate steps under the active step", async () => {
|
||||
const state = extractActivePlanState(buildSampleMessages(), true, "2026-10-22T12:00:00Z")!;
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<PlanCard block={state.block} steps={state.steps} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
const activeStepRow = container.querySelector('[data-testid="plan-step-s2"]');
|
||||
expect(activeStepRow).not.toBeNull();
|
||||
expect(activeStepRow?.querySelector('[data-testid="plan-intermediate-steps"]')).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
});
|
||||
@ -5,11 +5,14 @@ import { ATTACHMENT_ALLOWED_MIME_TYPES } from "@rakazo/contracts";
|
||||
import type { ComposerMention, SlashActionId } from "@rakazo/core";
|
||||
import {
|
||||
clampMentionHighlightIndex,
|
||||
clearComposerDraft,
|
||||
mentionChipKey,
|
||||
readComposerDraft,
|
||||
resolveMentionPickerKey,
|
||||
SLASH_ACTIONS,
|
||||
serializeComposerPrompt,
|
||||
truncateSlashDescription,
|
||||
writeComposerDraft,
|
||||
} from "@rakazo/core";
|
||||
import {
|
||||
BotAvatar,
|
||||
@ -28,6 +31,7 @@ import {
|
||||
Play,
|
||||
Plus,
|
||||
Puzzle,
|
||||
RotateCcw,
|
||||
Settings,
|
||||
Square,
|
||||
X,
|
||||
@ -37,6 +41,8 @@ import { memo, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } fr
|
||||
import { VoiceAuroraRim } from "../../components/voice/VoiceAurora";
|
||||
import { isFileDrag, isFilePaste } from "../../lib/pending-attachments";
|
||||
import { AutopilotDialog } from "./autopilot-dialog";
|
||||
import type { ComposerActivePlan } from "./message-cards";
|
||||
import { ComposerTaskPill } from "./message-cards";
|
||||
import { previewMessageText } from "./message-preview";
|
||||
import type { PendingAttachment } from "./shell-types";
|
||||
import { FALLBACK_BOT_COLOR } from "./shell-types";
|
||||
@ -48,6 +54,7 @@ const addButtonClass =
|
||||
"tactile-press size-10 shrink-0 rounded-full border border-border bg-foreground/5 text-foreground/80 transition-all hover:bg-foreground/10 hover:text-foreground flex items-center justify-center cursor-pointer";
|
||||
|
||||
export const Composer = memo(function Composer({
|
||||
draftKey,
|
||||
running,
|
||||
disabled,
|
||||
pendingAttachments,
|
||||
@ -76,7 +83,12 @@ export const Composer = memo(function Composer({
|
||||
onSlashAction,
|
||||
onConnectModel,
|
||||
onAutopilot,
|
||||
activePlan,
|
||||
stopped = false,
|
||||
onResume,
|
||||
onDismissStopped,
|
||||
}: {
|
||||
draftKey?: string;
|
||||
running: boolean;
|
||||
disabled?: boolean;
|
||||
pendingAttachments: PendingAttachment[];
|
||||
@ -107,15 +119,43 @@ export const Composer = memo(function Composer({
|
||||
onConnectModel?: () => void;
|
||||
/** Hand the order to the autopilot; the composer menu offers it only when set. */
|
||||
onAutopilot?: (goal: string, until?: string) => Promise<void>;
|
||||
activePlan?: ComposerActivePlan | null;
|
||||
stopped?: boolean;
|
||||
onResume?: () => void | Promise<void>;
|
||||
onDismissStopped?: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const [draft, setDraft] = useState("");
|
||||
const initialDraft = useMemo(() => readComposerDraft(draftKey), [draftKey]);
|
||||
const [draft, setDraft] = useState(() => initialDraft.text);
|
||||
const [autopilotOpen, setAutopilotOpen] = useState(false);
|
||||
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
||||
const [mentionHighlightIndex, setMentionHighlightIndex] = useState(0);
|
||||
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
||||
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(null);
|
||||
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>([]);
|
||||
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(
|
||||
() => initialDraft.selectedSkill,
|
||||
);
|
||||
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>(
|
||||
() => initialDraft.selectedMentions,
|
||||
);
|
||||
const loadedDraftKeyRef = useRef(draftKey);
|
||||
useLayoutEffect(() => {
|
||||
if (loadedDraftKeyRef.current !== draftKey) {
|
||||
loadedDraftKeyRef.current = draftKey;
|
||||
const stored = readComposerDraft(draftKey);
|
||||
setDraft(stored.text);
|
||||
setSelectedSkill(stored.selectedSkill);
|
||||
setSelectedMentions(stored.selectedMentions);
|
||||
setMentionQuery(null);
|
||||
setMentionHighlightIndex(0);
|
||||
setSlashQuery(null);
|
||||
return;
|
||||
}
|
||||
writeComposerDraft(draftKey, {
|
||||
text: draft,
|
||||
selectedSkill,
|
||||
selectedMentions,
|
||||
});
|
||||
}, [draft, draftKey, selectedMentions, selectedSkill]);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const runErrorRef = useRef<HTMLDivElement>(null);
|
||||
const submitFingerprint = useRef<string | null>(null);
|
||||
@ -265,6 +305,11 @@ export const Composer = memo(function Composer({
|
||||
function runSlashAction(action: SlashActionId) {
|
||||
setDraft("");
|
||||
setSlashQuery(null);
|
||||
writeComposerDraft(draftKey, {
|
||||
text: "",
|
||||
selectedSkill,
|
||||
selectedMentions,
|
||||
});
|
||||
onSlashAction?.(action);
|
||||
}
|
||||
|
||||
@ -300,7 +345,10 @@ export const Composer = memo(function Composer({
|
||||
const slashSkillOptions = useMemo(() => {
|
||||
if (slashQuery === null) return [];
|
||||
const query = slashQuery.trim().toLowerCase();
|
||||
const skills = agentSkills ?? [];
|
||||
// The user's own skills come first so built-in ones never crowd them out of the picker.
|
||||
const skills = [...(agentSkills ?? [])].sort(
|
||||
(a, b) => Number(a.source !== "user") - Number(b.source !== "user"),
|
||||
);
|
||||
return skills
|
||||
.filter((skill) => {
|
||||
if (!query) return true;
|
||||
@ -331,6 +379,7 @@ export const Composer = memo(function Composer({
|
||||
);
|
||||
if (submitFingerprint.current === fingerprint) return;
|
||||
submitFingerprint.current = fingerprint;
|
||||
clearComposerDraft(draftKey);
|
||||
setDraft("");
|
||||
setMentionQuery(null);
|
||||
setMentionHighlightIndex(0);
|
||||
@ -417,11 +466,11 @@ export const Composer = memo(function Composer({
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`relative z-30 m-0 min-w-0 border-0 px-3 pb-4 pt-3 md:px-6 md:pb-6 ${
|
||||
className={`composer-bottom-fade pointer-events-none absolute inset-x-0 bottom-0 z-30 m-0 min-w-0 max-w-full overflow-x-hidden border-0 px-3 pb-4 pt-10 md:px-6 md:pb-6 md:pt-12 ${
|
||||
draggingFiles ? "rounded-[14px] ring-2 ring-inset ring-ring" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto w-full max-w-4xl">
|
||||
<div className="pointer-events-auto mx-auto w-full max-w-4xl">
|
||||
{onConnectModel ? (
|
||||
<div
|
||||
data-testid="composer-model-needed"
|
||||
@ -442,6 +491,18 @@ export const Composer = memo(function Composer({
|
||||
className="mb-3 flex items-center gap-2 rounded-[14px] border border-destructive/40 bg-destructive/10 px-4 py-2 text-ui-body-sm text-destructive"
|
||||
>
|
||||
<span className="min-w-0 flex-1">{sendError ?? runError}</span>
|
||||
{runError && !sendError && onResume ? (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="composer-error-resume"
|
||||
disabled={sending}
|
||||
onClick={() => void onResume()}
|
||||
className="inline-flex shrink-0 items-center gap-1 rounded-full border border-destructive/30 bg-background/80 px-2.5 py-1 text-ui-label font-medium text-foreground transition-colors hover:bg-background disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
<RotateCcw className="size-3" aria-hidden="true" />
|
||||
<Trans>Resume</Trans>
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t`Dismiss error`}
|
||||
@ -455,6 +516,39 @@ export const Composer = memo(function Composer({
|
||||
<X strokeWidth={2} className="size-icon-sm" />
|
||||
</button>
|
||||
</div>
|
||||
) : stopped && !running && onResume ? (
|
||||
<div
|
||||
data-testid="composer-stopped"
|
||||
className="mb-3 flex items-center gap-2 rounded-[14px] border border-border bg-muted px-4 py-2 text-ui-body-sm text-muted-foreground"
|
||||
>
|
||||
<span className="min-w-0 flex-1">
|
||||
<Trans>Stopped</Trans>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="composer-resume"
|
||||
disabled={sending}
|
||||
onClick={() => void onResume()}
|
||||
className="inline-flex shrink-0 items-center gap-1 rounded-full border border-border bg-background px-2.5 py-1 text-ui-label font-medium text-foreground transition-colors hover:bg-accent disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
<RotateCcw className="size-3" aria-hidden="true" />
|
||||
<Trans>Resume</Trans>
|
||||
</button>
|
||||
{onDismissStopped ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t`Dismiss`}
|
||||
data-testid="composer-stopped-dismiss"
|
||||
onClick={() => {
|
||||
onDismissStopped();
|
||||
window.requestAnimationFrame(() => textareaRef.current?.focus());
|
||||
}}
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X strokeWidth={2} className="size-icon-sm" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{replyTarget ? (
|
||||
<div
|
||||
@ -564,7 +658,7 @@ export const Composer = memo(function Composer({
|
||||
{showSlashPicker ? (
|
||||
<div
|
||||
data-testid="slash-picker"
|
||||
className="mb-2 overflow-hidden rounded-[14px] border border-border bg-muted"
|
||||
className="mb-2 max-h-64 overflow-y-auto rounded-[14px] border border-border bg-muted"
|
||||
>
|
||||
{slashSkillOptions.map((skill) => (
|
||||
<button
|
||||
@ -608,6 +702,7 @@ export const Composer = memo(function Composer({
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{activePlan ? <ComposerTaskPill activePlan={activePlan} onResume={onResume} /> : null}
|
||||
<div
|
||||
data-testid="composer-bar"
|
||||
className="apple-island relative flex items-center gap-3.5 rounded-[30px] py-3 pe-3.5 ps-4 transition-all duration-200"
|
||||
@ -665,6 +760,11 @@ export const Composer = memo(function Composer({
|
||||
onStart={async (goal, until) => {
|
||||
await onAutopilot(goal, until);
|
||||
setDraft("");
|
||||
writeComposerDraft(draftKey, {
|
||||
text: "",
|
||||
selectedSkill,
|
||||
selectedMentions,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@ -1,105 +0,0 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import type { Bot, ComputerStatus } from "@rakazo/contracts";
|
||||
import { Maximize2 } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
ComputersUnavailableHint,
|
||||
computersAreUnavailable,
|
||||
} from "../../components/ComputersUnavailableHint";
|
||||
import { AgentCursor } from "../../components/computer/AgentCursor";
|
||||
import {
|
||||
computerLabel,
|
||||
computerPlaceholder,
|
||||
DesktopKindEmptyState,
|
||||
screenIframeSandbox,
|
||||
} from "./computer-screen";
|
||||
import { WorkspaceFilesView } from "./workspace-files";
|
||||
|
||||
/** The bot's screen in the side panel: a live, non-interactive preview that opens full window. */
|
||||
export function ComputerPanelPreview({
|
||||
active,
|
||||
computer,
|
||||
computerOpen,
|
||||
booting,
|
||||
embeddedScreenUrl,
|
||||
computerScreenError,
|
||||
sandboxProvider,
|
||||
onOpen,
|
||||
}: {
|
||||
active: Bot;
|
||||
computer: ComputerStatus | null;
|
||||
computerOpen: boolean;
|
||||
booting: boolean;
|
||||
embeddedScreenUrl: string | null;
|
||||
computerScreenError: ReactNode;
|
||||
sandboxProvider: string | undefined;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
// Opening the panel never starts the computer, so a resting one shows its Open action outright.
|
||||
const resting =
|
||||
!booting &&
|
||||
computer?.state !== "running" &&
|
||||
computer?.state !== "booting" &&
|
||||
!computersAreUnavailable(sandboxProvider);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-testid="computer-preview"
|
||||
className="group relative aspect-[16/10] overflow-hidden rounded-[14px] bg-background"
|
||||
>
|
||||
{computerOpen ? (
|
||||
<div className="grid h-full place-items-center text-sm text-muted-foreground/80">
|
||||
<Trans>Open in full window</Trans>
|
||||
</div>
|
||||
) : computer?.kind === "desktop" ? (
|
||||
<DesktopKindEmptyState className="grid h-full place-items-center px-6 text-center text-sm text-muted-foreground/80" />
|
||||
) : computer?.state === "running" && embeddedScreenUrl && !computerScreenError ? (
|
||||
<>
|
||||
<iframe
|
||||
title={t`Bot screen preview`}
|
||||
src={embeddedScreenUrl}
|
||||
sandbox={screenIframeSandbox(embeddedScreenUrl)}
|
||||
className="h-full w-full border-0 bg-black"
|
||||
allow="clipboard-read; clipboard-write"
|
||||
style={{ pointerEvents: "none" }}
|
||||
/>
|
||||
<AgentCursor botId={active.id} color={active.color} name={active.name} />
|
||||
</>
|
||||
) : (
|
||||
<div className="grid h-full place-items-center px-6 text-center text-sm text-muted-foreground/80">
|
||||
{computerScreenError ??
|
||||
(computersAreUnavailable(sandboxProvider) ? (
|
||||
<ComputersUnavailableHint />
|
||||
) : (
|
||||
computerPlaceholder(
|
||||
computer?.state,
|
||||
booting,
|
||||
computerLabel(computer?.mode, active.name),
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!computerScreenError ? (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="computer-preview-open"
|
||||
data-resting={resting ? "" : undefined}
|
||||
className="absolute inset-0 flex cursor-pointer items-center justify-center opacity-0 transition-opacity group-hover:bg-overlay/40 group-hover:opacity-100 focus-visible:bg-overlay/40 focus-visible:opacity-100 data-resting:items-end data-resting:pb-4 data-resting:opacity-100"
|
||||
aria-label={t`Open`}
|
||||
onClick={onOpen}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 rounded-full bg-overlay px-3.5 py-2 text-ui-body text-foreground shadow-md">
|
||||
<Maximize2 strokeWidth={1.9} aria-hidden className="size-icon-md" />
|
||||
<Trans>Open</Trans>
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-2 truncate text-ui-body text-muted-foreground" dir="auto">
|
||||
{t`${active.name}'s screen`}
|
||||
</p>
|
||||
<WorkspaceFilesView botId={active.id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -108,14 +108,20 @@ export function NewSpaceDialog({
|
||||
<Dialog open onOpenChange={closeUnlessBusy(saving, onCancel)}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className={isProject ? "max-h-[90vh] overflow-y-auto max-w-xl" : undefined}
|
||||
className={
|
||||
isProject
|
||||
? "flex max-h-[88dvh] w-[calc(100vw-2rem)] max-w-xl flex-col gap-0 overflow-hidden p-0 sm:max-w-xl"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2.5">
|
||||
<DialogHeader
|
||||
className={isProject ? "shrink-0 border-b border-border px-5 py-4" : undefined}
|
||||
>
|
||||
<DialogTitle className="flex min-w-0 items-center gap-2.5">
|
||||
{isProject ? (
|
||||
<>
|
||||
<ProjectIcon icon={icon} color={color} size={30} />
|
||||
<span>
|
||||
<span className="truncate">
|
||||
<Trans>New project</Trans>
|
||||
</span>
|
||||
</>
|
||||
@ -133,7 +139,13 @@ export function NewSpaceDialog({
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 pt-1">
|
||||
<div
|
||||
className={
|
||||
isProject
|
||||
? "rk-scroll min-h-0 min-w-0 flex-1 space-y-4 overflow-y-auto px-5 py-4"
|
||||
: "min-w-0 space-y-4 pt-1"
|
||||
}
|
||||
>
|
||||
<label htmlFor={nameId} className="block text-ui-body text-foreground/75">
|
||||
<Trans>Name</Trans>
|
||||
<Input
|
||||
@ -179,7 +191,7 @@ export function NewSpaceDialog({
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={t`Icon`}
|
||||
className="grid max-h-36 grid-cols-8 gap-1 overflow-y-auto rounded-lg border border-border p-1"
|
||||
className="grid max-h-36 grid-cols-6 gap-1 overflow-y-auto rounded-lg border border-border p-1.5 sm:grid-cols-8"
|
||||
>
|
||||
{PROJECT_ICONS.map((option) => {
|
||||
const Glyph = PROJECT_ICON_COMPONENTS[option.id];
|
||||
@ -304,8 +316,12 @@ export function NewSpaceDialog({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null}
|
||||
<DialogFooter className="mt-4">
|
||||
{error ? (
|
||||
<p className={`text-ui-body text-destructive ${isProject ? "px-5 pt-2" : "mt-2"}`}>
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<DialogFooter className={isProject ? "mx-0 mb-0 mt-0 shrink-0 px-5 py-3.5" : "mt-4"}>
|
||||
<Button variant="outline" disabled={saving} onClick={onCancel}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@ -84,11 +84,11 @@ export function FullscreenComputerOverlay({
|
||||
)}
|
||||
{!recordingSkill && hasControl ? (
|
||||
computer?.takeoverRequested ? (
|
||||
<span className="rounded-full bg-warning/15 px-[11px] py-1 text-ui-body-sm text-warning">
|
||||
<span className="shrink-0 whitespace-nowrap rounded-full bg-warning/15 px-[11px] py-1 text-ui-body-sm text-warning">
|
||||
<Trans>Needs you</Trans>
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-full bg-success/15 px-[11px] py-1 text-ui-body-sm text-success">
|
||||
<span className="shrink-0 whitespace-nowrap rounded-full bg-success/15 px-[11px] py-1 text-ui-body-sm text-success">
|
||||
<Trans>You have control</Trans>
|
||||
</span>
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -95,16 +95,16 @@ export function ProjectLookDialog({
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
data-testid="project-look-dialog"
|
||||
className="max-h-[90vh] overflow-y-auto max-w-xl"
|
||||
className="flex max-h-[88dvh] w-[calc(100vw-2rem)] max-w-xl flex-col gap-0 overflow-hidden p-0 sm:max-w-xl"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-3">
|
||||
<DialogHeader className="shrink-0 border-b border-border px-5 py-4">
|
||||
<DialogTitle className="flex min-w-0 items-center gap-3">
|
||||
<ProjectIcon icon={icon} color={color} size={36} />
|
||||
<span className="min-w-0 truncate">{name.trim() || initial.name}</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 pt-1">
|
||||
<div className="rk-scroll min-h-0 min-w-0 flex-1 space-y-4 overflow-y-auto px-5 py-4">
|
||||
<label htmlFor={nameId} className="block text-ui-body text-foreground/75">
|
||||
<Trans>Name</Trans>
|
||||
<Input
|
||||
@ -147,7 +147,7 @@ export function ProjectLookDialog({
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={t`Icon`}
|
||||
className="grid max-h-40 grid-cols-8 gap-1 overflow-y-auto rounded-lg border border-border p-1"
|
||||
className="grid max-h-40 grid-cols-6 gap-1 overflow-y-auto rounded-lg border border-border p-1.5 sm:grid-cols-8"
|
||||
>
|
||||
{PROJECT_ICONS.map((option) => {
|
||||
const Glyph = PROJECT_ICON_COMPONENTS[option.id];
|
||||
@ -243,8 +243,8 @@ export function ProjectLookDialog({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null}
|
||||
<DialogFooter className="mt-4">
|
||||
{error ? <p className="px-5 pt-2 text-ui-body text-destructive">{error}</p> : null}
|
||||
<DialogFooter className="mx-0 mb-0 mt-0 shrink-0 px-5 py-3.5">
|
||||
<Button variant="outline" disabled={saving} onClick={onCancel}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user