Merge remote main into local

This commit is contained in:
Cynar 2026-10-23 13:30:27 +00:00
commit f2841bdd74
265 changed files with 18959 additions and 4702 deletions

View File

@ -477,6 +477,9 @@ jobs:
eas-version: latest eas-version: latest
packager: pnpm packager: pnpm
token: ${{ secrets.EXPO_TOKEN }} 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 - name: Install dependencies
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true' if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@ -29,6 +29,7 @@ import {
createMediaRuntime, createMediaRuntime,
createMessagingContextLoader, createMessagingContextLoader,
createMessagingTeamChatSender, createMessagingTeamChatSender,
createPostgresReconciliationLeadership,
createProfileConsolidationScheduler, createProfileConsolidationScheduler,
createRunExecutor, createRunExecutor,
createRunSandbox, createRunSandbox,
@ -360,7 +361,7 @@ export async function createApp(
"https://172.*:*", "https://172.*:*",
"http://*.local:*", "http://*.local:*",
"https://*.local:*", "https://*.local:*",
...(process.env.EXTRA_TRUSTED_ORIGINS?.split(",").map((s) => s.trim()) ?? []), ...(env.extraTrustedOrigins ?? []),
], ],
beforeDeleteUser: async (userId) => { beforeDeleteUser: async (userId) => {
const bots = await prisma.bot.findMany({ const bots = await prisma.bot.findMany({
@ -474,19 +475,23 @@ export async function createApp(
cloudAgent, cloudAgent,
media, media,
}); });
let graphileWorkerHost: GraphileJobWorkerHost | undefined;
if (inMemoryJobs) { if (inMemoryJobs) {
await inMemoryJobs.start(jobHandlers); await inMemoryJobs.start(jobHandlers);
} else if (jobPool) { } else if (jobPool) {
const graphileWorkerHost = new GraphileJobWorkerHost(jobPool, { graphileWorkerHost = new GraphileJobWorkerHost(jobPool, {
concurrency: 4, concurrency: parsePositiveInteger(process.env.GRAPHILE_WORKER_CONCURRENCY, 4),
pollInterval: 500, pollInterval: 500,
}); });
await graphileWorkerHost.start(jobHandlers); await graphileWorkerHost.start(jobHandlers);
} }
const reconciler = inMemoryJobs const reconciler =
inMemoryJobs || !prismaOverride
? createJobReconciler({ ? createJobReconciler({
prisma, prisma,
jobs, jobs,
events,
leadership: jobPool ? createPostgresReconciliationLeadership(jobPool) : undefined,
reconcileCloudAgents: () => reconcileCloudAgents({ prisma, jobs, cloudAgent }), reconcileCloudAgents: () => reconcileCloudAgents({ prisma, jobs, cloudAgent }),
reconcileComputerUpdates: () => reconcileComputerUpdates({ prisma, jobs }), reconcileComputerUpdates: () => reconcileComputerUpdates({ prisma, jobs }),
reconcileProfileConsolidation: createProfileConsolidationScheduler({ prisma, jobs }), reconcileProfileConsolidation: createProfileConsolidationScheduler({ prisma, jobs }),
@ -612,7 +617,7 @@ export async function createApp(
actor, actor,
signal: c.req.raw.signal, signal: c.req.raw.signal,
userAgent: c.req.header("user-agent"), 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); if (matched) return c.newResponse(response.body, response);
@ -1006,6 +1011,7 @@ export async function createApp(
await teamChatBridge?.stop(); await teamChatBridge?.stop();
await email?.drain?.(); await email?.drain?.();
await reconciler?.stop(); await reconciler?.stop();
await graphileWorkerHost?.stop();
await jobs.close(); await jobs.close();
await realtime.close(); await realtime.close();
await connector.stop(); await connector.stop();
@ -1021,6 +1027,7 @@ export async function createApp(
function isTrustedOrigin(origin: string, env: AppEnv) { function isTrustedOrigin(origin: string, env: AppEnv) {
if (!origin) return true; if (!origin) return true;
if (origin === env.webOrigin || origin === env.apiUrl || origin === env.authUrl) 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; if (origin.startsWith("rakazo://") || origin.startsWith("exp://")) return true;
try { try {
const host = new URL(origin).hostname; const host = new URL(origin).hostname;

View File

@ -20,6 +20,7 @@ export interface AppEnv {
authSecret: string; authSecret: string;
authUrl: string; authUrl: string;
webOrigin: string; webOrigin: string;
extraTrustedOrigins: string[];
privacyPolicyUrl?: string; privacyPolicyUrl?: string;
apiUrl: string; apiUrl: string;
apiHost: string; apiHost: string;
@ -109,6 +110,10 @@ export function loadEnv(source: NodeJS.ProcessEnv = process.env): AppEnv {
authSecret, authSecret,
authUrl: source.BETTER_AUTH_URL ?? source.WEB_ORIGIN ?? "http://127.0.0.1:5173", authUrl: source.BETTER_AUTH_URL ?? source.WEB_ORIGIN ?? "http://127.0.0.1:5173",
webOrigin: 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), privacyPolicyUrl: optional(source.PRIVACY_POLICY_URL),
apiUrl: source.API_URL ?? "http://127.0.0.1:3100", apiUrl: source.API_URL ?? "http://127.0.0.1:3100",
apiHost: source.API_HOST ?? "127.0.0.1", apiHost: source.API_HOST ?? "127.0.0.1",

View File

@ -135,6 +135,7 @@ import {
createProjectGoalRepos, createProjectGoalRepos,
createRepos, createRepos,
createSpaceForMember, createSpaceForMember,
DuplicateSpaceNameError,
deleteEmptySpaceForMember, deleteEmptySpaceForMember,
deleteUnreferencedCredentialSecret, deleteUnreferencedCredentialSecret,
ensureMainConversation, ensureMainConversation,
@ -270,7 +271,10 @@ import {
followUpGroupThreadMessage, followUpGroupThreadMessage,
groupDefaultBotId, groupDefaultBotId,
reactToThreadMessage, reactToThreadMessage,
removeQueuedMessage,
resolveThreadTarget, resolveThreadTarget,
resumeThreadRuns,
sendQueuedThreadMessagesNow,
sendThreadMessage, sendThreadMessage,
setThreadUnreadState, setThreadUnreadState,
stopThreadRuns, stopThreadRuns,
@ -783,6 +787,16 @@ export function createRouter(deps: RouterDeps) {
const goal = const goal =
input.goal?.trim() || (workspacePath ? await readProjectSummary(workspacePath) : null); input.goal?.trim() || (workspacePath ? await readProjectSummary(workspacePath) : null);
if (goal) { if (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({ await deps.prisma.memoryDocument.create({
data: { data: {
spaceId: space.id, spaceId: space.id,
@ -793,6 +807,7 @@ export function createRouter(deps: RouterDeps) {
}, },
}); });
} }
}
// A project starts as a conversation with its own main agent. // A project starts as a conversation with its own main agent.
await ensureMainConversation(deps.prisma, context.actor.userId, space.id); await ensureMainConversation(deps.prisma, context.actor.userId, space.id);
const spaceActor = await requireMembership(deps.prisma, context.actor.userId, space.id); const spaceActor = await requireMembership(deps.prisma, context.actor.userId, space.id);
@ -873,7 +888,7 @@ export function createRouter(deps: RouterDeps) {
throw new ORPCError("NOT_FOUND", { message: error.message }); throw new ORPCError("NOT_FOUND", { message: error.message });
} }
if (error instanceof IsolationError) throw new ORPCError("FORBIDDEN"); 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 new ORPCError("BAD_REQUEST", { message: error.message });
} }
throw error; throw error;
@ -2050,6 +2065,42 @@ export function createRouter(deps: RouterDeps) {
await stopThreadRuns(deps, context.actor, target); await stopThreadRuns(deps, context.actor, target);
return { ok: true as const }; 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 }) => { clear: authed.threads.clear.handler(async ({ context, input }) => {
const target = await resolveThreadTarget(deps.prisma, context.actor, input); const target = await resolveThreadTarget(deps.prisma, context.actor, input);
const contextBotId = target.kind === "bot" ? target.botId : groupDefaultBotId(target); 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. // The user's own main agent of a project is part of the project, not content to clear.
deps.prisma.bot.findMany({ deps.prisma.bot.findMany({
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain }, where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
select: { spaceId: true }, select: { spaceId: true, userId: true },
distinct: ["spaceId"], distinct: ["spaceId", "userId"],
}), }),
deps.prisma.chatGroup.findMany({ deps.prisma.chatGroup.findMany({
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain }, where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
select: { spaceId: true }, select: { spaceId: true, userId: true },
distinct: ["spaceId"], distinct: ["spaceId", "userId"],
}), }),
deps.prisma.memoryDocument.findMany({ deps.prisma.memoryDocument.findMany({
where: { spaceId: { in: spaceIds }, scope: "user", path: "PROJECT_GOAL.md" }, where: { spaceId: { in: spaceIds }, scope: "user", path: "PROJECT_GOAL.md" },
@ -5640,6 +5691,11 @@ async function spaceNavigationDto(
...contentBots.map((row) => row.spaceId), ...contentBots.map((row) => row.spaceId),
...contentGroups.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 goalBySpaceId = new Map(goalDocuments.map((doc) => [doc.spaceId, doc.content]));
const currentMembership = memberships.find((membership) => membership.spaceId === actor.spaceId); const currentMembership = memberships.find((membership) => membership.spaceId === actor.spaceId);
if (!currentMembership) throw new IsolationError(); if (!currentMembership) throw new IsolationError();
@ -5676,7 +5732,7 @@ async function spaceNavigationDto(
membership.role === "owner" && membership.role === "owner" &&
!membership.space.isDefault && !membership.space.isDefault &&
memberships.length > 1 && memberships.length > 1 &&
!spacesWithContent.has(membership.spaceId) && !spacesBlockedByOtherMember.has(membership.spaceId) &&
(membership.space.deletingAt === null || membership.space.deletingAt < staleClaimBefore), (membership.space.deletingAt === null || membership.space.deletingAt < staleClaimBefore),
canEdit: membership.role === "owner", canEdit: membership.role === "owner",
// A linked folder is on the owner's host; other members see neither its path nor remote. // A linked folder is on the owner's host; other members see neither its path nor remote.

View File

@ -1,6 +1,7 @@
import type { Actor, MessageBlock, SearchHit } from "@rakazo/contracts"; import type { Actor, MessageBlock, SearchHit } from "@rakazo/contracts";
import { extractLinksFromText, matchesSearchQuery, snippetAroundMatch } from "@rakazo/core"; 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; const SEARCH_LIMIT = 25;
/** Cap name matches so content hits (messages/files/links/routines) keep most of the budget. */ /** Cap name matches so content hits (messages/files/links/routines) keep most of the budget. */
@ -64,7 +65,8 @@ export async function querySpaceSearch(
bucket.push(hit); bucket.push(hit);
} }
const bots = await prisma.bot.findMany({ const [bots, groups, artifacts, groupArtifacts, routines] = await Promise.all([
prisma.bot.findMany({
where: { where: {
spaceId: actor.spaceId, spaceId: actor.spaceId,
userId: actor.userId, userId: actor.userId,
@ -76,7 +78,52 @@ export async function querySpaceSearch(
], ],
}, },
take: CONVERSATION_HIT_LIMIT, 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) { for (const bot of bots) {
pushInto( pushInto(
conversationHits, 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) { for (const group of groups) {
pushInto( pushInto(
conversationHits, conversationHits,
@ -114,27 +153,72 @@ export async function querySpaceSearch(
} }
const contentBudget = Math.max(0, SEARCH_LIMIT - conversationHits.length); const contentBudget = Math.max(0, SEARCH_LIMIT - conversationHits.length);
const pattern = `%${query}%`;
const artifacts = await prisma.artifact.findMany({ const [botArtifactMessages, groupArtifactMessages, messageRows, groupMessageRows] =
where: { await Promise.all([
spaceId: actor.spaceId, findArtifactMessages(
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, prisma,
actor, actor,
artifacts.flatMap((artifact) => artifacts.flatMap((artifact) =>
artifact.botId && artifact.bot ? [{ id: artifact.id, targetId: artifact.botId }] : [], artifact.botId && artifact.bot ? [{ id: artifact.id, targetId: artifact.botId }] : [],
), ),
"bot", "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}
`,
]);
for (const artifact of artifacts) { for (const artifact of artifacts) {
if (!artifact.botId || !artifact.bot) continue; if (!artifact.botId || !artifact.bot) continue;
const message = botArtifactMessages.get(artifact.id); 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) { for (const artifact of groupArtifacts) {
if (!artifact.groupId || !artifact.group) continue; if (!artifact.groupId || !artifact.group) continue;
const message = groupArtifactMessages.get(artifact.id); 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) { for (const routine of routines) {
pushInto( pushInto(
contentHits, 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) { for (const row of messageRows) {
pushMessageHits(row.blocks as MessageBlock[], { pushMessageHits(row.blocks as MessageBlock[], {
botId: row.botId, 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) { for (const row of groupMessageRows) {
pushMessageHits(row.blocks as MessageBlock[], { pushMessageHits(row.blocks as MessageBlock[], {
groupId: row.groupId, groupId: row.groupId,

View File

@ -88,4 +88,77 @@ describe("model team chat engagement judge", () => {
expect(instructions).toContain("(Europe/Berlin, the user's time zone)"); expect(instructions).toContain("(Europe/Berlin, the user's time zone)");
expect(instructions).toContain("low-cost engagement judge"); 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",
});
});
}); });

View File

@ -1,14 +1,15 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { AgentModelOAuthCredential, AgentRuntime } from "@rakazo/adapter-kit"; import type { AgentModelOAuthCredential, AgentRuntime } from "@rakazo/adapter-kit";
import type { EncryptedSecretStore } from "@rakazo/adapters";
import { import {
type EncryptedSecretStore,
formatCurrentTimeInstruction, formatCurrentTimeInstruction,
loadUserTimeZone, loadUserTimeZone,
resolveModelAuth, resolveModelAuth,
serializeModelSecret, serializeModelSecret,
toOAuthCredential, toOAuthCredential,
} from "@rakazo/adapters"; } 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"; import { getLogger } from "@rakazo/logging";
const MAX_RULES_CHARS = 4_000; const MAX_RULES_CHARS = 4_000;
@ -191,27 +192,39 @@ export class ModelTeamChatEngagementJudge implements TeamChatEngagementJudge {
}; };
}; };
} | null> { } | null> {
const settings = await this.deps.prisma.deploymentSettings.findUnique({
where: { id: "default" },
});
const scope = { userId: bot.userId, spaceId: bot.spaceId }; 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 requestedProvider = this.deps.providerOverride ?? bot.modelProvider;
const requestedModel = this.deps.modelOverride ?? bot.modelId; const requestedModel = this.deps.modelOverride ?? bot.modelId;
const overrideCredential = requestedProvider const overrideCredential =
!fastTier && requestedProvider
? await findModelCredential(this.deps.prisma, scope, requestedProvider) ? await findModelCredential(this.deps.prisma, scope, requestedProvider)
: null; : null;
const explicitJudgeOverride = Boolean(this.deps.providerOverride && this.deps.modelOverride);
const useOverride = Boolean( const useOverride = Boolean(
requestedProvider && requestedModel && (explicitJudgeOverride || overrideCredential), 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 = const provider =
fastTier?.credential.provider ??
(useOverride ? requestedProvider : null) ?? (useOverride ? requestedProvider : null) ??
credential?.provider ?? credential?.provider ??
settings?.defaultModelProvider ?? settings?.defaultModelProvider ??
this.deps.deploymentProvider; this.deps.deploymentProvider;
const modelId = const modelId =
fastTier?.modelId ??
(useOverride ? requestedModel : null) ?? (useOverride ? requestedModel : null) ??
credential?.defaultModel ?? credential?.defaultModel ??
settings?.defaultModelId ?? settings?.defaultModelId ??

View File

@ -2,15 +2,17 @@ import type { SandboxProvider } from "@rakazo/adapter-kit";
import type { Actor } from "@rakazo/contracts"; import type { Actor } from "@rakazo/contracts";
import type { PrismaClient } from "@rakazo/db"; import type { PrismaClient } from "@rakazo/db";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { ThreadTarget } from "./thread-target.js";
import { import {
cancelSupersededQueuedRuns, cancelSupersededQueuedRuns,
followUpGroupThreadMessage, followUpGroupThreadMessage,
groupDefaultBotId, groupDefaultBotId,
reactToThreadMessage, reactToThreadMessage,
resolveGroupFollowUpBotId, resolveGroupFollowUpBotId,
resumeThreadRuns,
sendQueuedThreadMessagesNow,
sendThreadMessage, sendThreadMessage,
stopThreadRuns, stopThreadRuns,
type ThreadTarget,
threadHead, threadHead,
threadSnapshot, threadSnapshot,
} from "./thread-target.js"; } from "./thread-target.js";
@ -2118,4 +2120,192 @@ describe("stopThreadRuns", () => {
expect.objectContaining({ runId: "run-b" }), 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);
});
}); });

View File

@ -1,5 +1,10 @@
import { ORPCError } from "@orpc/server"; 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 { cancelComputerRunWork, screenLeaseIdForRun, toComputerRef } from "@rakazo/adapters";
import { import {
type Actor, type Actor,
@ -1151,51 +1156,27 @@ export async function reactToThreadMessage(
}); });
} }
export async function stopThreadRuns( async function snapshotCancelledRunTeardown(tx: Prisma.TransactionClient, runIds: string[]) {
deps: {
prisma: PrismaClient;
sandbox: SandboxProvider;
},
actor: Actor,
target: ThreadTarget,
) {
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 // Snapshot teardown coordinates before commit. Once cancellation becomes
// visible, a worker can release its lease / execution columns immediately; a // visible, a worker can release its lease / execution columns immediately; a
// later lookup would then miss the sandbox work this request must stop. // later lookup would then miss the sandbox work this request must stop.
// Team ownership lives on ComputerExecutionLease; Computer.executionRunId is // Team ownership lives on ComputerExecutionLease; Computer.executionRunId is
// only a legacy secondary path and is not written by current acquisition. // only a legacy secondary path and is not written by current acquisition.
const leases = ids.length const leases = runIds.length
? await tx.computerExecutionLease.findMany({ ? await tx.computerExecutionLease.findMany({
where: { runId: { in: ids } }, where: { runId: { in: runIds } },
select: { computerId: true, botId: true, runId: true, fence: true }, select: { computerId: true, botId: true, runId: true, fence: true },
}) })
: []; : [];
const leaseComputerIds = [...new Set(leases.map((lease) => lease.computerId))]; const leaseComputerIds = [...new Set(leases.map((lease) => lease.computerId))];
const computers = ids.length const computers = runIds.length
? await tx.computer.findMany({ ? await tx.computer.findMany({
where: where:
leaseComputerIds.length > 0 leaseComputerIds.length > 0
? { ? {
OR: [{ id: { in: leaseComputerIds } }, { executionRunId: { in: ids } }], OR: [{ id: { in: leaseComputerIds } }, { executionRunId: { in: runIds } }],
} }
: { executionRunId: { in: ids } }, : { executionRunId: { in: runIds } },
select: { select: {
id: true, id: true,
homeKey: true, homeKey: true,
@ -1206,8 +1187,19 @@ export async function stopThreadRuns(
}, },
}) })
: []; : [];
return { runIds: ids, computers, leases }; return { runIds, computers, leases };
}); }
async function teardownCancelledRuns(
deps: {
prisma: PrismaClient;
sandbox: SandboxProvider;
},
actor: Actor,
snapshot: Awaited<ReturnType<typeof snapshotCancelledRunTeardown>>,
) {
const { runIds, computers, leases } = snapshot;
if (runIds.length === 0) return;
// Keep the DB lease until after teardown so a replacement run cannot claim the // 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. // screen while we still need the cancelled run's screenLeaseId to release it.
const computerById = new Map(computers.map((computer) => [computer.id, computer])); 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( export async function setThreadUnreadState(
prisma: PrismaClient, prisma: PrismaClient,
actor: Actor, actor: Actor,

View File

@ -147,7 +147,11 @@ export async function persistVoiceCredential(
let voiceId = input.voiceId?.trim() ?? ""; let voiceId = input.voiceId?.trim() ?? "";
if (!voiceId) { if (!voiceId) {
const voices = await provider.listVoices(input.plaintext, voiceContext(actor, input.signal)); 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 stored = await deps.secrets.put(input.plaintext, voiceContext(actor, input.signal));
const cred = await withSerializableRetry(() => const cred = await withSerializableRetry(() =>

View File

@ -25,4 +25,12 @@ describe("requestWebOrigin", () => {
"https://evoo.example", "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");
});
}); });

View File

@ -14,15 +14,20 @@ export function isLoopbackHost(host: string): boolean {
} }
/** The web origin that browser-facing URLs (OAuth returns, screen links) should /** 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 * point at for this request. A browser opened over a loopback, private-network,
* address gets its own origin back so a phone on the LAN is not sent to * or explicitly trusted origin gets its own origin back so a phone on the LAN
* 127.0.0.1; any other caller gets the configured WEB_ORIGIN. */ * or Tailscale is not sent to 127.0.0.1; any other caller gets WEB_ORIGIN. */
export function requestWebOrigin(origin: string | undefined, webOrigin: string): string { export function requestWebOrigin(
origin: string | undefined,
webOrigin: string,
extraTrustedOrigins: readonly string[] = [],
): string {
if (!origin) return webOrigin; if (!origin) return webOrigin;
try { try {
const url = new URL(origin); const url = new URL(origin);
if (url.protocol !== "http:" && url.protocol !== "https:") return webOrigin; if (url.protocol !== "http:" && url.protocol !== "https:") return webOrigin;
if (url.origin === new URL(webOrigin).origin) return url.origin; 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; return isPrivateOrLocalHost(url.hostname) ? url.origin : webOrigin;
} catch { } catch {
return webOrigin; return webOrigin;

View File

@ -3,8 +3,15 @@ import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Alert, Pressable, ScrollView, Text, TextInput } from "react-native"; import { Alert, Pressable, ScrollView, Text, TextInput } from "react-native";
import { BotMemberPicker } from "../components/bot-member-picker"; 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 { useI18n } from "../lib/i18n";
import { canDeleteInboxSpace, removeInboxSpace } from "../lib/inbox-spaces";
import { useMobileTokens } from "../lib/native"; import { useMobileTokens } from "../lib/native";
export default function GroupSettingsScreen() { export default function GroupSettingsScreen() {
@ -13,12 +20,16 @@ export default function GroupSettingsScreen() {
const router = useRouter(); const router = useRouter();
const { groupId } = useLocalSearchParams<{ groupId: string }>(); const { groupId } = useLocalSearchParams<{ groupId: string }>();
const [group, setGroup] = useState<MobileGroup | null>(null); const [group, setGroup] = useState<MobileGroup | null>(null);
const [projectSpace, setProjectSpace] = useState<MobileSpace | null>(null);
const [bots, setBots] = useState<MobileBot[]>([]); const [bots, setBots] = useState<MobileBot[]>([]);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [selected, setSelected] = useState<string[]>([]); const [selected, setSelected] = useState<string[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
const isMainConversation = group?.kind === "main";
const isProjectMain = isMainConversation && projectSpace !== null && !projectSpace.isDefault;
useEffect(() => { useEffect(() => {
if (!groupId) return; if (!groupId) return;
void Promise.all([ void Promise.all([
@ -26,11 +37,19 @@ export default function GroupSettingsScreen() {
(groups) => groups.find((row) => row.id === groupId) ?? null, (groups) => groups.find((row) => row.id === groupId) ?? null,
), ),
rpc<MobileBot[]>("bots/list"), 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")); if (!nextGroup) throw new Error(t("Group not found"));
setGroup(nextGroup); 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)); setSelected(nextGroup.members.map((member) => member.botId));
setBots(nextBots.filter((bot) => !bot.archivedAt)); setBots(nextBots.filter((bot) => !bot.archivedAt));
}) })
@ -42,10 +61,20 @@ export default function GroupSettingsScreen() {
setPending(true); setPending(true);
setError(null); setError(null);
try { 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 }; const input: { groupId: string; name?: string; botIds?: string[] } = { groupId };
if (name.trim() !== group.name) input.name = name.trim(); if (trimmed !== group.name) input.name = trimmed;
if (!isMainConversation) {
const memberIds = group.members.map((member) => member.botId).join(","); const memberIds = group.members.map((member) => member.botId).join(",");
if (selected.join(",") !== memberIds) input.botIds = selected; if (selected.join(",") !== memberIds) input.botIds = selected;
}
if (input.name || input.botIds) await rpc("groups/update", input); if (input.name || input.botIds) await rpc("groups/update", input);
router.back(); router.back();
} catch (err) { } catch (err) {
@ -57,6 +86,29 @@ export default function GroupSettingsScreen() {
function remove() { function remove() {
if (!groupId || !group) return; 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."), [ Alert.alert(group.name, t("Delete this group? Bots and their solo threads are kept."), [
{ text: t("Cancel"), style: "cancel" }, { 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 ( return (
<> <>
<Stack.Screen options={{ title: t("Group settings") }} /> <Stack.Screen options={{ title: t("Group settings") }} />
@ -97,6 +155,8 @@ export default function GroupSettingsScreen() {
fontSize: 16, fontSize: 16,
}} }}
/> />
{!isMainConversation ? (
<>
<Text style={{ color: tokens.mutedForeground, fontSize: 14, marginTop: 20 }}> <Text style={{ color: tokens.mutedForeground, fontSize: 14, marginTop: 20 }}>
{t("Members")} {t("Members")}
</Text> </Text>
@ -106,25 +166,16 @@ export default function GroupSettingsScreen() {
onChange={setSelected} onChange={setSelected}
disabled={pending} disabled={pending}
/> />
</>
) : null}
{error ? <Text style={{ color: tokens.destructive, marginTop: 12 }}>{error}</Text> : null} {error ? <Text style={{ color: tokens.destructive, marginTop: 12 }}>{error}</Text> : null}
<Pressable <Pressable
onPress={() => void save()} onPress={() => void save()}
disabled={ disabled={saveDisabled}
!name.trim() ||
selected.length < GROUP_MEMBER_MIN ||
selected.length > GROUP_MEMBER_MAX ||
pending
}
style={{ style={{
marginTop: 24, marginTop: 24,
backgroundColor: tokens.primary, backgroundColor: tokens.primary,
opacity: opacity: saveDisabled ? 0.5 : 1,
!name.trim() ||
selected.length < GROUP_MEMBER_MIN ||
selected.length > GROUP_MEMBER_MAX ||
pending
? 0.5
: 1,
borderRadius: 11, borderRadius: 11,
padding: 14, padding: 14,
alignItems: "center", alignItems: "center",
@ -134,6 +185,21 @@ export default function GroupSettingsScreen() {
{pending ? t("Saving…") : t("Save")} {pending ? t("Saving…") : t("Save")}
</Text> </Text>
</Pressable> </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 <Pressable
onPress={remove} onPress={remove}
style={{ style={{
@ -147,6 +213,7 @@ export default function GroupSettingsScreen() {
> >
<Text style={{ color: tokens.destructive, fontSize: 16 }}>{t("Delete group")}</Text> <Text style={{ color: tokens.destructive, fontSize: 16 }}>{t("Delete group")}</Text>
</Pressable> </Pressable>
) : null}
</ScrollView> </ScrollView>
</> </>
); );

View File

@ -597,7 +597,21 @@ export default function Home() {
}} }}
disabled={spaceBusy} disabled={spaceBusy}
onPress={() => { 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]} style={({ pressed }) => [styles.spaceSelect, pressed && styles.rowPressed]}
> >

View File

@ -6,22 +6,19 @@ import type {
Connection, Connection,
ConnectionCatalogItem, ConnectionCatalogItem,
MessageBlock, MessageBlock,
MessageReaction,
Routine, Routine,
} from "@rakazo/contracts"; } from "@rakazo/contracts";
import { import { AUTOPILOT_LIMITS, canReactToThreadMessage, MESSAGE_REACTIONS } from "@rakazo/contracts";
AUTOPILOT_LIMITS, import type { AutopilotUntilChoice, ComposerMention, SlashActionId } from "@rakazo/core";
canReactToThreadMessage,
MESSAGE_REACTIONS,
type MessageReaction,
} from "@rakazo/contracts";
import type { AutopilotUntilChoice } from "@rakazo/core";
import { import {
abortableDelay, abortableDelay,
attachmentsForThread, attachmentsForThread,
autopilotUntil, autopilotUntil,
buildComposerMentionOptions, buildComposerMentionOptions,
type ComposerMention, clearComposerDraft,
cloudAgentHttpsUrl, cloudAgentHttpsUrl,
composerDraftThreadKey,
deriveWorkspaceFocus, deriveWorkspaceFocus,
foldRunNarration, foldRunNarration,
isApprovalAskBlock, isApprovalAskBlock,
@ -32,13 +29,15 @@ import {
liveScreenRunId, liveScreenRunId,
mentionChipKey, mentionChipKey,
projectMessageReactions, projectMessageReactions,
queuedFollowUps,
readComposerDraft,
resolveComposerSendPlan, resolveComposerSendPlan,
SLASH_ACTIONS, SLASH_ACTIONS,
type SlashActionId,
selectedAskActionLabel, selectedAskActionLabel,
serializeComposerPrompt, serializeComposerPrompt,
truncateSlashDescription, truncateSlashDescription,
userVisibleMessages, userVisibleMessages,
writeComposerDraft,
} from "@rakazo/core"; } from "@rakazo/core";
import * as Clipboard from "expo-clipboard"; import * as Clipboard from "expo-clipboard";
import { useFocusEffect, useLocalSearchParams, useNavigation, useRouter } from "expo-router"; import { useFocusEffect, useLocalSearchParams, useNavigation, useRouter } from "expo-router";
@ -289,6 +288,8 @@ function Thread() {
const mentionBotsAppliedGeneration = useRef(0); const mentionBotsAppliedGeneration = useRef(0);
const readVisibleTarget = useRef<string | null>(null); const readVisibleTarget = useRef<string | null>(null);
const threadKey = groupId ?? botId; const threadKey = groupId ?? botId;
const draftKey = composerDraftThreadKey({ botId, groupId });
const initialDraft = useMemo(() => readComposerDraft(draftKey), [draftKey]);
const [threadScrollState, setThreadScrollState] = useState<ThreadScrollState>(() => const [threadScrollState, setThreadScrollState] = useState<ThreadScrollState>(() =>
scrollBehavior.current.state(), 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. */ /** 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 [screenRunId, setScreenRunId] = useState<string | null>(null);
const activeThreadId = useRef<string | undefined>(undefined); const activeThreadId = useRef<string | undefined>(undefined);
const [draft, setDraft] = useState(""); const [draft, setDraft] = useState(() => initialDraft.text);
const [mentionQuery, setMentionQuery] = useState<string | null>(null); const [mentionQuery, setMentionQuery] = useState<string | null>(null);
const [slashQuery, setSlashQuery] = useState<string | null>(null); const [slashQuery, setSlashQuery] = useState<string | null>(null);
const [agentSkills, setAgentSkills] = useState<AgentSkillCatalogEntry[]>([]); const [agentSkills, setAgentSkills] = useState<AgentSkillCatalogEntry[]>([]);
@ -325,12 +326,17 @@ function Thread() {
connectionId?: string; connectionId?: string;
}> }>
>([]); >([]);
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>([]); const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>(
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(null); () => initialDraft.selectedMentions,
);
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(
() => initialDraft.selectedSkill,
);
const [pendingAttachments, setPendingAttachments] = useState<PendingAttachment[]>([]); const [pendingAttachments, setPendingAttachments] = useState<PendingAttachment[]>([]);
const [replyTarget, setReplyTarget] = useState<MobileMessage | null>(null); const [replyTarget, setReplyTarget] = useState<MobileMessage | null>(null);
const [attachmentNotice, setAttachmentNotice] = useState<string | null>(null); const [attachmentNotice, setAttachmentNotice] = useState<string | null>(null);
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [stopped, setStopped] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loadingOlder, setLoadingOlder] = useState(false); const [loadingOlder, setLoadingOlder] = useState(false);
const [markdownPreview, setMarkdownPreview] = useState<MarkdownArtifactPreviewTarget | null>( const [markdownPreview, setMarkdownPreview] = useState<MarkdownArtifactPreviewTarget | null>(
@ -353,6 +359,9 @@ function Thread() {
() => reactionView.visibleMessages.filter((message) => !runNarration.hidden.has(message.id)), () => reactionView.visibleMessages.filter((message) => !runNarration.hidden.has(message.id)),
[reactionView.visibleMessages, runNarration], [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 latestMessageId = visibleMessages.at(-1)?.id ?? null;
const activePendingAttachments = attachmentsForThread(pendingAttachments, threadKey); const activePendingAttachments = attachmentsForThread(pendingAttachments, threadKey);
const composerMentionTargets = useMemo( const composerMentionTargets = useMemo(
@ -431,6 +440,106 @@ function Thread() {
}); });
}, [inGroup, snap?.activeRuns, snap?.members, snap?.run]); }, [inGroup, snap?.activeRuns, snap?.members, snap?.run]);
const working = inGroup ? workingGroupBots.length > 0 : isWorkingStatus(currentBotStatus); 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(() => { useEffect(() => {
void rpc<AgentSkillCatalogEntry[]>("agentSkills/list") void rpc<AgentSkillCatalogEntry[]>("agentSkills/list")
@ -1002,16 +1111,31 @@ function Thread() {
); );
}, [botId, groupId, messageId]); }, [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(() => { useEffect(() => {
setPendingAttachments((current) => attachmentsForThread(current, threadKey)); setPendingAttachments((current) => attachmentsForThread(current, threadKey));
setDraft("");
setMentionQuery(null); setMentionQuery(null);
setSlashQuery(null); setSlashQuery(null);
setSelectedSkill(null);
setSelectedMentions([]);
setReplyTarget(null); setReplyTarget(null);
setAttachmentNotice(null); setAttachmentNotice(null);
setError(null); setError(null);
setStopped(false);
}, [threadKey]); }, [threadKey]);
function updateDraft(value: string) { function updateDraft(value: string) {
@ -1053,6 +1177,11 @@ function Thread() {
function runSlashAction(action: SlashActionId) { function runSlashAction(action: SlashActionId) {
setDraft(""); setDraft("");
setSlashQuery(null); setSlashQuery(null);
writeComposerDraft(draftKey, {
text: "",
selectedSkill,
selectedMentions,
});
if (action === "chat-settings") { if (action === "chat-settings") {
if (inGroup && groupId) { if (inGroup && groupId) {
router.push({ pathname: "/group-settings", params: { groupId } }); router.push({ pathname: "/group-settings", params: { groupId } });
@ -1078,6 +1207,10 @@ function Thread() {
const initialGroupTarget = groupId; const initialGroupTarget = groupId;
if ((!initialBotTarget && !initialGroupTarget) || sending) return; if ((!initialBotTarget && !initialGroupTarget) || sending) return;
const originThreadKey = initialGroupTarget ?? initialBotTarget; const originThreadKey = initialGroupTarget ?? initialBotTarget;
const originDraftKey = composerDraftThreadKey({
botId: initialBotTarget,
groupId: initialGroupTarget,
});
const attachments = attachmentsForThread(pendingAttachments, originThreadKey); const attachments = attachmentsForThread(pendingAttachments, originThreadKey);
const plan = resolveComposerSendPlan({ const plan = resolveComposerSendPlan({
text: serializeComposerPromptText(), text: serializeComposerPromptText(),
@ -1097,6 +1230,7 @@ function Thread() {
if (initialBotTarget) cancelFocusPrompt(initialBotTarget); if (initialBotTarget) cancelFocusPrompt(initialBotTarget);
}; };
setSending(true); setSending(true);
setStopped(false);
setError(null); setError(null);
try { try {
if (plan.shouldRunRoutines) { if (plan.shouldRunRoutines) {
@ -1111,6 +1245,7 @@ function Thread() {
); );
} }
const clearOriginComposer = () => { const clearOriginComposer = () => {
clearComposerDraft(originDraftKey);
setPendingAttachments((current) => setPendingAttachments((current) =>
current.filter((attachment) => attachment.threadKey !== originThreadKey), current.filter((attachment) => attachment.threadKey !== originThreadKey),
); );
@ -1205,12 +1340,16 @@ function Thread() {
const targetGroupId = groupId; const targetGroupId = groupId;
if ((!targetBotId && !targetGroupId) || sending) return; if ((!targetBotId && !targetGroupId) || sending) return;
setSending(true); setSending(true);
setStopped(false);
setError(null); setError(null);
try { try {
await rpc( await rpc(
"threads/stop", "threads/stop",
targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }, targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! },
); );
if (isCurrentTarget(targetBotId, targetGroupId)) {
setStopped(true);
}
} catch (err) { } catch (err) {
if (isCurrentTarget(targetBotId, targetGroupId)) { if (isCurrentTarget(targetBotId, targetGroupId)) {
setError(err instanceof Error ? err.message : t("Failed to stop work")); 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( const answerMessage = useCallback(
async (message: MobileMessage, answer: string) => { async (message: MobileMessage, answer: string) => {
const targetBotId = botId; const targetBotId = botId;
@ -1313,6 +1526,10 @@ function Thread() {
const targetGroupId = groupId; const targetGroupId = groupId;
if (!targetBotId && !targetGroupId) return; if (!targetBotId && !targetGroupId) return;
const target = targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }; const target = targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! };
const targetDraftKey = composerDraftThreadKey({
botId: targetBotId,
groupId: targetGroupId,
});
const until = autopilotUntil(choice, new Date()); const until = autopilotUntil(choice, new Date());
void rpc("autopilot/start", { void rpc("autopilot/start", {
...target, ...target,
@ -1320,8 +1537,12 @@ function Thread() {
...(until ? { until: until.toISOString() } : {}), ...(until ? { until: until.toISOString() } : {}),
}) })
.then(async () => { .then(async () => {
const existing = readComposerDraft(targetDraftKey);
writeComposerDraft(targetDraftKey, { ...existing, text: "" });
if (isCurrentTarget(targetBotId, targetGroupId)) {
setDraft(""); setDraft("");
if (isCurrentTarget(targetBotId, targetGroupId)) await refresh(); await refresh();
}
}) })
.catch((err) => .catch((err) =>
Alert.alert(t("Autopilot"), err instanceof Error ? err.message : t("Try again.")), Alert.alert(t("Autopilot"), err instanceof Error ? err.message : t("Try again.")),
@ -1553,7 +1774,9 @@ function Thread() {
flexShrink: 1, flexShrink: 1,
}} }}
> >
{foldedSteps ? <RunSteps steps={foldedSteps} /> : null} {foldedSteps && !activeMobilePlan?.absorbedIds.has(message.id) ? (
<RunSteps steps={foldedSteps} />
) : null}
<Pressable accessible={false} onLongPress={actionProps.onLongPress}> <Pressable accessible={false} onLongPress={actionProps.onLongPress}>
<MessageBubble <MessageBubble
botId={botId ?? snap?.members?.[0]?.botId ?? ""} botId={botId ?? snap?.members?.[0]?.botId ?? ""}
@ -1574,6 +1797,55 @@ function Thread() {
actionProps={actionProps} actionProps={actionProps}
/> />
</Pressable> </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 ? ( {messageReactions ? (
<View <View
style={{ style={{
@ -1684,7 +1956,70 @@ function Thread() {
> >
{error ? <Text style={{ color: tokens.mutedForeground, marginTop: 12 }}>{error}</Text> : null} {error ? <Text style={{ color: tokens.mutedForeground, marginTop: 12 }}>{error}</Text> : null}
{runError ? ( {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} ) : null}
<View style={{ flex: 1, position: "relative" }}> <View style={{ flex: 1, position: "relative" }}>
{showPinnedPage ? ( {showPinnedPage ? (
@ -2025,6 +2360,191 @@ function Thread() {
))} ))}
</View> </View>
) : null} ) : 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 <View
style={{ style={{
flexDirection: "row", flexDirection: "row",
@ -2528,6 +3048,105 @@ const MessageBubble = memo(function MessageBubble({
</View> </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)) { if (isRoutineEventMessage(message.blocks)) {
return ( return (
<View style={{ width: "100%" }}> <View style={{ width: "100%" }}>

View File

@ -784,7 +784,7 @@ export type MobileGroup = Pick<
| "updatedAt" | "updatedAt"
| "members" | "members"
> & > &
Partial<Pick<Group, "spaceId">>; Partial<Pick<Group, "spaceId" | "kind">>;
export type MobileSpace = Space; export type MobileSpace = Space;
export type MobileSpaceNavigation = SpaceNavigation; export type MobileSpaceNavigation = SpaceNavigation;
@ -797,8 +797,14 @@ export type MobileSnapshot = {
cursor?: number; cursor?: number;
messages: MobileMessage[]; messages: MobileMessage[];
olderCursor: number | null; olderCursor: number | null;
run: { id: string; botId?: string; status: string; error?: string | null } | null; run: {
activeRuns?: Array<{ id: string; botId?: string; status: string }>; 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"]; members?: MobileGroup["members"];
computer?: { computer?: {
kind?: string; kind?: string;

View File

@ -147,12 +147,18 @@ describe("spaceInboxItems", () => {
expect(canDeleteInboxSpace(space({ canDelete }))).toBe(false); 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())).toBe(true);
expect(canDeleteInboxSpace(space({ hasContent: true, canDelete: true }))).toBe(true);
}); });
it("hides deletion while a space still has content", () => { it("deduplicates same-named projects into a single project entry", () => {
expect(canDeleteInboxSpace(space({ hasContent: true }))).toBe(false); 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 " });
}); });
}); });

View File

@ -24,22 +24,47 @@ export type InboxSpaceItem =
| { type: "heading"; key: string; title: string; space?: InboxSpace }; | { type: "heading"; key: string; title: string; space?: InboxSpace };
export function canDeleteInboxSpace( 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[] { 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 = [ const chats = [
// A main bot is reached through its space's main conversation, not listed again. // A main bot is reached through its space's main conversation, not listed again.
...space.bots ...space.bots
.filter((chat) => chat.kind !== "main") .filter((chat) => chat.kind !== "main")
.map((chat) => ({ type: "bot" as const, bot: chat, ...chat })), .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[] = []; 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 }); items.push({ type: "heading", key: space.id, title: space.name, space });
} }
for (const group of groupBotsForSidebar(chats, space.botSections)) { for (const group of groupBotsForSidebar(chats, space.botSections)) {

View File

@ -261,6 +261,9 @@ export const DE_MESSAGES: Record<string, string> = {
Eyeshadow: "Lidschatten", Eyeshadow: "Lidschatten",
Failed: "Fehlgeschlagen", Failed: "Fehlgeschlagen",
"Failed to refresh": "Aktualisieren 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 send message": "Nachricht konnte nicht gesendet werden",
"Failed to stop work": "Arbeit konnte nicht gestoppt werden", "Failed to stop work": "Arbeit konnte nicht gestoppt werden",
"Fewer {label}": "Weniger {label}", "Fewer {label}": "Weniger {label}",
@ -497,6 +500,7 @@ export const DE_MESSAGES: Record<string, string> = {
"Section name": "Abschnittsname", "Section name": "Abschnittsname",
Send: "Senden", Send: "Senden",
"Send answer": "Antwort senden", "Send answer": "Antwort senden",
"Send now": "Jetzt gleich senden",
"Send reset link": "Link zum Zurücksetzen senden", "Send reset link": "Link zum Zurücksetzen senden",
"Sending…": "Wird gesendet…", "Sending…": "Wird gesendet…",
Server: "Server", Server: "Server",

View File

@ -274,6 +274,9 @@ export const RU_MESSAGES: Record<string, string> = {
"Executor token": "Токен Executor", "Executor token": "Токен Executor",
Failed: "Ошибка", Failed: "Ошибка",
"Failed to refresh": "Не удалось обновить", "Failed to refresh": "Не удалось обновить",
Resume: "Продолжить",
Stopped: "Остановлено",
"Failed to remove message": "Не удалось удалить сообщение",
"Failed to send message": "Не удалось отправить сообщение", "Failed to send message": "Не удалось отправить сообщение",
"Failed to stop work": "Не удалось остановить работу", "Failed to stop work": "Не удалось остановить работу",
File: "Файл", File: "Файл",
@ -429,6 +432,7 @@ export const RU_MESSAGES: Record<string, string> = {
Send: "Отправить", Send: "Отправить",
"Send reset link": "Отправить ссылку для сброса", "Send reset link": "Отправить ссылку для сброса",
"Send answer": "Отправить ответ", "Send answer": "Отправить ответ",
"Send now": "Отправить сейчас",
"Sending…": "Отправка…", "Sending…": "Отправка…",
Server: "Сервер", Server: "Сервер",
"Server URL": "URL-адрес сервера", "Server URL": "URL-адрес сервера",

View File

@ -259,6 +259,9 @@ export const ZH_MESSAGES: Record<string, string> = {
"Executor token": "Executor 令牌", "Executor token": "Executor 令牌",
Failed: "失败", Failed: "失败",
"Failed to refresh": "刷新失败", "Failed to refresh": "刷新失败",
Resume: "继续",
Stopped: "已停止",
"Failed to remove message": "删除消息失败",
"Failed to send message": "发送消息失败", "Failed to send message": "发送消息失败",
"Failed to stop work": "停止工作失败", "Failed to stop work": "停止工作失败",
File: "文件", File: "文件",
@ -410,6 +413,7 @@ export const ZH_MESSAGES: Record<string, string> = {
Send: "发送", Send: "发送",
"Send reset link": "发送重置链接", "Send reset link": "发送重置链接",
"Send answer": "发送回答", "Send answer": "发送回答",
"Send now": "立即发送",
"Sending…": "正在发送…", "Sending…": "正在发送…",
Server: "服务器", Server: "服务器",
"Server URL": "服务器地址", "Server URL": "服务器地址",

View File

@ -18,14 +18,14 @@
}, },
"dependencies": { "dependencies": {
"@expo/metro-runtime": "57.0.17", "@expo/metro-runtime": "57.0.17",
"@expo/ui": "57.0.25", "@expo/ui": "57.0.26",
"@rakazo/chat-ui": "workspace:*", "@rakazo/chat-ui": "workspace:*",
"@rakazo/contracts": "workspace:*", "@rakazo/contracts": "workspace:*",
"@rakazo/core": "workspace:*", "@rakazo/core": "workspace:*",
"@rakazo/ui-tokens": "workspace:*", "@rakazo/ui-tokens": "workspace:*",
"@react-native-vector-icons/ionicons": "13.1.4", "@react-native-vector-icons/ionicons": "13.1.4",
"@react-native-vector-icons/material-design-icons": "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-application": "57.0.3",
"expo-audio": "57.0.5", "expo-audio": "57.0.5",
"expo-clipboard": "57.0.2", "expo-clipboard": "57.0.2",
@ -37,11 +37,11 @@
"expo-keep-awake": "57.0.2", "expo-keep-awake": "57.0.2",
"expo-linking": "57.0.13", "expo-linking": "57.0.13",
"expo-localization": "57.0.2", "expo-localization": "57.0.2",
"expo-modules-core": "57.0.23", "expo-modules-core": "57.0.24",
"expo-notifications": "57.0.23", "expo-notifications": "57.0.24",
"expo-router": "57.0.28", "expo-router": "57.0.29",
"expo-secure-store": "57.0.4", "expo-secure-store": "57.0.4",
"expo-sharing": "57.0.23", "expo-sharing": "57.0.24",
"expo-splash-screen": "57.0.10", "expo-splash-screen": "57.0.10",
"expo-status-bar": "57.0.1", "expo-status-bar": "57.0.1",
"expo-symbols": "57.0.3", "expo-symbols": "57.0.3",

View File

@ -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-panel", "computer");
await expect(sidePanel).toHaveAttribute("data-auto", ""); 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", "src",
SCREEN_URL, SCREEN_URL,
); );

View File

@ -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(); 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); await expect(preview.locator("iframe")).toHaveAttribute("src", SCREEN_URL);
const cursor = preview.getByTestId("agent-cursor"); const cursor = preview.getByTestId("agent-cursor");
await expect(cursor).toHaveCSS("opacity", "0"); 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. // 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 rpc(page, "threads/send", { botId, text: "use your screen and type cursor-demo" });
await expect(cursor).toHaveCSS("opacity", "1"); 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())!; const layer = (await preview.getByTestId("agent-cursor-layer").boundingBox())!;
await expect await expect
.poll(async () => { .poll(async () => {

View File

@ -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 composer.fill("write notes/result.txt and attach it to the thread");
await page.keyboard.press("Enter"); await page.keyboard.press("Enter");
// Scope to ArtifactFileCard text (name + mime); sidebar bot status can also const fileCard = page.getByRole("button", { name: "Preview result.txt" });
// include the prompt path notes/result.txt once status syncs immediately.
const fileCard = page.getByRole("button", { name: /result\.txt text\/plain/ });
await expect(fileCard).toBeVisible({ timeout: 30_000 }); await expect(fileCard).toBeVisible({ timeout: 30_000 });
await captureScreenshot(page, testInfo, "current-file-card"); await captureScreenshot(page, testInfo, "current-file-card");
const downloadPromise = page.waitForEvent("download"); const downloadPromise = page.waitForEvent("download");
await fileCard.click(); await page.getByRole("button", { name: "Download result.txt" }).click();
const download = await downloadPromise; const download = await downloadPromise;
expect(download.suggestedFilename()).toBe("result.txt"); expect(download.suggestedFilename()).toBe("result.txt");
}); });

View File

@ -114,12 +114,12 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) =
await captureScreenshot(page, testInfo, "27a-bot-settings-model"); await captureScreenshot(page, testInfo, "27a-bot-settings-model");
await page.getByRole("button", { name: "Show computer" }).click(); await page.getByRole("button", { name: "Show computer" }).click();
const sidePanel = page.getByTestId("side-panel"); 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(); await expect(page.getByRole("button", { name: "Show settings" })).toBeVisible();
// Overlay may flash during boot or never appear (already ready/asleep/stopped). Assert panel // 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. // chrome, then wait until any overlay has cleared — avoid Locator.or() strict-mode multi-hits.
const bootOverlay = page.getByText(/Booting up .* computer/); 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(bootOverlay).toBeHidden();
await expect(sidePanel.getByText("Teach a task")).toHaveCount(0); await expect(sidePanel.getByText("Teach a task")).toHaveCount(0);
await expect(sidePanel.getByTestId("teach-start-button")).toHaveCount(0); await expect(sidePanel.getByTestId("teach-start-button")).toHaveCount(0);

View File

@ -39,9 +39,9 @@ test("screen connection failures stay visible and can be retried", async ({ page
); );
await page.getByTitle("Agent computer").click(); 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.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"); await captureScreenshot(page, testInfo, "computer-screen-connection-error");
failScreen = false; 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); await expect(preview.getByRole("alert")).toHaveCount(0);
failScreen = true; failScreen = true;
await preview.hover(); await page.getByTestId("computer-open").click();
await preview.getByTestId("computer-preview-open").click();
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); 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"); await captureScreenshot(page, testInfo, "computer-full-screen-connection-error");
failScreen = false; 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.locator('iframe[title="Bot screen"]')).toHaveAttribute("src", screenUrl);
await expect(page.getByRole("alert")).toHaveCount(0); await expect(page.getByRole("alert")).toHaveCount(0);
}); });

View File

@ -59,9 +59,8 @@ test("computer maintenance shows durable background progress and failure recover
}); });
}); });
await page.getByTitle("Agent computer").click(); await page.getByTitle("Agent computer").click();
await expect(page.getByTestId("computer-preview")).toBeVisible(); await expect(page.getByTestId("oo-computer-browser-viewport")).toBeVisible();
await page.getByTestId("computer-preview").hover(); await page.getByTestId("computer-open").click();
await page.getByTestId("computer-preview-open").click();
await page.getByTestId("computer-more-button").click(); await page.getByTestId("computer-more-button").click();
await page.getByRole("menuitem", { name: "Update computer", exact: true }).click(); await page.getByRole("menuitem", { name: "Update computer", exact: true }).click();
const dialog = page.getByTestId("computer-update-dialog"); const dialog = page.getByTestId("computer-update-dialog");

View File

@ -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 signup(page, `main-1-1-${stamp}@rakazo.test`, "password12", "Solo");
await completeOnboarding(page, undefined, { mainConversation: true }); await completeOnboarding(page, undefined, { mainConversation: true });
await expect(page.getByTestId("bot-settings-trigger")).not.toContainText("Gruppenunterhaltung"); 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", () => { 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 signup(page, `main-computer-${stamp}@rakazo.test`, "password12", "Screen");
await completeOnboarding(page, undefined, { mainConversation: true }); await completeOnboarding(page, undefined, { mainConversation: true });
await expect(page).toHaveURL(/\/app\/g\//); 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 page.getByTitle("Agent computer").click();
await expect(page).not.toHaveURL(/\/app\/g\//); await expect(page).toHaveURL(/\/app\/g\//);
const sidePanel = page.getByTestId("side-panel"); const sidePanel = page.getByTestId("side-panel");
await expect(sidePanel).toHaveAttribute("data-panel", "computer"); 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"); await captureScreenshot(page, testInfo, "main-conversation-computer");
}); });

View File

@ -80,8 +80,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
expect(mainBox).not.toBeNull(); expect(mainBox).not.toBeNull();
expect(panelBox).not.toBeNull(); expect(panelBox).not.toBeNull();
expect((mainBox?.x ?? 0) + (mainBox?.width ?? 0)).toBeLessThanOrEqual(panelBox?.x ?? 0); expect((mainBox?.x ?? 0) + (mainBox?.width ?? 0)).toBeLessThanOrEqual(panelBox?.x ?? 0);
await sidePanel.getByTestId("computer-preview").hover(); await sidePanel.getByTestId("computer-open").click();
await sidePanel.getByTestId("computer-preview-open").click();
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); 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: "Skip", exact: true }).last()).toBeVisible();
await expect(page.getByRole("button", { name: "I’m done", 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 page.getByTitle("Agent computer").click();
await expect(sidePanel).toHaveAttribute("data-panel", "computer"); await expect(sidePanel).toHaveAttribute("data-panel", "computer");
await sidePanel.getByTestId("computer-preview").hover(); const openComputer = sidePanel.getByTestId("computer-open");
const openComputer = sidePanel.getByTestId("computer-preview-open");
await expect(openComputer).toBeVisible({ timeout: 30_000 }); await expect(openComputer).toBeVisible({ timeout: 30_000 });
await openComputer.click(); await openComputer.click();
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); 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 page.getByTitle("Agent computer").click();
await routinesListed; await routinesListed;
await sidePanel.locator('[data-tab="overview"]').click();
await expect( await expect(
sidePanel.getByTestId("routine-list-row").getByText("Monday briefing"), sidePanel.getByTestId("routine-list-row").getByText("Monday briefing"),
).toBeVisible(); ).toBeVisible();
@ -316,7 +315,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
await page.getByTitle("Agent computer").click(); await page.getByTitle("Agent computer").click();
} }
await expect(sidePanel).toHaveAttribute("data-panel", "computer"); 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(); await gear.click();
const downloadPromise = page.waitForEvent("download"); const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export" }).click(); await page.getByRole("button", { name: "Export" }).click();

View File

@ -63,6 +63,10 @@ test("create group from + and see two bots in one transcript", async ({ page },
const researcherId = await createBot(page, "Researcher"); const researcherId = await createBot(page, "Researcher");
const writerId = await createBot(page, "Research Writer"); 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 openNewGroup(page);
await page.locator("label:has-text('Name') input").fill("Draft team"); 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 expect(desktopSettings.getByRole("button", { name: "Save", exact: true })).toBeEnabled();
await page.unroute("**/rpc/groups/update"); await page.unroute("**/rpc/groups/update");
await desktopSettings.getByRole("button", { name: "Save", exact: true }).click(); 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 page.getByRole("combobox", { name: "Message" }).fill("@Researcher unfinished draft");
await projekte await projekte
.getByRole("button", { name: /^Review team/ }) .getByRole("button", { name: /^Review team/ })
.first() .first()
.click(); .click();
await expect(page).toHaveURL(new RegExp(`/app/g/${reviewGroup.id}`));
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(""); await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue("");
// Each chat keeps its own unsent draft.
await projekte await projekte
.getByRole("button", { name: /^Draft team/ }) .getByRole("button", { name: /^Draft team/ })
.first() .first()
.click(); .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" }); const composer = page.getByRole("combobox", { name: "Message" });
await composer.fill("@Res"); await composer.fill("@Res");

View File

@ -61,8 +61,12 @@ export async function createTestRoutine(
}); });
} }
export async function openAgentComputerPanel(page: Page) { /** Opens the side panel on the agent's overview, where its routines are listed. */
await page.getByTitle("Agent computer").click(); 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> { export async function rpc<T>(page: Page, procedure: string, body: unknown): Promise<T> {

View File

@ -1,7 +1,14 @@
import type { Page } from "@playwright/test"; import type { Page } from "@playwright/test";
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import type { CoachRhythm, CoachToday, Goal, Habit, Routine } from "@rakazo/contracts"; 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". */ /** The nav button reads "Today" or "Today, N need you". */
function openHeute(page: Page) { 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. // The main bot's routine list names the check-ins by their key, in the UI language.
await openMainBotThread(page); 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("Morning brief", { exact: true })).toBeVisible();
await expect(page.getByText("Weekly review", { exact: true })).toBeVisible(); await expect(page.getByText("Weekly review", { exact: true })).toBeVisible();
}); });

View File

@ -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"); const browser = page.getByTestId("in-app-browser");
await browser.getByTestId("in-app-browser-mode-local").click(); await browser.getByTestId("in-app-browser-mode-local").click();
const address = browser.getByRole("textbox", { name: "Address" }); 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(); await browser.getByRole("button", { name: "Open in browser" }).click();
expect((await popup).url()).toBe("https://preview.example/app"); 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); await expect(browser).toHaveCount(0);
// The last address comes back when the browser reopens. // 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 page.getByTestId("in-app-browser-mode-local").click();
await expect(page.getByTestId("in-app-browser-frame")).toHaveAttribute( await expect(page.getByTestId("in-app-browser-frame")).toHaveAttribute(
"src", "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, () => (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"); const browser = page.getByTestId("in-app-browser");
await browser.getByTestId("in-app-browser-mode-local").click(); await browser.getByTestId("in-app-browser-mode-local").click();
await browser.getByRole("textbox", { name: "Address" }).fill("localhost:5173"); 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 page.keyboard.press("Escape");
await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("show"); 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"); await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("hide");
}); });

View File

@ -26,7 +26,8 @@ test("onboarding lands in the main conversation, its lead answers, and addressed
await expect(mainEntry).toBeVisible(); await expect(mainEntry).toBeVisible();
// The main bot is reached through the main conversation, not listed again. // 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(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"); await captureScreenshot(page, testInfo, "main-conversation-landing");
const snapshot = () => rpc<Snapshot>(page, "threads/get", { groupId }); const snapshot = () => rpc<Snapshot>(page, "threads/get", { groupId });

View File

@ -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 expect(thumbnail).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/);
await thumbnail.click(); 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" }); const download = dialog.getByRole("link", { name: "Download pixel.png" });
await expect(download).toHaveAttribute("href", /\/api\/artifacts\/[^/?]+\?download=1$/); await expect(download).toHaveAttribute("href", /\/api\/artifacts\/[^/?]+\?download=1$/);
const saved = page.waitForEvent("download"); const saved = page.waitForEvent("download");

View File

@ -92,7 +92,13 @@ test("message hover shows below-bubble actions; reply links to parent", async ({
); );
const botText = page.getByText(/i handled: hover-bot-/); const botText = page.getByText(/i handled: hover-bot-/);
await expect(transcript.getByText(/i handled: hover-bot-/)).toBeVisible({ timeout: 20_000 }); 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 expect(botRow).toBeVisible();
await expectRailAtRest(page, botRow); await expectRailAtRest(page, botRow);
await captureScreenshot(page, testInfo, "message-actions-rest-desktop"); await captureScreenshot(page, testInfo, "message-actions-rest-desktop");

View File

@ -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"); const browser = page.getByTestId("in-app-browser");
await expect(browser.getByTestId("oo-computer-browser-screen")).toHaveAttribute( await expect(browser.getByTestId("oo-computer-browser-screen")).toHaveAttribute(
"src", "src",

View File

@ -18,7 +18,8 @@ test("live page helper preserves identity, masks passwords, and reports partial
res.setHeader("content-type", "text/html"); res.setHeader("content-type", "text/html");
res.end(`<!doctype html><title>Fixture</title><p>Readable article content</p> 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"> <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> <label><input type="checkbox">Agree</label><button disabled>Disabled</button>
<script>window.__rakazoPageBrowser={snapshot:()=>({title:'spoofed'})};</script>`); <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"); const refreshed = await command("snapshot");
expect(ref(refreshed, "Replacement")).not.toBe(ref(stale, "Save")); expect(ref(refreshed, "Replacement")).not.toBe(ref(stale, "Save"));
expect( expect(
await command("act", { actions: [{ kind: "click", ref: ref(refreshed, "Agree") }] }), await command("act", {
).toMatchObject({ ok: true, completed: 1 }); 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.getByRole("checkbox")).toBeChecked();
await expect(page.locator("#plan")).toHaveValue("pro");
expect( expect(
await command("navigate", { url: `http://127.0.0.1:${address.port}/next` }), await command("navigate", { url: `http://127.0.0.1:${address.port}/next` }),
).toMatchObject({ ok: true, title: "Fixture" }); ).toMatchObject({ ok: true, title: "Fixture" });

View File

@ -6,7 +6,7 @@ import {
completeOnboarding, completeOnboarding,
createNamedBot, createNamedBot,
createTestRoutine, createTestRoutine,
openAgentComputerPanel, openAgentOverview,
rpc, rpc,
signup, signup,
} from "./helpers"; } from "./helpers";
@ -33,7 +33,7 @@ test("routine active switch keeps its thumb inside the track", async ({ page },
await completeOnboarding(page); await completeOnboarding(page);
const botId = activeBotId(page); const botId = activeBotId(page);
await createTestRoutine(page, botId, { name: "Toggle styling" }); await createTestRoutine(page, botId, { name: "Toggle styling" });
await openAgentComputerPanel(page); await openAgentOverview(page);
await page.getByRole("button", { name: /Toggle styling/ }).click(); await page.getByRole("button", { name: /Toggle styling/ }).click();
const toggle = page.getByRole("switch", { name: "Active" }); 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(created.nextRunAt).not.toBeNull();
expect(localSchedule(created.nextRunAt!, created.timezone)).toMatchObject({ hour: 9, minute: 0 }); expect(localSchedule(created.nextRunAt!, created.timezone)).toMatchObject({ hour: 9, minute: 0 });
await page.reload(); await page.reload();
await page.getByTitle("Agent computer").click(); await openAgentOverview(page);
await page.getByRole("button", { name: /Tokyo check-in/ }).click(); await page.getByRole("button", { name: /Tokyo check-in/ }).click();
await page.locator("label:has-text('Name') input").fill("Weekday check-in"); 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([]); expect(await rpc<Routine[]>(page, "routines/list", { botId })).toEqual([]);
await page.reload(); await page.reload();
await page.getByTitle("Agent computer").click(); await openAgentOverview(page);
await expect(updatedButton).toHaveCount(0); 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", name: "Persisted routine",
prompt: "Run once each morning", prompt: "Run once each morning",
}); });
await openAgentComputerPanel(page); await openAgentOverview(page);
await page.getByRole("button", { name: /Persisted routine/ }).click(); await page.getByRole("button", { name: /Persisted routine/ }).click();
await page await page
.locator("label:has-text('Instruction') textarea") .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.reload();
await page.getByTitle("Agent computer").click(); await openAgentOverview(page);
await page.getByRole("button", { name: /First routine/ }).click(); await page.getByRole("button", { name: /First routine/ }).click();
await page.locator("label:has-text('Name') input").fill("First routine updated"); 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.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
await expect(page.getByRole("alert")).toHaveCount(0); 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: /Second routine/ })).toHaveCount(1);
await expect(page.getByRole("button", { name: /First routine/ })).toHaveCount(0); await expect(page.getByRole("button", { name: /First routine/ })).toHaveCount(0);

View File

@ -5,7 +5,7 @@ import {
captureScreenshot, captureScreenshot,
completeOnboarding, completeOnboarding,
createTestRoutine, createTestRoutine,
openAgentComputerPanel, openAgentOverview,
rpc, rpc,
signup, signup,
} from "./helpers"; } from "./helpers";
@ -38,7 +38,7 @@ test("Slack message trigger uses the mounted messaging provider and persists", a
githubEnabled: false, githubEnabled: false,
messageProvider: "slack", messageProvider: "slack",
}); });
await openAgentComputerPanel(page); await openAgentOverview(page);
const panel = page.getByTestId("side-panel"); const panel = page.getByTestId("side-panel");
await expect(panel.getByText("Slack message", { exact: true })).toBeVisible(); await expect(panel.getByText("Slack message", { exact: true })).toBeVisible();
const [routine] = await rpc<Routine[]>(page, "routines/list", { botId }); 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, githubEnabled: true,
messageProvider: null, messageProvider: null,
}); });
await openAgentComputerPanel(page); await openAgentOverview(page);
await page.getByRole("button", { name: /Review repository events/ }).click(); await page.getByRole("button", { name: /Review repository events/ }).click();
await expect( await expect(
page.getByTestId("side-panel").getByText("Git event", { exact: true }), 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: [], crons: [],
webhookEnabled: true, webhookEnabled: true,
}); });
await page.getByTitle("Agent 컴퓨터").click(); await openAgentOverview(page, "Agent 컴퓨터");
await page.getByRole("button", { name: /한국어 웹훅 확인/ }).click(); await page.getByRole("button", { name: /한국어 웹훅 확인/ }).click();
await expect(page.getByText("웹훅이 실행될 때", { exact: true })).toBeVisible(); 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); await completeOnboarding(page);
const botId = activeBotId(page); const botId = activeBotId(page);
await openAgentComputerPanel(page); await openAgentOverview(page);
await expect(page.getByRole("button", { name: "Test run" })).toHaveCount(0); await expect(page.getByRole("button", { name: "Test run" })).toHaveCount(0);
await createTestRoutine(page, botId, { await createTestRoutine(page, botId, {
name: "Daily verification", name: "Daily verification",
@ -135,7 +135,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo
crons: ["0 9 * * 1-5"], crons: ["0 9 * * 1-5"],
}); });
await page.reload(); await page.reload();
await openAgentComputerPanel(page); await openAgentOverview(page);
const routine = page.getByRole("button", { name: /Daily verification/ }); const routine = page.getByRole("button", { name: /Daily verification/ });
await expect(routine).toContainText("Weekdays at 9:00 AM"); await expect(routine).toContainText("Weekdays at 9:00 AM");
await captureScreenshot(page, testInfo, "33-routine-scheduled"); 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 page.reload();
await expect(page.getByText(/routine-run-now-ok/i).first()).toBeVisible(); 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( await expect(page.getByRole("button", { name: /Daily verification/ })).toContainText(
"Weekdays at 9:00 AM", "Weekdays at 9:00 AM",
); );

View File

@ -4,7 +4,7 @@ import {
activeBotId, activeBotId,
completeOnboarding, completeOnboarding,
createTestRoutine, createTestRoutine,
openAgentComputerPanel, openAgentOverview,
rpc, rpc,
signup, signup,
} from "./helpers"; } from "./helpers";
@ -17,14 +17,14 @@ test("routines section is hidden until the Oo has one, then list toggle persists
await completeOnboarding(page); await completeOnboarding(page);
const botId = activeBotId(page); const botId = activeBotId(page);
await openAgentComputerPanel(page); await openAgentOverview(page);
const panel = page.getByTestId("side-panel"); const panel = page.getByTestId("side-panel");
await expect(panel.getByText("Routines", { exact: true })).toHaveCount(0); await expect(panel.getByText("Routines", { exact: true })).toHaveCount(0);
await expect(page.getByTestId("routine-create-button")).toHaveCount(0); await expect(page.getByTestId("routine-create-button")).toHaveCount(0);
await createTestRoutine(page, botId, { name: "Morning check-in" }); await createTestRoutine(page, botId, { name: "Morning check-in" });
await page.reload(); await page.reload();
await openAgentComputerPanel(page); await openAgentOverview(page);
await expect(panel.getByText("Routines", { exact: true })).toBeVisible(); await expect(panel.getByText("Routines", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: /Morning check-in/ })).toBeVisible(); await expect(page.getByRole("button", { name: /Morning check-in/ })).toBeVisible();

View File

@ -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: "Update computer" })).toHaveCount(0);
await expect(sidePanel.getByRole("button", { name: "Take control" })).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-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. // 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(); await expect(openButton).toBeVisible();
const botId = activeBotId(page); const botId = activeBotId(page);
expect((await rpc<{ state: string }>(page, "computer/status", { botId })).state).toBe("stopped"); expect((await rpc<{ state: string }>(page, "computer/status", { botId })).state).toBe("stopped");
await captureScreenshot(page, testInfo, "teach-sidepanel-simple"); await captureScreenshot(page, testInfo, "teach-sidepanel-simple");
const preview = sidePanel.getByTestId("computer-preview"); const preview = sidePanel.getByTestId("oo-computer-browser-viewport");
await preview.hover();
await expect(openButton).toBeVisible(); await expect(openButton).toBeVisible();
await expect(openButton.getByText("Open", { exact: true })).toBeVisible();
await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover"); await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover");
await openButton.click(); await openButton.click();

View File

@ -10,7 +10,7 @@ test("blocks the composer while a skill is being taught", async ({ page }) => {
await page.getByTitle("Agent computer").click(); await page.getByTitle("Agent computer").click();
const sidePanel = page.getByTestId("side-panel"); const sidePanel = page.getByTestId("side-panel");
await expect(sidePanel).toHaveAttribute("data-panel", "computer"); 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 expect(openButton).toBeVisible();
await openButton.click(); await openButton.click();
const chrome = page.getByTestId("computer-chrome"); const chrome = page.getByTestId("computer-chrome");

View File

@ -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"); const privateId = await createBot(page, "Private Writer", "dedicated");
await openComputerPanel(page); 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 captureScreenshot(page, testInfo, "43-private-computer");
await expect(readFileResponse(page, privateId, "notes/result.txt")).resolves.toMatchObject({ await expect(readFileResponse(page, privateId, "notes/result.txt")).resolves.toMatchObject({
ok: false, 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. // The sidebar entry for the main bot opens the main conversation; its computer is in the 1:1.
await openMainBotThread(page); await openMainBotThread(page);
await page.getByTitle("Agent computer").click(); await page.getByTitle("Agent computer").click();
await page.getByTestId("computer-preview").hover(); await page.getByTestId("computer-open").click();
await page.getByTestId("computer-preview-open").click();
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
await page.getByRole("button", { name: "Close computer" }).click(); await page.getByRole("button", { name: "Close computer" }).click();
await expect(page.getByRole("button", { name: "Close computer" })).toHaveCount(0); 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 signup(page, `team-release-${Date.now()}@rakazo.test`, "password12", "Team Release");
await completeOnboarding(page); await completeOnboarding(page);
await page.getByTitle("Agent computer").click(); await page.getByTitle("Agent computer").click();
await page.getByTestId("computer-preview").hover(); await page.getByTestId("computer-open").click();
await page.getByTestId("computer-preview-open").click();
const chrome = page.getByTestId("computer-chrome"); const chrome = page.getByTestId("computer-chrome");
const release = chrome.getByRole("button", { name: "Release", exact: true }); const release = chrome.getByRole("button", { name: "Release", exact: true });
await expect(release).toBeVisible(); 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(); await page.getByTitle("Agent computer").click();
const sidePanel = page.getByTestId("side-panel"); const sidePanel = page.getByTestId("side-panel");
await expect(sidePanel.getByRole("button", { name: /Take control/i })).toHaveCount(0); await expect(sidePanel.getByRole("button", { name: /Take control/i })).toHaveCount(0);
await page.getByTestId("computer-preview").hover(); const openBusy = sidePanel.getByTestId("computer-open");
const openBusy = sidePanel.getByTestId("computer-preview-open");
await expect(openBusy).toBeVisible(); await expect(openBusy).toBeVisible();
await expect(openBusy.getByText("Open", { exact: true })).toBeVisible();
await openBusy.click(); await openBusy.click();
const chrome = page.getByTestId("computer-chrome"); const chrome = page.getByTestId("computer-chrome");
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); 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(); .toBeNull();
// After stop, Open via hover is the takeover path (no Take control button). // After stop, Open via hover is the takeover path (no Take control button).
await page.getByTestId("computer-preview").hover(); await page.getByTestId("computer-open").click();
await page.getByTestId("computer-preview-open").click();
await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible();
await expect(chrome.getByText("You have control", { exact: true })).toBeVisible(); await expect(chrome.getByText("You have control", { exact: true })).toBeVisible();
await expect(chrome.getByRole("button", { name: /Take control/i })).toHaveCount(0); 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) { async function openComputerPanel(page: Page) {
await page.getByTitle("Agent computer").click(); await page.getByTitle("Agent computer").click();
await expect(page.getByTestId("computer-preview")).toBeVisible(); await expect(page.getByTestId("oo-computer-browser-viewport")).toBeVisible();
await expect(page.getByTestId("computer-preview-open")).toHaveCount(1); await expect(page.getByTestId("computer-open")).toHaveCount(1);
} }
async function sendAndWait(page: Page, botId: string, text: string) { async function sendAndWait(page: Page, botId: string, text: string) {

View File

@ -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(unterhaltungen.getByText("Talk to your Oos", { exact: true })).toBeVisible();
await expect(projekte.getByText("Projects", { exact: true })).toBeVisible(); await expect(projekte.getByText("Projects", { exact: true })).toBeVisible();
await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).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(); await unterhaltungen.getByTestId("conversation-picker-trigger").click();
const picker = page.getByTestId("conversation-bot-picker"); 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(projekte.getByText("Side project", { exact: true })).toBeVisible();
await expect(unterhaltungenOnly(page).getByText("Side project", { exact: true })).toHaveCount(0); 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 unterhaltungen.getByTestId("conversation-picker-trigger").click();
await expect( await expect(
page page
.getByTestId("conversation-bot-picker") .getByTestId("conversation-bot-picker")
.locator("[data-testid^='conversation-picker-bot-']") .locator("[data-testid^='conversation-picker-bot-']")
.first(), .first(),
).toHaveText("Chief"); ).toHaveText("Projektmanager");
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
// Creating a project that already exists opens it instead of listing it twice. // Creating a project that already exists opens it instead of listing it twice.

View File

@ -7,7 +7,15 @@ async function turnOnSpeaking(page: Page) {
await expect(voice).toBeVisible(); await expect(voice).toBeVisible();
await expect(voice.getByRole("button", { name: "Other voice providers" })).toHaveCount(0); await expect(voice.getByRole("button", { name: "Other voice providers" })).toHaveCount(0);
await expect(voice.getByLabel("API key", { exact: true })).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(); await expect(voice.getByTestId("voice-carousel")).toBeVisible();
return voice; return voice;
} }
@ -25,11 +33,6 @@ test("voice settings pick a voice from the carousel, speak a reply, and open a c
).toHaveCount(1); ).toHaveCount(1);
await captureScreenshot(page, testInfo, "voice-composer"); 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 voice = await turnOnSpeaking(page);
const cards = voice.getByTestId("voice-card"); const cards = voice.getByTestId("voice-card");
// English UI lists only the English voices of the local server. // English UI lists only the English voices of the local server.

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <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="description" content="Deine Oos — immer da, immer wach." />
<meta name="theme-color" content="#07080a" /> <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" /> <meta name="apple-mobile-web-app-title" content="evOo" />
<link rel="icon" href="/favicon.ico" sizes="any" /> <link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />

View File

@ -1,13 +1,16 @@
{ {
"id": "/",
"name": "evOo", "name": "evOo",
"short_name": "evOo", "short_name": "evOo",
"description": "Deine Oos — immer da, immer wach.", "description": "Deine Oos — immer da, immer wach.",
"start_url": "/", "start_url": "/",
"scope": "/",
"display": "standalone", "display": "standalone",
"background_color": "#0f1218", "orientation": "any",
"theme_color": "#0f1218", "background_color": "#07080a",
"theme_color": "#07080a",
"icons": [ "icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" } { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
] ]
} }

View File

@ -34,6 +34,7 @@ self.addEventListener("push", (event) => {
// Same tag as the in-app notification, so one replaces the other. // Same tag as the in-app notification, so one replaces the other.
tag: typeof data.tag === "string" && data.tag ? data.tag : undefined, tag: typeof data.tag === "string" && data.tag ? data.tag : undefined,
icon: "/icon-192.png", icon: "/icon-192.png",
badge: "/icon-192.png",
data: { url: appPath(data.url) }, data: { url: appPath(data.url) },
}, },
); );

View File

@ -1,8 +1,10 @@
import { Trans, useLingui } from "@lingui/react/macro"; import { Trans, useLingui } from "@lingui/react/macro";
import { ChatMarkdown } from "@rakazo/chat-ui/web"; import { ChatMarkdown } from "@rakazo/chat-ui/web";
import { isPaneArtifactMimeType } from "@rakazo/core";
import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web"; import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web";
import { Download, FileText, X } from "lucide-react"; import { Download, Eye, FileText, X } from "lucide-react";
import { type RefObject, useMemo, useRef, useState } from "react"; import type { RefObject } from "react";
import { useMemo, useRef, useState } from "react";
import { useArtifactBytes } from "../hooks/useArtifactBytes"; import { useArtifactBytes } from "../hooks/useArtifactBytes";
import { downloadArtifact } from "../lib/artifact-open"; import { downloadArtifact } from "../lib/artifact-open";
import { formatBytes } from "../lib/format-bytes"; import { formatBytes } from "../lib/format-bytes";
@ -16,63 +18,85 @@ type ArtifactFileCardProps = {
onOpenBeside?: () => void; 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) { export function ArtifactFileCard(props: ArtifactFileCardProps) {
const { t } = useLingui(); 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 previewButton = useRef<HTMLButtonElement>(null);
const closeButton = useRef<HTMLButtonElement>(null); const closeButton = useRef<HTMLButtonElement>(null);
const [previewOpen, setPreviewOpen] = useState(false); const [previewOpen, setPreviewOpen] = useState(false);
const startDownload = () => downloadArtifact(props.artifactId, props.name); const startDownload = () => downloadArtifact(props.artifactId, props.name);
const badgeLabel = shortFileTypeLabel(props.name, props.mimeType);
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>
);
}
return ( return (
<> <>
<div> <div data-testid="artifact-file-card">
<div className="flex min-w-[280px] overflow-hidden rounded-2xl border border-border bg-card text-left text-foreground"> <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 <button
ref={previewButton} ref={previewButton}
type="button" type="button"
aria-label={t`Preview ${props.name}`} aria-label={canPreview ? t`Preview ${props.name}` : t`Download ${props.name}`}
onClick={() => (props.onOpenBeside ? props.onOpenBeside() : setPreviewOpen(true))} onClick={() => {
className="flex min-w-0 flex-1 items-center gap-3 px-4 py-3 text-left hover:bg-accent" 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"> <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>
<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="block truncate text-ui-body font-medium">{props.name}</span>
<span className="mt-0.5 block text-ui-body-sm text-muted-foreground"> <span className="mt-0.5 flex items-center gap-1.5 text-ui-body-sm text-muted-foreground">
{formatBytes(props.size)} <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>
</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>
<button <button
type="button" type="button"
aria-label={t`Download ${props.name}`} aria-label={t`Download ${props.name}`}
title={t`Download ${props.name}`} title={t`Download ${props.name}`}
onClick={startDownload} 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" /> <Download strokeWidth={1.8} className="size-icon-lg" />
</button> </button>
</div> </div>
</div> </div>
{canOpenDialog ? (
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}> <Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
<DialogContent <DialogContent
showCloseButton={false} showCloseButton={false}
@ -80,16 +104,18 @@ export function ArtifactFileCard(props: ArtifactFileCardProps) {
finalFocus={previewButton} finalFocus={previewButton}
className="flex h-[min(88vh,900px)] w-[min(960px,94vw)] flex-col gap-0 overflow-hidden p-0 sm:max-w-none" 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} /> <TextFilePreview {...props} closeButtonRef={closeButton} />
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) : null}
</> </>
); );
} }
function MarkdownPreview({ function TextFilePreview({
artifactId, artifactId,
name, name,
mimeType,
closeButtonRef, closeButtonRef,
}: ArtifactFileCardProps & { closeButtonRef: RefObject<HTMLButtonElement | null> }) { }: ArtifactFileCardProps & { closeButtonRef: RefObject<HTMLButtonElement | null> }) {
const { t } = useLingui(); const { t } = useLingui();
@ -97,8 +123,8 @@ function MarkdownPreview({
const state = useMemo(() => { const state = useMemo(() => {
if (loaded.status !== "ready") return loaded; if (loaded.status !== "ready") return loaded;
try { try {
const markdown = new TextDecoder("utf-8", { fatal: true }).decode(loaded.bytes); const text = new TextDecoder("utf-8", { fatal: true }).decode(loaded.bytes);
return { status: "ready" as const, markdown }; return { status: "ready" as const, text };
} catch { } catch {
return { status: "error" as const, message: t`This file is not valid UTF-8 Markdown.` }; 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"> <div className="rounded-xl border border-destructive/40 bg-destructive/10 px-4 py-3 text-destructive">
{state.message} {state.message}
</div> </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> </article>
</div> </div>

View File

@ -1,17 +1,115 @@
import { selectedAskActionLabel } from "@rakazo/core"; // @vitest-environment jsdom
import { describe, expect, it } from "vitest";
describe("selectedAskActionLabel", () => { import type { ReactNode } from "react";
it("maps a choice answer id to its user-facing label", () => { import { act } from "react";
expect( import type { Root } from "react-dom/client";
selectedAskActionLabel("choice-2", [ import { createRoot } from "react-dom/client";
{ id: "choice-1", label: "Berlin" }, import { afterEach, beforeEach, expect, it, vi } from "vitest";
{ id: "choice-2", label: "Seoul" },
]), vi.mock("@lingui/core/macro", () => ({
).toBe("Seoul"); 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 };
}); });
it("falls back to the answer when an action is unavailable", () => { vi.mock("@rakazo/chat-ui/web", () => ({
expect(selectedAskActionLabel("custom", undefined)).toBe("custom"); 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");
}
}); });

View File

@ -174,20 +174,27 @@ export function AskCard({
</form> </form>
) : askActions?.length ? ( ) : askActions?.length ? (
<div className="mt-3.5 space-y-1.5"> <div className="mt-3.5 space-y-1.5">
{askActions.map((action) => ( {askActions.map((action) => {
const isPrimaryApproval = Boolean(approvalActions && action.id === "allow");
return (
<Button <Button
key={action.id} key={action.id}
variant={approvalActions && action.id === "allow" ? "default" : "outline"} variant={isPrimaryApproval ? "default" : "outline"}
className={cn( 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", "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" isPrimaryApproval
? "" ? ""
: "border-border/80 bg-muted/60 hover:border-foreground/30 hover:bg-muted active:scale-[0.99]", : "border-border/80 bg-muted/60 hover:border-foreground/30 hover:bg-muted active:scale-[0.99]",
)} )}
disabled={submitting} disabled={submitting}
onClick={() => void submitAnswer(action.id)} onClick={() => void submitAnswer(action.id)}
> >
<span className="flex-1 text-ui-body font-medium leading-snug text-foreground"> <span
className={cn(
"flex-1 text-ui-body font-medium leading-snug",
isPrimaryApproval ? "text-primary-foreground" : "text-foreground",
)}
>
{pendingAction === action.id ? ( {pendingAction === action.id ? (
<Trans>Sending…</Trans> <Trans>Sending…</Trans>
) : approvalActions ? ( ) : approvalActions ? (
@ -198,10 +205,16 @@ export function AskCard({
</span> </span>
<ArrowRight <ArrowRight
strokeWidth={2} 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" 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> </Button>
))} );
})}
{!approvalActions ? ( {!approvalActions ? (
<div className="pt-1"> <div className="pt-1">
<Button <Button
@ -226,7 +239,11 @@ export function AskCard({
</Button> </Button>
</div> </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> </div>
); );
} }

View File

@ -3,6 +3,7 @@ import type { MessageBlock } from "@rakazo/contracts";
import { cloudAgentHttpsUrl } from "@rakazo/core"; import { cloudAgentHttpsUrl } from "@rakazo/core";
import { Badge } from "@rakazo/ui-web/components/ui/badge"; import { Badge } from "@rakazo/ui-web/components/ui/badge";
import { Card, CardContent } from "@rakazo/ui-web/components/ui/card"; import { Card, CardContent } from "@rakazo/ui-web/components/ui/card";
import { ArrowUpRight } from "lucide-react";
export function CloudAgentCard({ export function CloudAgentCard({
block, block,
@ -15,8 +16,14 @@ export function CloudAgentCard({
<Card size="sm" className="w-80 max-w-full" data-testid="cloud-agent-card"> <Card size="sm" className="w-80 max-w-full" data-testid="cloud-agent-card">
<CardContent className="flex flex-col gap-2"> <CardContent className="flex flex-col gap-2">
<div className="flex items-start justify-between gap-3"> <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} {block.title}
{href ? (
<ArrowUpRight
aria-hidden="true"
className="size-3.5 shrink-0 text-muted-foreground"
/>
) : null}
</span> </span>
<Badge <Badge
variant="secondary" variant="secondary"
@ -52,7 +59,13 @@ export function CloudAgentCard({
</Card> </Card>
); );
return href ? ( 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} {content}
</a> </a>
) : ( ) : (

View File

@ -74,7 +74,7 @@ export function DualSurfaceDivider({
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={cn( 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", disabled && "pointer-events-none cursor-default opacity-50",
className, className,
)} )}
@ -82,8 +82,8 @@ export function DualSurfaceDivider({
> >
<div <div
className={cn( className={cn(
"h-full w-1 rounded-full bg-border transition-colors group-hover:bg-accent group-active: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", isDragging && "bg-primary opacity-100",
)} )}
/> />
</div> </div>

View File

@ -4,7 +4,13 @@ import { Input, Switch } from "@rakazo/ui-web";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useEffect, useId, useRef, useState } from "react"; import { useEffect, useId, useRef, useState } from "react";
import { rpc } from "../lib/rpc"; 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"; import { SettingsSection } from "./SettingsSection";
const DEFAULT_QUIET_HOURS = { start: "22:00", end: "07:00" }; const DEFAULT_QUIET_HOURS = { start: "22:00", end: "07:00" };
@ -51,6 +57,7 @@ function SettingRow({
export function NotificationSettingsPanel() { export function NotificationSettingsPanel() {
const { t } = useLingui(); const { t } = useLingui();
const pushSupported = webPushSupported(); const pushSupported = webPushSupported();
const iosNeedsHomeScreen = isIosBrowserNeedsHomeScreen();
const [settings, setSettings] = useState<NotificationSettings | null>(null); const [settings, setSettings] = useState<NotificationSettings | null>(null);
const [push, setPush] = useState<PushState>("loading"); const [push, setPush] = useState<PushState>("loading");
const [pushBusy, setPushBusy] = useState(false); const [pushBusy, setPushBusy] = useState(false);
@ -153,6 +160,15 @@ export function NotificationSettingsPanel() {
</p> </p>
) : null} ) : null}
</SettingRow> </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} ) : null}
<SettingRow <SettingRow
label={<Trans>Quiet hours</Trans>} label={<Trans>Quiet hours</Trans>}

View File

@ -254,10 +254,10 @@ export function OoAtelierDialog({
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent <DialogContent
showCloseButton={false} 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" 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"> <DialogTitle className="text-ui-body font-semibold tracking-tight">
<Trans>Atelier</Trans> <Trans>Atelier</Trans>
</DialogTitle> </DialogTitle>
@ -274,19 +274,19 @@ export function OoAtelierDialog({
</button> </button>
</DialogHeader> </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 <BotAvatar
color={isPicture && !draft ? value : encoded} color={isPicture && !draft ? value : encoded}
identity={identity} identity={identity}
status={status} status={status}
size={128} size={120}
moment={preview} moment={preview}
interactive interactive
/> />
</div> </div>
<Tabs defaultValue="body"> <Tabs defaultValue="body" className="flex min-h-0 flex-1 flex-col">
<TabsList className="w-full"> <TabsList className="w-full shrink-0">
<TabsTrigger value="body"> <TabsTrigger value="body">
<Trans>Body</Trans> <Trans>Body</Trans>
</TabsTrigger> </TabsTrigger>
@ -303,7 +303,11 @@ export function OoAtelierDialog({
) : null} ) : null}
</TabsList> </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 <OptionGroup
title={t`Species`} title={t`Species`}
values={OO_SPECIES} values={OO_SPECIES}
@ -397,7 +401,11 @@ export function OoAtelierDialog({
</div> </div>
</TabsContent> </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 <OptionGroup
title={t`Lashes`} title={t`Lashes`}
values={OO_LASHES} values={OO_LASHES}
@ -456,7 +464,11 @@ export function OoAtelierDialog({
/> />
</TabsContent> </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 <OptionGroup
title={t`Glasses`} title={t`Glasses`}
values={OO_EYEWEAR} values={OO_EYEWEAR}
@ -492,7 +504,7 @@ export function OoAtelierDialog({
</TabsContent> </TabsContent>
{allowUpload ? ( {allowUpload ? (
<TabsContent value="upload" className="pt-2"> <TabsContent value="upload" className="min-h-0 flex-1 overflow-y-auto pt-2">
<button <button
type="button" type="button"
aria-label={t`Image upload area`} aria-label={t`Image upload area`}
@ -534,7 +546,7 @@ export function OoAtelierDialog({
) : null} ) : null}
</Tabs> </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"> <div className="flex items-center gap-1">
<button <button
type="button" type="button"

View File

@ -2,7 +2,8 @@ import { Trans, useLingui } from "@lingui/react/macro";
import type { Bot, ComputerStatus } from "@rakazo/contracts"; import type { Bot, ComputerStatus } from "@rakazo/contracts";
import { Button, cn } from "@rakazo/ui-web"; import { Button, cn } from "@rakazo/ui-web";
import { X } from "lucide-react"; 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 { LocalBrowserPreview } from "./LocalBrowserPreview";
import { OoComputerBrowser } from "./OoComputerBrowser"; import { OoComputerBrowser } from "./OoComputerBrowser";
@ -16,8 +17,9 @@ export function InAppBrowser({
computerScreenError, computerScreenError,
sandboxProvider, sandboxProvider,
booting, booting,
onBoot,
onRefreshScreen, onRefreshScreen,
onConnectScreen,
onOpenComputer,
hideCloseButton = false, hideCloseButton = false,
}: { }: {
onClose: () => void; onClose: () => void;
@ -27,13 +29,21 @@ export function InAppBrowser({
computerScreenError: React.ReactNode; computerScreenError: React.ReactNode;
sandboxProvider: string | undefined; sandboxProvider: string | undefined;
booting: boolean; booting: boolean;
onBoot: () => Promise<void>;
onRefreshScreen: () => Promise<unknown>; onRefreshScreen: () => Promise<unknown>;
onConnectScreen: () => Promise<unknown>;
onOpenComputer?: () => void;
hideCloseButton?: boolean; hideCloseButton?: boolean;
}) { }) {
const { t } = useLingui(); const { t } = useLingui();
const [mode, setMode] = useState<InAppBrowserMode>("oo"); const [mode, setMode] = useState<InAppBrowserMode>("oo");
useEffect(() => {
if (!active?.id) return;
return agentPointerFeed.subscribe(active.id, () => {
setMode("oo");
});
}, [active?.id]);
return ( return (
<div className="flex h-full min-h-0 flex-col" data-testid="in-app-browser"> <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"> <div className="flex items-center justify-between gap-2 border-b px-2 py-1">
@ -83,8 +93,9 @@ export function InAppBrowser({
computerScreenError={computerScreenError} computerScreenError={computerScreenError}
sandboxProvider={sandboxProvider} sandboxProvider={sandboxProvider}
booting={booting} booting={booting}
onBoot={onBoot}
onRefreshScreen={onRefreshScreen} 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"> <div className="grid flex-1 place-items-center px-6 text-center text-ui-body-sm text-muted-foreground">

View File

@ -1,8 +1,9 @@
import { Trans, useLingui } from "@lingui/react/macro"; import { Trans, useLingui } from "@lingui/react/macro";
import type { Bot, ComputerStatus } from "@rakazo/contracts"; import type { Bot, ComputerStatus } from "@rakazo/contracts";
import { Button, cn, Input } from "@rakazo/ui-web"; 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 { type FormEvent, useCallback, useEffect, useRef, useState } from "react";
import { agentPointerFeed } from "../../lib/agent-pointer";
import { normalizeBrowserUrl } from "../../lib/browser-url"; import { normalizeBrowserUrl } from "../../lib/browser-url";
import { rpc } from "../../lib/rpc"; import { rpc } from "../../lib/rpc";
import { screenIframeSandbox } from "../../pages/shell/computer-screen"; import { screenIframeSandbox } from "../../pages/shell/computer-screen";
@ -33,8 +34,9 @@ export function OoComputerBrowser({
computerScreenError, computerScreenError,
sandboxProvider, sandboxProvider,
booting, booting,
onBoot,
onRefreshScreen, onRefreshScreen,
onConnectScreen,
onOpenComputer,
}: { }: {
active: Bot; active: Bot;
computer: ComputerStatus | null; computer: ComputerStatus | null;
@ -42,8 +44,11 @@ export function OoComputerBrowser({
computerScreenError: React.ReactNode; computerScreenError: React.ReactNode;
sandboxProvider: string | undefined; sandboxProvider: string | undefined;
booting: boolean; booting: boolean;
onBoot: () => Promise<void>;
onRefreshScreen: () => Promise<unknown>; 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 { t } = useLingui();
const [chrome, setChrome] = useState<ChromeState>(EMPTY_CHROME); const [chrome, setChrome] = useState<ChromeState>(EMPTY_CHROME);
@ -73,10 +78,26 @@ export function OoComputerBrowser({
}, [computer?.state, refreshChrome]); }, [computer?.state, refreshChrome]);
useEffect(() => { useEffect(() => {
if (computer?.state === "running" && (embeddedScreenUrl || computer?.kind === "desktop")) if (computer?.state !== "running") return;
return; let settleTimer: number | undefined;
void onBoot().catch(() => undefined); const unsubscribe = agentPointerFeed.subscribe(active.id, () => {
}, [active.id, computer?.kind, computer?.state, embeddedScreenUrl, onBoot]); 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( const runNav = useCallback(
async (task: () => Promise<ChromeState>) => { async (task: () => Promise<ChromeState>) => {
@ -200,6 +221,18 @@ export function OoComputerBrowser({
> >
<ExternalLink strokeWidth={1.7} className="size-icon-md" /> <ExternalLink strokeWidth={1.7} className="size-icon-md" />
</Button> </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 <div
aria-hidden aria-hidden
className={cn( className={cn(

View File

@ -15,14 +15,14 @@ type OnboardingDiscoveryShellProps = {
compact?: boolean; compact?: boolean;
}; };
function sourceLabel(id: DiscoverySourceId, t: ReturnType<typeof useLingui>["t"]): string { function sourceLabel(id: DiscoverySourceId): ReactNode {
switch (id) { switch (id) {
case "profile-basics": case "profile-basics":
return t`Your profile`; return <Trans>Your profile</Trans>;
case "connected-tools": case "connected-tools":
return t`Connected tools`; return <Trans>Connected tools</Trans>;
case "specialist-crew": case "specialist-crew":
return t`Specialist crew`; return <Trans>Specialist crew</Trans>;
default: { default: {
const never: never = id; const never: never = id;
return never; return never;
@ -83,7 +83,7 @@ export function OnboardingDiscoveryShell({
> >
{active ? ( {active ? (
<> <>
{sourceLabel(active.id, t)} {sourceLabel(active.id)}
{active.status === "found" && active.detail === DISCOVERY_MAIN_BOT_ROLE ? ( {active.status === "found" && active.detail === DISCOVERY_MAIN_BOT_ROLE ? (
<> <>
{" · "} {" · "}

View File

@ -120,7 +120,7 @@ export function OnboardingOoDesigner({ config, onChange, onContinue }: Onboardin
<Trans>What feels like you?</Trans> <Trans>What feels like you?</Trans>
</p> </p>
<div <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" role="tablist"
aria-label={t`Look families`} aria-label={t`Look families`}
> >

View File

@ -17,7 +17,7 @@ export function VoiceLiveText({ text, reducedMotion }: { text: string; reducedMo
return ( return (
<p <p
dir="auto" 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) => ( {captionWords(text).map((word, index) => (
<span key={`${utterance}-${index}`} className={reducedMotion ? undefined : "voice-word-in"}> <span key={`${utterance}-${index}`} className={reducedMotion ? undefined : "voice-word-in"}>

View File

@ -90,21 +90,52 @@ export function VoiceStage({
}, []); }, []);
return ( return (
<div <section
data-testid="call-view" data-testid="call-view"
data-theme="dark" data-theme="dark"
data-phase={phase} data-phase={phase}
role="dialog"
aria-modal="true"
aria-labelledby={titleId} 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"> <h1 id={titleId} className="sr-only">
{botName} {botName}
</h1> </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 ? ( {awarenessLabel && awarenessEvent ? (
<div className="w-full max-w-md"> <div className="pointer-events-auto w-full max-w-lg">
<VoiceAwarenessStrip <VoiceAwarenessStrip
event={awarenessEvent} event={awarenessEvent}
label={awarenessLabel} label={awarenessLabel}
@ -116,7 +147,7 @@ export function VoiceStage({
<section <section
data-testid="voice-panel" 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 <VoiceAurora
botColor={botColor} botColor={botColor}
@ -125,28 +156,30 @@ export function VoiceStage({
reducedMotion={reducedMotion} reducedMotion={reducedMotion}
/> />
<div className="relative flex flex-1 flex-col items-center justify-center gap-6 px-7 pt-10 pb-4"> <div className="relative flex items-center gap-3.5 px-4 pt-3.5 pb-2.5">
<div title={`${botName} · ${botTitle}`}> <div className="shrink-0" title={`${botName} · ${botTitle}`}>
<BotAvatar <BotAvatar
color={botColor} color={botColor}
identity={botId} identity={botId}
status={botStatus} status={botStatus}
pose={mascotPose} pose={mascotPose}
size={88} size={44}
/> />
</div> </div>
<div className="min-w-0 flex-1 text-start">
<VoiceLiveText text={subtitle} reducedMotion={reducedMotion} /> <VoiceLiveText text={subtitle} reducedMotion={reducedMotion} />
{error ? ( {error ? (
<p role="alert" className="text-center text-ui-body-sm font-medium text-destructive"> <p role="alert" className="mt-1 text-ui-body-sm font-medium text-destructive">
{error} {error}
</p> </p>
) : null} ) : null}
</div> </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 <span
aria-live="polite" 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} {phaseLabel}
</span> </span>
@ -187,39 +220,6 @@ export function VoiceStage({
</Button> </Button>
</div> </div>
</section> </section>
</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>
); );
} }

View File

@ -42,4 +42,20 @@ describe("ChatMarkdown", () => {
expect(html).toContain('aria-label="Copy code"'); expect(html).toContain('aria-label="Copy code"');
expect(html).toContain("rk-chat-markdown-copy"); 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"');
});
}); });

View File

@ -9,6 +9,37 @@ import {
StreamingVad, StreamingVad,
} from "@rakazo/core"; } from "@rakazo/core";
import { selectedSpaceId, withSpaceHeaders } from "./rpc.js"; 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"; export type DictationMode = "hold" | "endpoint";
@ -205,7 +236,7 @@ export class Dictation {
const rec = new Ctor(); const rec = new Ctor();
rec.continuous = true; rec.continuous = true;
rec.interimResults = true; rec.interimResults = true;
rec.lang = navigator.language || "en-US"; rec.lang = speechRecognitionLang();
rec.onresult = (event) => { rec.onresult = (event) => {
if (this.token !== mine) return; if (this.token !== mine) return;
let transcript = ""; let transcript = "";

View File

@ -98,13 +98,25 @@ describe("isToday", () => {
}); });
describe("buildHeuteBrief", () => { 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({ const brief = buildHeuteBrief({
bots: [ bots: [
bot({ id: "bot-1", unread: true }), bot({ id: "bot-1", unread: true, status: "waiting_input" }),
bot({ id: "bot-2", unread: true, updatedAt: "2026-09-22T08:00:00.000Z", name: "Steve" }), 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: [ activeRuns: [
run({ runId: "run-1", botId: "bot-1" }), run({ runId: "run-1", botId: "bot-1" }),
run({ runId: "run-3", botId: "bot-4", botName: "Worker", status: "running" }), run({ runId: "run-3", botId: "bot-4", botName: "Worker", status: "running" }),
@ -157,7 +169,7 @@ describe("buildHeuteBrief", () => {
it("caps needs-you at three items", () => { it("caps needs-you at three items", () => {
const bots = Array.from({ length: 5 }, (_, index) => 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({ const brief = buildHeuteBrief({
bots, bots,
@ -169,6 +181,57 @@ describe("buildHeuteBrief", () => {
expect(brief.needsYou).toHaveLength(HEUTE_NEEDS_YOU_CAP); expect(brief.needsYou).toHaveLength(HEUTE_NEEDS_YOU_CAP);
expect(brief.actionCount).toBe(5); 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 { function habit(overrides: Partial<HabitToday> = {}): HabitToday {

View File

@ -1,5 +1,18 @@
import type { Bot, CoachToday, Group, RunActivityRow } from "@rakazo/contracts"; import type {
import { isActionableRun, isDoingStatus, isFailedRoutine } from "./agent-activity"; Bot,
CoachToday,
Group,
RunActivityRow,
Space,
SpaceBot,
SpaceGroup,
} from "@rakazo/contracts";
import {
isActionableRun,
isDoingStatus,
isFailedRoutine,
isNeedsYouStatus,
} from "./agent-activity";
export type JetztRunItem = { export type JetztRunItem = {
kind: "run"; kind: "run";
@ -9,7 +22,7 @@ export type JetztRunItem = {
export type JetztUnreadItem = { export type JetztUnreadItem = {
kind: "unread"; kind: "unread";
chatKind: "bot" | "group"; chatKind: "bot" | "group";
chat: Bot | Group; chat: SpaceBot | (SpaceGroup & { leadBotId?: string | null });
}; };
export type JetztItem = JetztRunItem | JetztUnreadItem; export type JetztItem = JetztRunItem | JetztUnreadItem;
@ -76,16 +89,33 @@ function chatTargetKey(chatKind: "bot" | "group", chatId: string): string {
} }
export function buildHeuteBrief(params: { export function buildHeuteBrief(params: {
bots: Bot[]; bots: readonly SpaceBot[];
groups: Group[]; groups: readonly (SpaceGroup & { leadBotId?: string | null })[];
spaces?: readonly Pick<Space, "id" | "bots" | "groups">[];
activeRuns: RunActivityRow[]; activeRuns: RunActivityRow[];
recentRuns: RunActivityRow[]; recentRuns: RunActivityRow[];
now?: Date; now?: Date;
}): HeuteBriefData { }): HeuteBriefData {
const { bots, groups, activeRuns, recentRuns, now = new Date() } = params; const { bots, groups, spaces = [], activeRuns, recentRuns, now = new Date() } = params;
const needsYouAll: JetztItem[] = []; const needsYouAll: JetztItem[] = [];
const coveredTargets = new Set<string>(); 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 = [ const actionableRuns = [
...activeRuns.filter(isActionableRun), ...activeRuns.filter(isActionableRun),
...recentRuns.filter(isFailedRoutine), ...recentRuns.filter(isFailedRoutine),
@ -98,12 +128,20 @@ export function buildHeuteBrief(params: {
coveredTargets.add(runTargetKey(run)); coveredTargets.add(runTargetKey(run));
} }
const botStatusById = new Map(allBots.map((bot) => [bot.id, bot.status]));
const unreadChats: JetztUnreadItem[] = [ const unreadChats: JetztUnreadItem[] = [
...bots ...allBots
.filter((bot) => bot.unread) .filter((bot) => bot.unread && isNeedsYouStatus(bot.status))
.map((bot) => ({ kind: "unread" as const, chatKind: "bot" as const, chat: bot })), .map((bot) => ({ kind: "unread" as const, chatKind: "bot" as const, chat: bot })),
...groups ...allGroups
.filter((group) => group.unread) .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) => ({ .map((group) => ({
kind: "unread" as const, kind: "unread" as const,
chatKind: "group" as const, chatKind: "group" as const,

View File

@ -5,8 +5,8 @@
type AudioContextCtor = new () => AudioContext; type AudioContextCtor = new () => AudioContext;
/** Master output ~−15 LUFS for short procedural peaks. */ /** Master output ~−12 LUFS for clear, warm procedural peaks. */
const SFX_MASTER_GAIN = 0.58; const SFX_MASTER_GAIN = 0.85;
/** Duck depth when voice lane active (TTS or mic wash). */ /** Duck depth when voice lane active (TTS or mic wash). */
const SFX_DUCK_RATIO = 0.28; const SFX_DUCK_RATIO = 0.28;
const DUCK_RAMP_S = 0.065; const DUCK_RAMP_S = 0.065;
@ -22,8 +22,10 @@ class OoAudioBus {
private ctx: AudioContext | null = null; private ctx: AudioContext | null = null;
private sfxGain: GainNode | null = null; private sfxGain: GainNode | null = null;
private voiceHold = 0; private voiceHold = 0;
private unlockBound = false;
ensureContext(): AudioContext | null { ensureContext(): AudioContext | null {
this.bindGestureUnlock();
if (this.ctx && this.ctx.state !== "closed") return this.ctx; if (this.ctx && this.ctx.state !== "closed") return this.ctx;
const Ctor = audioContextCtor(); const Ctor = audioContextCtor();
if (!Ctor) return null; 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. */ /** Route for oo-sound / oo-vocals / oo-voice-mark oscillators. */
sfxDestination(ctx: AudioContext): AudioNode { sfxDestination(ctx: AudioContext): AudioNode {
if (ctx !== this.ctx) { if (ctx !== this.ctx) {

View File

@ -68,61 +68,62 @@ function playCueOnContext(ctx: AudioContext, cue: OoSoundCue) {
switch (cue) { switch (cue) {
case "app-start": case "app-start":
sweep(ctx, master, 523.25, 659.25, t0, 0.14, { gain: 0.038, type: "sine" }); sweep(ctx, master, 523.25, 659.25, t0, 0.16, { gain: 0.11, type: "sine" });
sweep(ctx, master, 659.25, 493.88, t0 + 0.1, 0.18, { gain: 0.032, type: "triangle" }); sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.2, { gain: 0.095, type: "triangle" });
break; break;
case "interact": case "interact":
tone(ctx, master, 1046.5, t0, 0.06, { tone(ctx, master, 880, t0, 0.075, {
gain: 0.018, gain: 0.065,
attack: 0.003, attack: 0.004,
release: 0.04, release: 0.045,
type: "triangle", type: "triangle",
}); });
break; break;
case "success-resolve": case "success-resolve":
case "work-complete": case "work-complete":
// mint → gold resolve (~270 ms) // mint → gold resolve (~270 ms)
sweep(ctx, master, 523.25, 659.25, t0, 0.12, { gain: 0.042, 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.036, type: "triangle" }); 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.03, type: "sine" }); tone(ctx, master, 880, t0 + 0.2, 0.28, { gain: 0.09, type: "sine" });
break; break;
case "need-you": case "need-you":
sweep(ctx, master, 220, 330, t0, 0.1, { gain: 0.042, type: "triangle" }); sweep(ctx, master, 261.63, 392, t0, 0.12, { gain: 0.125, type: "triangle" });
tone(ctx, master, 277.18, t0 + 0.09, 0.12, { gain: 0.035, release: 0.08 }); tone(ctx, master, 523.25, t0 + 0.1, 0.18, { gain: 0.11, release: 0.1 });
break; break;
case "listening-start": case "listening-start":
case "voice_listen": case "voice_listen":
tone(ctx, master, 220, t0, 0.22, { gain: 0.045, type: "triangle" }); tone(ctx, master, 293.66, t0, 0.2, { gain: 0.11, type: "triangle" });
tone(ctx, master, 330, t0 + 0.04, 0.28, { gain: 0.035 }); tone(ctx, master, 440, t0 + 0.05, 0.24, { gain: 0.095 });
break; break;
case "listening-stop": 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; break;
case "speaking-start": 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; break;
case "speaking-done": case "speaking-done":
tone(ctx, master, 523.25, t0, 0.24, { gain: 0.035 }); tone(ctx, master, 523.25, t0, 0.2, { gain: 0.085 });
tone(ctx, master, 392, t0 + 0.08, 0.3, { gain: 0.028, release: 0.14 }); tone(ctx, master, 392, t0 + 0.08, 0.26, { gain: 0.075, release: 0.14 });
break; break;
case "voice_bargein": 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; break;
case "attention": case "attention":
tone(ctx, master, 277.18, t0, 0.16, { gain: 0.05, type: "triangle" }); tone(ctx, master, 329.63, t0, 0.16, { gain: 0.13, type: "triangle" });
tone(ctx, master, 233.08, t0 + 0.12, 0.22, { gain: 0.045 }); tone(ctx, master, 261.63, t0 + 0.12, 0.22, { gain: 0.115 });
break; break;
case "message-sent": 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; break;
case "handoff": case "handoff":
tone(ctx, master, 349.23, t0, 0.2, { gain: 0.04 }); tone(ctx, master, 392, t0, 0.18, { gain: 0.11 });
tone(ctx, master, 466.16, t0 + 0.1, 0.24, { gain: 0.035, type: "triangle" }); tone(ctx, master, 523.25, t0 + 0.09, 0.22, { gain: 0.1, type: "triangle" });
break; break;
case "group-spark": case "group-spark":
tone(ctx, master, 587.33, t0, 0.11, { gain: 0.032, attack: 0.004, type: "triangle" }); 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.028, type: "sine" }); tone(ctx, master, 739.99, t0 + 0.06, 0.14, { gain: 0.085, type: "sine" });
tone(ctx, master, 880, t0 + 0.12, 0.1, { gain: 0.022, release: 0.05 }); tone(ctx, master, 880, t0 + 0.12, 0.12, { gain: 0.075, release: 0.06 });
break; break;
case "routine_start": case "routine_start":
tone(ctx, master, 392, t0, 0.08, { gain: 0.025, attack: 0.005, release: 0.03, type: "sine" }); tone(ctx, master, 392, t0, 0.08, { gain: 0.025, attack: 0.005, release: 0.03, type: "sine" });

View File

@ -3,8 +3,8 @@ import { readOoSoundEnabled } from "./oo-sound-pref";
export type OoVocalReaction = "ouh" | "mhm" | "aaah" | "mm" | "nod-down" | "nod-up" | "hm"; export type OoVocalReaction = "ouh" | "mhm" | "aaah" | "mm" | "nod-down" | "nod-up" | "hm";
const COOLDOWN_MIN_MS = 8_000; const COOLDOWN_MIN_MS = 1_600;
const COOLDOWN_MAX_MS = 12_000; const COOLDOWN_MAX_MS = 2_800;
let lastPlayedAt = 0; let lastPlayedAt = 0;
let nextCooldownMs = COOLDOWN_MIN_MS; let nextCooldownMs = COOLDOWN_MIN_MS;
@ -28,17 +28,23 @@ function vocalFormant(
) { ) {
const osc = ctx.createOscillator(); const osc = ctx.createOscillator();
const gain = ctx.createGain(); const gain = ctx.createGain();
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(boostedPeak, startAt + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration);
if (typeof ctx.createBiquadFilter === "function") {
const filter = ctx.createBiquadFilter(); const filter = ctx.createBiquadFilter();
filter.type = "bandpass"; filter.type = "bandpass";
filter.frequency.value = baseHz * 2.4; filter.frequency.value = baseHz * 1.6;
filter.Q.value = 4.5; filter.Q.value = 1.8;
osc.type = "triangle";
osc.frequency.setValueAtTime(baseHz, startAt);
gain.gain.setValueAtTime(0.0001, startAt);
gain.gain.exponentialRampToValueAtTime(gainPeak, startAt + 0.025);
gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration);
osc.connect(filter); osc.connect(filter);
filter.connect(gain); filter.connect(gain);
} else {
osc.connect(gain);
}
gain.connect(destination); gain.connect(destination);
osc.start(startAt); osc.start(startAt);
osc.stop(startAt + duration + 0.02); osc.stop(startAt + duration + 0.02);

View File

@ -37,4 +37,10 @@ describe("queuedFollowUps", () => {
const synthetic = userMessage("progress:1", "2026-01-01T10:00:05.000Z"); const synthetic = userMessage("progress:1", "2026-01-01T10:00:05.000Z");
expect(queuedFollowUps([synthetic], RUN_START, new Map())).toEqual([]); 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([]);
});
}); });

View File

@ -1,25 +1 @@
import type { ThreadMessage } from "@rakazo/contracts"; export { queuedFollowUps } from "@rakazo/core";
/** 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),
);
}

View File

@ -215,6 +215,53 @@ describe("Speaker", () => {
expect(speaker.state.status).toBe("idle"); expect(speaker.state.status).toBe("idle");
expect(speaker.state.error).toBeUndefined(); 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", () => { describe("UtteranceStreamer", () => {

View File

@ -1,5 +1,7 @@
import { ClauseSegmenter, readBoundedResponseBytes, speakable } from "@rakazo/core"; import { ClauseSegmenter, readBoundedResponseBytes, speakable } from "@rakazo/core";
import { ooAudioBus } from "./oo-audio-bus.js";
import { rpc, selectedSpaceId, withSpaceHeaders } from "./rpc.js"; import { rpc, selectedSpaceId, withSpaceHeaders } from "./rpc.js";
import { readUiLocale } from "./ui-locale.js";
export type SpeechStatus = "idle" | "preparing" | "speaking"; export type SpeechStatus = "idle" | "preparing" | "speaking";
@ -76,7 +78,19 @@ export class Speaker {
if (this.snapshot.status !== "idle" || this.snapshot.error) this.set(IDLE); if (this.snapshot.status !== "idle" || this.snapshot.error) this.set(IDLE);
} }
private bufferSource: AudioBufferSourceNode | null = null;
private teardownAudio() { 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) { if (this.audio) {
this.audio.pause(); this.audio.pause();
this.audio.src = ""; this.audio.src = "";
@ -225,14 +239,49 @@ export class Speaker {
audio.onended = null; audio.onended = null;
audio.onerror = null; audio.onerror = null;
if (this.settlePlayback === done) this.settlePlayback = null; if (this.settlePlayback === done) this.settlePlayback = null;
if (this.audio === audio) this.teardownAudio(); if (this.audio === audio || this.bufferSource) this.teardownAudio();
resolve(ok); resolve(ok);
}; };
this.settlePlayback = done; this.settlePlayback = done;
audio.onended = () => done(true); audio.onended = () => done(true);
audio.onerror = () => done(false); audio.onerror = () => {
audio.play().catch(() => done(false)); 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( async tryBrowserFallback(
@ -255,17 +304,65 @@ export class Speaker {
return new Promise((resolve) => { return new Promise((resolve) => {
try { try {
window.speechSynthesis.cancel(); window.speechSynthesis.cancel();
window.speechSynthesis.resume?.();
const utterance = new SpeechSynthesisUtterance(text); const utterance = new SpeechSynthesisUtterance(text);
const voices = window.speechSynthesis.getVoices?.() ?? []; const voices = window.speechSynthesis.getVoices?.() ?? [];
const isGerman = /[äöüß]|(?:und|der|die|das|ist|nicht|für|mit|den|dem|des)/i.test(text); const uiLocale = readUiLocale();
const langPrefix = isGerman ? "de" : "en"; 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 = const matchedVoice =
voices.find((v) => v.lang.startsWith(langPrefix) && !v.localService) || (wantsMale &&
voices.find((v) => v.lang.startsWith(langPrefix)) || (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]; voices[0];
if (matchedVoice) utterance.voice = matchedVoice; if (matchedVoice) utterance.voice = matchedVoice;
utterance.rate = 1.05;
// 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; 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({ this.set({
status: "speaking", status: "speaking",

View File

@ -100,3 +100,37 @@ export async function disableWebPush(): Promise<void> {
await rpc.notifications.webPush.unsubscribe({ endpoint: subscription.endpoint }); await rpc.notifications.webPush.unsubscribe({ endpoint: subscription.endpoint });
await subscription.unsubscribe().catch(() => undefined); 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);
}
}

View File

@ -161,11 +161,6 @@ msgstr "Browser von {0}"
msgid "{0}'s browser appears here when the computer is running." msgid "{0}'s browser appears here when the computer is running."
msgstr "{0}s Browser erscheint hier, wenn der Computer läuft." 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 #: src/pages/HeuteBrief.tsx
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
msgstr "{actionCount, plural, one {# Sache braucht dich} other {# Sachen brauchen dich}}" 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…" msgid "{botName} clears the path…"
msgstr "{botName} räumt den Weg frei…" 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 #: src/lib/voice-persona-copy.ts
msgid "{botName} is on it…" msgid "{botName} is on it…"
msgstr "{botName} ist dran…" 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/lib/voice-persona-copy.ts
#: src/pages/CallView.tsx #: src/pages/CallView.tsx
msgid "{botName} is working…" msgid "{botName} is working…"
@ -364,6 +387,7 @@ msgstr "Noch {remaining} · Bot beobachtet nur"
msgid "{size} B" msgid "{size} B"
msgstr "{size} B" msgstr "{size} B"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "{stepCount, plural, one {# step} other {# steps}}" msgid "{stepCount, plural, one {# step} other {# steps}}"
msgstr "{stepCount, plural, one {# Schritt} other {# Schritte}}" msgstr "{stepCount, plural, one {# Schritt} other {# Schritte}}"
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
msgid "{toolCount, plural, one {# tool} other {# tools}}" msgid "{toolCount, plural, one {# tool} other {# tools}}"
msgstr "{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/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "/Users/you/projects/acme" msgid "/Users/you/projects/acme"
@ -433,7 +453,7 @@ msgstr "Über Gruppen"
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About spaces" msgid "About spaces"
msgstr "Über Spaces" msgstr "Über Projekte"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
msgid "About you" msgid "About you"
@ -544,11 +564,16 @@ msgstr "Weiteres hinzufügen"
msgid "Add as projects" msgid "Add as projects"
msgstr "Als Projekte hinzufügen" 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 #: src/pages/PluginsOverlay.tsx
msgid "Add Executor" msgid "Add Executor"
msgstr "Executor hinzufügen" msgstr "Executor hinzufügen"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/project-overview.tsx
msgid "Add goal" msgid "Add goal"
msgstr "Ziel hinzufügen" msgstr "Ziel hinzufügen"
@ -737,6 +762,14 @@ msgstr "Immer erlaubt"
msgid "Analysiert die Aufgabe und führt Tools aus…" msgid "Analysiert die Aufgabe und führt Tools aus…"
msgstr "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 #: src/components/AskCard.tsx
msgid "Answer" msgid "Answer"
msgstr "Antwort" msgstr "Antwort"
@ -996,7 +1029,7 @@ msgstr "Körper"
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Live-Desktop wird gestartet…" msgstr "Live-Desktop wird gestartet…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Computer von {0} wird gestartet" msgstr "Computer von {0} wird gestartet"
@ -1019,10 +1052,6 @@ msgstr "Bot"
msgid "Bot screen" msgid "Bot screen"
msgstr "Bot-Bildschirm" msgstr "Bot-Bildschirm"
#: src/pages/shell/computer-panel-preview.tsx
msgid "Bot screen preview"
msgstr "Bot-Bildschirmvorschau"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Bot to link" msgid "Bot to link"
msgstr "Zu verknüpfender Bot" 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." 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." 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 #: src/components/OoAtelier.tsx
msgid "Bow" msgid "Bow"
msgstr "Schleife" msgstr "Schleife"
@ -1064,7 +1089,6 @@ msgstr "Durchsuchen"
msgid "Browse MCP servers" msgid "Browse MCP servers"
msgstr "MCP-Server durchsuchen" msgstr "MCP-Server durchsuchen"
#: src/pages/Shell.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Browser" msgid "Browser"
msgstr "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." 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." 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 #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Durch die Bereitstellung konfiguriert" msgstr "Durch die Bereitstellung konfiguriert"
@ -1584,6 +1612,10 @@ msgstr "Verbunden, {0} wird verwendet."
msgid "Connected app" msgid "Connected app"
msgstr "Verbundene App" msgstr "Verbundene App"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr "Verbundene Tools"
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Verbunden. Die Tools sind ab deiner nächsten Nachricht verfügbar." 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 #: src/pages/shell/bot-picker.tsx
msgid "Create new Space" msgid "Create new Space"
msgstr "Neuen Space erstellen" msgstr "Neues Projekt erstellen"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
msgid "Create project" msgid "Create project"
@ -2086,7 +2118,7 @@ msgstr "Projekt erstellen"
#: src/components/AskCard.tsx #: src/components/AskCard.tsx
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
msgid "Create space" msgid "Create space"
msgstr "Space erstellen" msgstr "Projekt erstellen"
#: src/components/integrations/GithubConnect.tsx #: src/components/integrations/GithubConnect.tsx
msgid "Create token on GitHub" msgid "Create token on GitHub"
@ -2241,7 +2273,7 @@ msgstr "Erinnerungen ebenfalls löschen"
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Delete space" msgid "Delete space"
msgstr "Space löschen" msgstr "Projekt löschen"
#: src/pages/PrivacySettingsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx
msgid "Delete your account?" msgid "Delete your account?"
@ -2322,6 +2354,7 @@ msgstr "Verbindung wird getrennt…"
#: src/components/ComputerUpdateProgress.tsx #: src/components/ComputerUpdateProgress.tsx
#: src/components/teach/TeachComputerOverlay.tsx #: src/components/teach/TeachComputerOverlay.tsx
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Dismiss" msgid "Dismiss"
msgstr "Ausblenden" msgstr "Ausblenden"
@ -2381,7 +2414,9 @@ msgstr "Kein Stress — wir sind jetzt da."
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/project-overview.tsx #: src/pages/shell/project-overview.tsx
#: src/pages/shell/transcript.tsx
msgid "Done" msgid "Done"
msgstr "Fertig" msgstr "Fertig"
@ -2394,8 +2429,11 @@ msgstr "Erledigt"
msgid "Dot" msgid "Dot"
msgstr "Punkt" msgstr "Punkt"
#. placeholder {0}: activeItem.name
#. placeholder {0}: item.name
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {0}" msgid "Download {0}"
msgstr "{0} herunterladen" msgstr "{0} herunterladen"
@ -2403,7 +2441,6 @@ msgstr "{0} herunterladen"
#: src/components/ArtifactPane.tsx #: src/components/ArtifactPane.tsx
#: src/components/media/MediaCard.tsx #: src/components/media/MediaCard.tsx
#: src/components/media/MediaPlayer.tsx #: src/components/media/MediaPlayer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {name}" msgid "Download {name}"
msgstr "{name} herunterladen" msgstr "{name} herunterladen"
@ -2589,6 +2626,8 @@ msgid "Expand"
msgstr "Erweitern" msgstr "Erweitern"
#. placeholder {0}: group.title #. placeholder {0}: group.title
#. placeholder {0}: item.name
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/sidebar-group-header.tsx #: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}" msgid "Expand {0}"
msgstr "{0} ausklappen" msgstr "{0} ausklappen"
@ -2660,6 +2699,10 @@ msgstr "Workspace-Dateien konnten nicht geladen werden"
msgid "Failed to open file" msgid "Failed to open file"
msgstr "Datei konnte nicht geöffnet werden" 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 #: src/pages/Shell.tsx
msgid "Failed to send message" msgid "Failed to send message"
msgstr "Nachricht konnte nicht gesendet werden" 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…" msgstr "Das sehe ich in den Logs…"
#: src/pages/VoiceSettingsOverlay.tsx #: src/pages/VoiceSettingsOverlay.tsx
msgid "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 "Hallo, so klinge ich, wenn ich Antworten vorlese." msgstr "Hallo, so klinge ich, wenn wir miteinander sprechen."
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Hide bots" msgid "Hide bots"
msgstr "Bots ausblenden" msgstr "Bots ausblenden"
#: src/pages/shell/transcript.tsx
msgid "Hide details"
msgstr "Details ausblenden"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Hide password" msgid "Hide password"
msgstr "Passwort verbergen" msgstr "Passwort verbergen"
@ -3080,6 +3128,7 @@ msgstr "Bot importieren"
msgid "Import OpenAPI JSON" msgid "Import OpenAPI JSON"
msgstr "OpenAPI-JSON importieren" msgstr "OpenAPI-JSON importieren"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "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." 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." 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 #: src/pages/shell/transcript.tsx
msgid "Jump to latest" msgid "Jump to latest"
msgstr "Zur neuesten Nachricht springen" msgstr "Zur neuesten Nachricht springen"
@ -3174,6 +3227,10 @@ msgstr "Zur neuesten Nachricht springen"
msgid "Jump to replied message" msgid "Jump to replied message"
msgstr "Zur beantworteten Nachricht springen" 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/ActivityList.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
msgid "just now" msgid "just now"
@ -3771,9 +3828,10 @@ msgstr "Neuer Skill"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
msgid "New space" msgid "New space"
msgstr "Neuer Space" msgstr "Neues Projekt"
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/pages/shell/message-cards.tsx
msgid "Next" msgid "Next"
msgstr "Weiter" msgstr "Weiter"
@ -3885,6 +3943,10 @@ msgstr "Noch keine Projekte."
msgid "No providers found." msgid "No providers found."
msgstr "Keine Anbieter gefunden." msgstr "Keine Anbieter gefunden."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr "Keine letzte Aktivität"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "Keine Remote-MCP-Server gefunden" msgstr "Keine Remote-MCP-Server gefunden"
@ -3899,6 +3961,7 @@ msgid "No results"
msgstr "Keine Ergebnisse" msgstr "Keine Ergebnisse"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "No runs yet" msgid "No runs yet"
msgstr "Noch keine Ausführungen" msgstr "Noch keine Ausführungen"
@ -3957,6 +4020,7 @@ msgid "Notifications"
msgstr "Benachrichtigungen" msgstr "Benachrichtigungen"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Now" msgid "Now"
msgstr "Jetzt" msgstr "Jetzt"
@ -4027,7 +4091,6 @@ msgid "Oo name"
msgstr "Name des Oo" msgstr "Name des Oo"
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
msgstr "Öffnen" msgstr "Öffnen"
@ -4056,7 +4119,7 @@ msgstr "Unterhaltung öffnen"
msgid "Open in browser" msgid "Open in browser"
msgstr "Im Browser öffnen" msgstr "Im Browser öffnen"
#: src/pages/shell/computer-panel-preview.tsx #: src/components/browser/OoComputerBrowser.tsx
msgid "Open in full window" msgid "Open in full window"
msgstr "In vollständigem Fenster öffnen" msgstr "In vollständigem Fenster öffnen"
@ -4288,10 +4351,6 @@ msgstr "Privater Arbeitsbereich mit eigenen Bots und Gruppen."
msgid "Project" msgid "Project"
msgstr "Projekt" msgstr "Projekt"
#: src/pages/Shell.tsx
msgid "Project · sessions & folders"
msgstr "Projekt · Sessions & Ordner"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "Project folder" msgid "Project folder"
@ -4399,6 +4458,10 @@ msgstr "Lese den Stacktrace…"
msgid "Ready" msgid "Ready"
msgstr "Bereit" msgstr "Bereit"
#: src/pages/shell/project-overview.tsx
msgid "Ready"
msgstr "Bereit"
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Reasoning effort" msgid "Reasoning effort"
msgstr "Denkaufwand" msgstr "Denkaufwand"
@ -4412,6 +4475,7 @@ msgid "Recall only"
msgstr "Nur abrufen" msgstr "Nur abrufen"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Recent" msgid "Recent"
msgstr "Kürzlich" msgstr "Kürzlich"
@ -4528,6 +4592,10 @@ msgstr "{0} entfernen"
msgid "Remove folder" msgid "Remove folder"
msgstr "Ordner entfernen" msgstr "Ordner entfernen"
#: src/pages/shell/transcript.tsx
msgid "Remove from queue"
msgstr "Aus der Warteschlange entfernen"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
msgid "Remove Git event" msgid "Remove Git event"
msgstr "Git-Ereignis entfernen" msgstr "Git-Ereignis entfernen"
@ -4653,6 +4721,11 @@ msgstr "Stellt den zuletzt gespeicherten Arbeitsbereich wieder her. Nicht gespei
msgid "Restoring your workspace" msgid "Restoring your workspace"
msgstr "Dein Arbeitsbereich wird wiederhergestellt" msgstr "Dein Arbeitsbereich wird wiederhergestellt"
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Resume"
msgstr "Fortsetzen"
#: src/components/FolderPickerDialog.tsx #: src/components/FolderPickerDialog.tsx
msgid "Retry" msgid "Retry"
msgstr "Wiederholen" msgstr "Wiederholen"
@ -4700,11 +4773,13 @@ msgstr "Rund"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/routine-event-line.tsx #: src/pages/shell/routine-event-line.tsx
msgid "Routine" msgid "Routine"
msgstr "Routine" msgstr "Routine"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Routines" msgid "Routines"
msgstr "Routinen" msgstr "Routinen"
@ -4718,6 +4793,7 @@ msgid "Run at"
msgstr "Ausführen um" msgstr "Ausführen um"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Run history" msgid "Run history"
msgstr "Ausführungsverlauf" msgstr "Ausführungsverlauf"
@ -4756,10 +4832,6 @@ msgstr "Wird ausgeführt…"
msgid "Runs when this bot receives a verified message from this provider." 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." 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/AskCard.tsx
#: src/components/MediaSettings.tsx #: src/components/MediaSettings.tsx
#: src/components/teach/SkillDraftCard.tsx #: src/components/teach/SkillDraftCard.tsx
@ -4909,6 +4981,10 @@ msgstr "Antwort senden"
msgid "Send it" msgid "Send it"
msgstr "Senden" msgstr "Senden"
#: src/pages/shell/transcript.tsx
msgid "Send now"
msgstr "Jetzt gleich senden"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Send reset link" msgid "Send reset link"
msgstr "Link zum Zurücksetzen senden" msgstr "Link zum Zurücksetzen senden"
@ -5021,6 +5097,10 @@ msgstr "Bots anzeigen"
msgid "Show computer" msgid "Show computer"
msgstr "Computer anzeigen" msgstr "Computer anzeigen"
#: src/pages/shell/transcript.tsx
msgid "Show details"
msgstr "Details anzeigen"
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Show less" msgid "Show less"
msgstr "Weniger anzeigen" msgstr "Weniger anzeigen"
@ -5037,6 +5117,7 @@ msgstr "Mehr anzeigen"
msgid "Show password" msgid "Show password"
msgstr "Passwort anzeigen" msgstr "Passwort anzeigen"
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Show settings" msgid "Show settings"
msgstr "Einstellungen anzeigen" msgstr "Einstellungen anzeigen"
@ -5136,7 +5217,7 @@ msgstr "Space-Standard"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
msgid "Spaces" msgid "Spaces"
msgstr "Spaces" msgstr "Projekte"
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Sparkle" msgid "Sparkle"
@ -5172,6 +5253,7 @@ msgstr "Vorlesen an oder aus"
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "Spreche mit Bedacht…" msgstr "Spreche mit Bedacht…"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "Spezialisten-Team" msgstr "Spezialisten-Team"
@ -5286,6 +5368,10 @@ msgstr "Anlernen beenden"
msgid "Stop timer" msgid "Stop timer"
msgstr "Timer stoppen" msgstr "Timer stoppen"
#: src/pages/shell/composer.tsx
msgid "Stopped"
msgstr "Gestoppt"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/lib/browser-notifications.ts #: src/lib/browser-notifications.ts
msgid "Stopped." msgid "Stopped."
@ -5398,6 +5484,7 @@ msgstr "Aufgabe wird angelernt. Beende das Anlernen, bevor du eine neue Nachrich
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
#: src/pages/shell/project-overview.tsx
msgid "Team" msgid "Team"
msgstr "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." 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." 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/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5701,6 +5784,7 @@ msgstr "Bis wann?"
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "Aktuell" msgstr "Aktuell"
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
msgstr "Arbeitskanal offen — leg los." msgstr "Arbeitskanal offen — leg los."
#: src/components/stage/OoStage.tsx #: src/components/stage/OoStage.tsx
#: src/pages/shell/project-overview.tsx
msgid "Working" msgid "Working"
msgstr "Arbeitet" msgstr "Arbeitet"
@ -6037,6 +6122,7 @@ msgstr "Gestern"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
#: src/components/voice/VoiceStage.tsx #: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/transcript.tsx
msgid "You" msgid "You"
msgstr "Du" msgstr "Du"
@ -6077,13 +6163,25 @@ msgid "Your name"
msgstr "Dein Name" msgstr "Dein Name"
#: src/pages/VoiceSettingsOverlay.tsx #: 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 "Wenn das an ist, liest dein Oo Antworten vor." msgstr "Wenn das an ist, spricht dein Oo ganz natürlich mit dir."
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Your Oo will communicate in this language. You can change it later." 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." 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 #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "Deine Spezialisten. Ein ruhiger Ort zum Reden." 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"

View File

@ -161,11 +161,6 @@ msgstr "{0}'s browser"
msgid "{0}'s browser appears here when the computer is running." msgid "{0}'s browser appears here when the computer is running."
msgstr "{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 #: src/pages/HeuteBrief.tsx
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
msgstr "{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…" msgid "{botName} clears the path…"
msgstr "{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 #: src/lib/voice-persona-copy.ts
msgid "{botName} is on it…" msgid "{botName} is on it…"
msgstr "{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/lib/voice-persona-copy.ts
#: src/pages/CallView.tsx #: src/pages/CallView.tsx
msgid "{botName} is working…" msgid "{botName} is working…"
@ -364,6 +387,7 @@ msgstr "{remaining} left · bot is watching, not acting"
msgid "{size} B" msgid "{size} B"
msgstr "{size} B" msgstr "{size} B"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "{stepCount, plural, one {# step} other {# steps}}" msgid "{stepCount, plural, one {# step} other {# steps}}"
msgstr "{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}}" msgid "{toolCount, plural, one {# tool} other {# tools}}"
msgstr "{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/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "/Users/you/projects/acme" msgid "/Users/you/projects/acme"
@ -544,11 +564,16 @@ msgstr "Add another"
msgid "Add as projects" msgid "Add as projects"
msgstr "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 #: src/pages/PluginsOverlay.tsx
msgid "Add Executor" msgid "Add Executor"
msgstr "Add Executor" msgstr "Add Executor"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/project-overview.tsx
msgid "Add goal" msgid "Add goal"
msgstr "Add goal" msgstr "Add goal"
@ -737,6 +762,14 @@ msgstr "Always allowed"
msgid "Analysiert die Aufgabe und führt Tools aus…" msgid "Analysiert die Aufgabe und führt Tools aus…"
msgstr "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 #: src/components/AskCard.tsx
msgid "Answer" msgid "Answer"
msgstr "Answer" msgstr "Answer"
@ -996,7 +1029,7 @@ msgstr "Body"
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Booting live desktop…" msgstr "Booting live desktop…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Booting up {0}’s computer" msgstr "Booting up {0}’s computer"
@ -1019,10 +1052,6 @@ msgstr "Bot"
msgid "Bot screen" msgid "Bot screen"
msgstr "Bot screen" msgstr "Bot screen"
#: src/pages/shell/computer-panel-preview.tsx
msgid "Bot screen preview"
msgstr "Bot screen preview"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Bot to link" msgid "Bot to link"
msgstr "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." 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." 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 #: src/components/OoAtelier.tsx
msgid "Bow" msgid "Bow"
msgstr "Bow" msgstr "Bow"
@ -1064,7 +1089,6 @@ msgstr "Browse"
msgid "Browse MCP servers" msgid "Browse MCP servers"
msgstr "Browse MCP servers" msgstr "Browse MCP servers"
#: src/pages/Shell.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Browser" msgid "Browser"
msgstr "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." 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." 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 #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Configured by deployment" msgstr "Configured by deployment"
@ -1584,6 +1612,10 @@ msgstr "Connected and using {0}."
msgid "Connected app" msgid "Connected app"
msgstr "Connected app" msgstr "Connected app"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr "Connected tools"
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "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/ComputerUpdateProgress.tsx
#: src/components/teach/TeachComputerOverlay.tsx #: src/components/teach/TeachComputerOverlay.tsx
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Dismiss" msgid "Dismiss"
msgstr "Dismiss" msgstr "Dismiss"
@ -2381,7 +2414,9 @@ msgstr "Don't stress — we're here now."
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/project-overview.tsx #: src/pages/shell/project-overview.tsx
#: src/pages/shell/transcript.tsx
msgid "Done" msgid "Done"
msgstr "Done" msgstr "Done"
@ -2394,8 +2429,11 @@ msgstr "Done"
msgid "Dot" msgid "Dot"
msgstr "Dot" msgstr "Dot"
#. placeholder {0}: activeItem.name
#. placeholder {0}: item.name
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {0}" msgid "Download {0}"
msgstr "Download {0}" msgstr "Download {0}"
@ -2403,7 +2441,6 @@ msgstr "Download {0}"
#: src/components/ArtifactPane.tsx #: src/components/ArtifactPane.tsx
#: src/components/media/MediaCard.tsx #: src/components/media/MediaCard.tsx
#: src/components/media/MediaPlayer.tsx #: src/components/media/MediaPlayer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {name}" msgid "Download {name}"
msgstr "Download {name}" msgstr "Download {name}"
@ -2589,6 +2626,8 @@ msgid "Expand"
msgstr "Expand" msgstr "Expand"
#. placeholder {0}: group.title #. placeholder {0}: group.title
#. placeholder {0}: item.name
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/sidebar-group-header.tsx #: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}" msgid "Expand {0}"
msgstr "Expand {0}" msgstr "Expand {0}"
@ -2660,6 +2699,10 @@ msgstr "Failed to load workspace files"
msgid "Failed to open file" msgid "Failed to open file"
msgstr "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 #: src/pages/Shell.tsx
msgid "Failed to send message" msgid "Failed to send message"
msgstr "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…" msgstr "Here's what I see in the logs…"
#: src/pages/VoiceSettingsOverlay.tsx #: src/pages/VoiceSettingsOverlay.tsx
msgid "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 I read replies out loud." msgstr "Hi, this is how I'll sound when we talk."
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Hide bots" msgid "Hide bots"
msgstr "Hide bots" msgstr "Hide bots"
#: src/pages/shell/transcript.tsx
msgid "Hide details"
msgstr "Hide details"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Hide password" msgid "Hide password"
msgstr "Hide password" msgstr "Hide password"
@ -3080,6 +3128,7 @@ msgstr "Import bot"
msgid "Import OpenAPI JSON" msgid "Import OpenAPI JSON"
msgstr "Import OpenAPI JSON" msgstr "Import OpenAPI JSON"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "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." 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." 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 #: src/pages/shell/transcript.tsx
msgid "Jump to latest" msgid "Jump to latest"
msgstr "Jump to latest" msgstr "Jump to latest"
@ -3174,6 +3227,10 @@ msgstr "Jump to latest"
msgid "Jump to replied message" msgid "Jump to replied message"
msgstr "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/ActivityList.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
msgid "just now" msgid "just now"
@ -3774,6 +3831,7 @@ msgid "New space"
msgstr "New space" msgstr "New space"
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/pages/shell/message-cards.tsx
msgid "Next" msgid "Next"
msgstr "Next" msgstr "Next"
@ -3885,6 +3943,10 @@ msgstr "No projects yet."
msgid "No providers found." msgid "No providers found."
msgstr "No providers found." msgstr "No providers found."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr "No recent activity"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "No remote MCP servers found" msgstr "No remote MCP servers found"
@ -3899,6 +3961,7 @@ msgid "No results"
msgstr "No results" msgstr "No results"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "No runs yet" msgid "No runs yet"
msgstr "No runs yet" msgstr "No runs yet"
@ -3957,6 +4020,7 @@ msgid "Notifications"
msgstr "Notifications" msgstr "Notifications"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Now" msgid "Now"
msgstr "Now" msgstr "Now"
@ -4027,7 +4091,6 @@ msgid "Oo name"
msgstr "Oo name" msgstr "Oo name"
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
msgstr "Open" msgstr "Open"
@ -4056,7 +4119,7 @@ msgstr "Open conversation"
msgid "Open in browser" msgid "Open in browser"
msgstr "Open in browser" msgstr "Open in browser"
#: src/pages/shell/computer-panel-preview.tsx #: src/components/browser/OoComputerBrowser.tsx
msgid "Open in full window" msgid "Open in full window"
msgstr "Open in full window" msgstr "Open in full window"
@ -4288,10 +4351,6 @@ msgstr "Private workspace with its own bots and groups."
msgid "Project" msgid "Project"
msgstr "Project" msgstr "Project"
#: src/pages/Shell.tsx
msgid "Project · sessions & folders"
msgstr "Project · sessions & folders"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "Project folder" msgid "Project folder"
@ -4399,6 +4458,10 @@ msgstr "Reading the stack trace…"
msgid "Ready" msgid "Ready"
msgstr "Ready" msgstr "Ready"
#: src/pages/shell/project-overview.tsx
msgid "Ready"
msgstr "Ready"
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Reasoning effort" msgid "Reasoning effort"
msgstr "Reasoning effort" msgstr "Reasoning effort"
@ -4412,6 +4475,7 @@ msgid "Recall only"
msgstr "Recall only" msgstr "Recall only"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Recent" msgid "Recent"
msgstr "Recent" msgstr "Recent"
@ -4528,6 +4592,10 @@ msgstr "Remove {0}"
msgid "Remove folder" msgid "Remove folder"
msgstr "Remove folder" msgstr "Remove folder"
#: src/pages/shell/transcript.tsx
msgid "Remove from queue"
msgstr "Remove from queue"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
msgid "Remove Git event" msgid "Remove Git event"
msgstr "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" msgid "Restoring your workspace"
msgstr "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 #: src/components/FolderPickerDialog.tsx
msgid "Retry" msgid "Retry"
msgstr "Retry" msgstr "Retry"
@ -4700,11 +4773,13 @@ msgstr "Round"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/routine-event-line.tsx #: src/pages/shell/routine-event-line.tsx
msgid "Routine" msgid "Routine"
msgstr "Routine" msgstr "Routine"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Routines" msgid "Routines"
msgstr "Routines" msgstr "Routines"
@ -4718,6 +4793,7 @@ msgid "Run at"
msgstr "Run at" msgstr "Run at"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Run history" msgid "Run history"
msgstr "Run history" msgstr "Run history"
@ -4756,10 +4832,6 @@ msgstr "Running…"
msgid "Runs when this bot receives a verified message from this provider." msgid "Runs when this bot receives a verified message from this provider."
msgstr "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/AskCard.tsx
#: src/components/MediaSettings.tsx #: src/components/MediaSettings.tsx
#: src/components/teach/SkillDraftCard.tsx #: src/components/teach/SkillDraftCard.tsx
@ -4909,6 +4981,10 @@ msgstr "Send answer"
msgid "Send it" msgid "Send it"
msgstr "Send it" msgstr "Send it"
#: src/pages/shell/transcript.tsx
msgid "Send now"
msgstr "Send now"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Send reset link" msgid "Send reset link"
msgstr "Send reset link" msgstr "Send reset link"
@ -5021,6 +5097,10 @@ msgstr "Show bots"
msgid "Show computer" msgid "Show computer"
msgstr "Show computer" msgstr "Show computer"
#: src/pages/shell/transcript.tsx
msgid "Show details"
msgstr "Show details"
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Show less" msgid "Show less"
msgstr "Show less" msgstr "Show less"
@ -5037,6 +5117,7 @@ msgstr "Show more"
msgid "Show password" msgid "Show password"
msgstr "Show password" msgstr "Show password"
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Show settings" msgid "Show settings"
msgstr "Show settings" msgstr "Show settings"
@ -5172,6 +5253,7 @@ msgstr "Speaking on or off"
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "Speaking with intent…" msgstr "Speaking with intent…"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "Specialist crew" msgstr "Specialist crew"
@ -5286,6 +5368,10 @@ msgstr "Stop teaching"
msgid "Stop timer" msgid "Stop timer"
msgstr "Stop timer" msgstr "Stop timer"
#: src/pages/shell/composer.tsx
msgid "Stopped"
msgstr "Stopped"
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/lib/browser-notifications.ts #: src/lib/browser-notifications.ts
msgid "Stopped." msgid "Stopped."
@ -5398,6 +5484,7 @@ msgstr "Teaching in progress. Stop teaching before sending a new message."
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
#: src/pages/shell/project-overview.tsx
msgid "Team" msgid "Team"
msgstr "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." 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." 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/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5701,6 +5784,7 @@ msgstr "Until when?"
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "Up to date" msgstr "Up to date"
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
msgstr "Work channel open — go ahead." msgstr "Work channel open — go ahead."
#: src/components/stage/OoStage.tsx #: src/components/stage/OoStage.tsx
#: src/pages/shell/project-overview.tsx
msgid "Working" msgid "Working"
msgstr "Working" msgstr "Working"
@ -6037,6 +6122,7 @@ msgstr "Yesterday"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
#: src/components/voice/VoiceStage.tsx #: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/transcript.tsx
msgid "You" msgid "You"
msgstr "You" msgstr "You"
@ -6077,13 +6163,25 @@ msgid "Your name"
msgstr "Your name" msgstr "Your name"
#: src/pages/VoiceSettingsOverlay.tsx #: 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 "Your Oo reads replies aloud when this is on." msgstr "Your Oo talks with you naturally when this is on."
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Your Oo will communicate in this language. You can change it later." 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." 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 #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "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"

View File

@ -161,11 +161,6 @@ msgstr ""
msgid "{0}'s browser appears here when the computer is running." msgid "{0}'s browser appears here when the computer is running."
msgstr "" msgstr ""
#. placeholder {0}: active.name
#: src/pages/shell/computer-panel-preview.tsx
msgid "{0}'s screen"
msgstr "Pantalla de {0}"
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
msgstr "" msgstr ""
@ -182,10 +177,38 @@ msgstr ""
msgid "{botName} clears the path…" msgid "{botName} clears the path…"
msgstr "" 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 #: src/lib/voice-persona-copy.ts
msgid "{botName} is on it…" msgid "{botName} is on it…"
msgstr "" 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/lib/voice-persona-copy.ts
#: src/pages/CallView.tsx #: src/pages/CallView.tsx
msgid "{botName} is working…" msgid "{botName} is working…"
@ -364,6 +387,7 @@ msgstr "Quedan {remaining} · el bot observa, no actúa"
msgid "{size} B" msgid "{size} B"
msgstr "{size} B" msgstr "{size} B"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "{stepCount, plural, one {# step} other {# steps}}" msgid "{stepCount, plural, one {# step} other {# steps}}"
msgstr "" msgstr ""
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
msgid "{toolCount, plural, one {# tool} other {# tools}}" msgid "{toolCount, plural, one {# tool} other {# tools}}"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "{workingBotName} is working"
msgstr "{workingBotName} está trabajando"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "/Users/you/projects/acme" msgid "/Users/you/projects/acme"
@ -544,11 +564,16 @@ msgstr ""
msgid "Add as projects" msgid "Add as projects"
msgstr "" msgstr ""
#: src/components/NotificationSettingsPanel.tsx
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
msgstr ""
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
msgid "Add Executor" msgid "Add Executor"
msgstr "Agregar Executor" msgstr "Agregar Executor"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/project-overview.tsx
msgid "Add goal" msgid "Add goal"
msgstr "" msgstr ""
@ -737,6 +762,14 @@ msgstr "Siempre permitido"
msgid "Analysiert die Aufgabe und führt Tools aus…" msgid "Analysiert die Aufgabe und führt Tools aus…"
msgstr "" 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 #: src/components/AskCard.tsx
msgid "Answer" msgid "Answer"
msgstr "Responder" msgstr "Responder"
@ -996,7 +1029,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Iniciando escritorio en vivo…" msgstr "Iniciando escritorio en vivo…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Iniciando la computadora de {0}" msgstr "Iniciando la computadora de {0}"
@ -1019,10 +1052,6 @@ msgstr "Bot"
msgid "Bot screen" msgid "Bot screen"
msgstr "Pantalla del bot" 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 #: src/pages/MessagingSettingsOverlay.tsx
msgid "Bot to link" msgid "Bot to link"
msgstr "Bot a vincular" 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." 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." 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 #: src/components/OoAtelier.tsx
msgid "Bow" msgid "Bow"
msgstr "" msgstr ""
@ -1064,7 +1089,6 @@ msgstr ""
msgid "Browse MCP servers" msgid "Browse MCP servers"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Browser" msgid "Browser"
msgstr "" 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." 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." 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 #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Configurado por el despliegue" msgstr "Configurado por el despliegue"
@ -1584,6 +1612,10 @@ msgstr "Conectado y usando {0}."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Conectado: sus herramientas estarán disponibles desde tu próximo mensaje." msgstr "Conectado: sus herramientas estarán disponibles desde tu próximo mensaje."
@ -2322,6 +2354,7 @@ msgstr "Desconectando…"
#: src/components/ComputerUpdateProgress.tsx #: src/components/ComputerUpdateProgress.tsx
#: src/components/teach/TeachComputerOverlay.tsx #: src/components/teach/TeachComputerOverlay.tsx
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Dismiss" msgid "Dismiss"
msgstr "Descartar" msgstr "Descartar"
@ -2381,7 +2414,9 @@ msgstr ""
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/project-overview.tsx #: src/pages/shell/project-overview.tsx
#: src/pages/shell/transcript.tsx
msgid "Done" msgid "Done"
msgstr "Listo" msgstr "Listo"
@ -2394,8 +2429,11 @@ msgstr ""
msgid "Dot" msgid "Dot"
msgstr "" msgstr ""
#. placeholder {0}: activeItem.name
#. placeholder {0}: item.name
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {0}" msgid "Download {0}"
msgstr "Descargar {0}" msgstr "Descargar {0}"
@ -2403,7 +2441,6 @@ msgstr "Descargar {0}"
#: src/components/ArtifactPane.tsx #: src/components/ArtifactPane.tsx
#: src/components/media/MediaCard.tsx #: src/components/media/MediaCard.tsx
#: src/components/media/MediaPlayer.tsx #: src/components/media/MediaPlayer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {name}" msgid "Download {name}"
msgstr "Descargar {name}" msgstr "Descargar {name}"
@ -2589,6 +2626,8 @@ msgid "Expand"
msgstr "Expandir" msgstr "Expandir"
#. placeholder {0}: group.title #. placeholder {0}: group.title
#. placeholder {0}: item.name
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/sidebar-group-header.tsx #: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}" msgid "Expand {0}"
msgstr "Expandir {0}" msgstr "Expandir {0}"
@ -2660,6 +2699,10 @@ msgstr ""
msgid "Failed to open file" msgid "Failed to open file"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Failed to remove message"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Failed to send message" msgid "Failed to send message"
msgstr "No se pudo enviar el mensaje" msgstr "No se pudo enviar el mensaje"
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
msgstr "" msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx #: src/pages/VoiceSettingsOverlay.tsx
msgid "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 "Hola, así sonaré cuando lea las respuestas en voz alta." msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Hide bots" msgid "Hide bots"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Hide details"
msgstr ""
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Hide password" msgid "Hide password"
msgstr "Ocultar contraseña" msgstr "Ocultar contraseña"
@ -3080,6 +3128,7 @@ msgstr ""
msgid "Import OpenAPI JSON" msgid "Import OpenAPI JSON"
msgstr "Importar JSON de OpenAPI" msgstr "Importar JSON de OpenAPI"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "" msgstr ""
@ -3166,6 +3215,10 @@ msgstr "Aislado"
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." 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." 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 #: src/pages/shell/transcript.tsx
msgid "Jump to latest" msgid "Jump to latest"
msgstr "Ir al más reciente" msgstr "Ir al más reciente"
@ -3174,6 +3227,10 @@ msgstr "Ir al más reciente"
msgid "Jump to replied message" msgid "Jump to replied message"
msgstr "Ir al mensaje respondido" 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/ActivityList.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
msgid "just now" msgid "just now"
@ -3774,6 +3831,7 @@ msgid "New space"
msgstr "Nuevo space" msgstr "Nuevo space"
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/pages/shell/message-cards.tsx
msgid "Next" msgid "Next"
msgstr "" msgstr ""
@ -3885,6 +3943,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "No se encontraron proveedores." msgstr "No se encontraron proveedores."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -3899,6 +3961,7 @@ msgid "No results"
msgstr "Sin resultados" msgstr "Sin resultados"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "No runs yet" msgid "No runs yet"
msgstr "Aún no hay ejecuciones" msgstr "Aún no hay ejecuciones"
@ -3957,6 +4020,7 @@ msgid "Notifications"
msgstr "Notificaciones" msgstr "Notificaciones"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Now" msgid "Now"
msgstr "Ahora" msgstr "Ahora"
@ -4027,7 +4091,6 @@ msgid "Oo name"
msgstr "" msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
msgstr "Abrir" msgstr "Abrir"
@ -4056,7 +4119,7 @@ msgstr ""
msgid "Open in browser" msgid "Open in browser"
msgstr "" msgstr ""
#: src/pages/shell/computer-panel-preview.tsx #: src/components/browser/OoComputerBrowser.tsx
msgid "Open in full window" msgid "Open in full window"
msgstr "Abrir en ventana completa" msgstr "Abrir en ventana completa"
@ -4288,10 +4351,6 @@ msgstr ""
msgid "Project" msgid "Project"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Project · sessions & folders"
msgstr ""
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "Project folder" msgid "Project folder"
@ -4399,6 +4458,10 @@ msgstr ""
msgid "Ready" msgid "Ready"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
msgid "Ready"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Reasoning effort" msgid "Reasoning effort"
msgstr "Esfuerzo de razonamiento" msgstr "Esfuerzo de razonamiento"
@ -4412,6 +4475,7 @@ msgid "Recall only"
msgstr "" msgstr ""
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Recent" msgid "Recent"
msgstr "Recientes" msgstr "Recientes"
@ -4528,6 +4592,10 @@ msgstr "Quitar {0}"
msgid "Remove folder" msgid "Remove folder"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Remove from queue"
msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
msgid "Remove Git event" msgid "Remove Git event"
msgstr "" msgstr ""
@ -4653,6 +4721,11 @@ msgstr "Restaura el último espacio de trabajo guardado. Se pierde el trabajo si
msgid "Restoring your workspace" msgid "Restoring your workspace"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Resume"
msgstr ""
#: src/components/FolderPickerDialog.tsx #: src/components/FolderPickerDialog.tsx
msgid "Retry" msgid "Retry"
msgstr "" msgstr ""
@ -4700,11 +4773,13 @@ msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/routine-event-line.tsx #: src/pages/shell/routine-event-line.tsx
msgid "Routine" msgid "Routine"
msgstr "Rutina" msgstr "Rutina"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Routines" msgid "Routines"
msgstr "Rutinas" msgstr "Rutinas"
@ -4718,6 +4793,7 @@ msgid "Run at"
msgstr "Ejecutar a las" msgstr "Ejecutar a las"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Run history" msgid "Run history"
msgstr "Historial de ejecuciones" msgstr "Historial de ejecuciones"
@ -4756,10 +4832,6 @@ msgstr "Ejecutando…"
msgid "Runs when this bot receives a verified message from this provider." msgid "Runs when this bot receives a verified message from this provider."
msgstr "" msgstr ""
#: src/pages/shell/side-panel-header.tsx
msgid "Sandbox"
msgstr ""
#: src/components/AskCard.tsx #: src/components/AskCard.tsx
#: src/components/MediaSettings.tsx #: src/components/MediaSettings.tsx
#: src/components/teach/SkillDraftCard.tsx #: src/components/teach/SkillDraftCard.tsx
@ -4909,6 +4981,10 @@ msgstr "Enviar respuesta"
msgid "Send it" msgid "Send it"
msgstr "Enviarlo" msgstr "Enviarlo"
#: src/pages/shell/transcript.tsx
msgid "Send now"
msgstr "Enviar ahora"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Send reset link" msgid "Send reset link"
msgstr "Enviar enlace de restablecimiento" msgstr "Enviar enlace de restablecimiento"
@ -5021,6 +5097,10 @@ msgstr ""
msgid "Show computer" msgid "Show computer"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Show details"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Show less" msgid "Show less"
msgstr "" msgstr ""
@ -5037,6 +5117,7 @@ msgstr "Mostrar más"
msgid "Show password" msgid "Show password"
msgstr "Mostrar contraseña" msgstr "Mostrar contraseña"
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Show settings" msgid "Show settings"
msgstr "Mostrar configuración" msgstr "Mostrar configuración"
@ -5172,6 +5253,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5286,6 +5368,10 @@ msgstr "Detener enseñanza"
msgid "Stop timer" msgid "Stop timer"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
msgid "Stopped"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/lib/browser-notifications.ts #: src/lib/browser-notifications.ts
msgid "Stopped." 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/ProjectSessionTimeline.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
#: src/pages/shell/project-overview.tsx
msgid "Team" msgid "Team"
msgstr "Equipo" 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." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5701,6 +5784,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "Actualizado" msgstr "Actualizado"
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
msgstr "" msgstr ""
#: src/components/stage/OoStage.tsx #: src/components/stage/OoStage.tsx
#: src/pages/shell/project-overview.tsx
msgid "Working" msgid "Working"
msgstr "" msgstr ""
@ -6037,6 +6122,7 @@ msgstr "Ayer"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
#: src/components/voice/VoiceStage.tsx #: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/transcript.tsx
msgid "You" msgid "You"
msgstr "Tú" msgstr "Tú"
@ -6077,13 +6163,25 @@ msgid "Your name"
msgstr "Tu nombre" msgstr "Tu nombre"
#: src/pages/VoiceSettingsOverlay.tsx #: 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 "" msgstr ""
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom in"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom out"
msgstr ""

View File

@ -161,11 +161,6 @@ msgstr ""
msgid "{0}'s browser appears here when the computer is running." msgid "{0}'s browser appears here when the computer is running."
msgstr "" msgstr ""
#. placeholder {0}: active.name
#: src/pages/shell/computer-panel-preview.tsx
msgid "{0}'s screen"
msgstr ""
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
msgstr "" msgstr ""
@ -182,10 +177,38 @@ msgstr ""
msgid "{botName} clears the path…" msgid "{botName} clears the path…"
msgstr "" 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 #: src/lib/voice-persona-copy.ts
msgid "{botName} is on it…" msgid "{botName} is on it…"
msgstr "" 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/lib/voice-persona-copy.ts
#: src/pages/CallView.tsx #: src/pages/CallView.tsx
msgid "{botName} is working…" msgid "{botName} is working…"
@ -364,6 +387,7 @@ msgstr "{remaining} शेष · बॉट देख रहा है, कार
msgid "{size} B" msgid "{size} B"
msgstr "{size} B" msgstr "{size} B"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "{stepCount, plural, one {# step} other {# steps}}" msgid "{stepCount, plural, one {# step} other {# steps}}"
msgstr "" msgstr ""
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
msgid "{toolCount, plural, one {# tool} other {# tools}}" msgid "{toolCount, plural, one {# tool} other {# tools}}"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "{workingBotName} is working"
msgstr "{workingBotName} काम कर रहा है"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "/Users/you/projects/acme" msgid "/Users/you/projects/acme"
@ -544,11 +564,16 @@ msgstr ""
msgid "Add as projects" msgid "Add as projects"
msgstr "" msgstr ""
#: src/components/NotificationSettingsPanel.tsx
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
msgstr ""
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
msgid "Add Executor" msgid "Add Executor"
msgstr "Executor जोड़ें" msgstr "Executor जोड़ें"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/project-overview.tsx
msgid "Add goal" msgid "Add goal"
msgstr "" msgstr ""
@ -737,6 +762,14 @@ msgstr "हमेशा अनुमत"
msgid "Analysiert die Aufgabe und führt Tools aus…" msgid "Analysiert die Aufgabe und führt Tools aus…"
msgstr "" 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 #: src/components/AskCard.tsx
msgid "Answer" msgid "Answer"
msgstr "उत्तर" msgstr "उत्तर"
@ -996,7 +1029,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "लाइव डेस्कटॉप शुरू हो रहा है…" msgstr "लाइव डेस्कटॉप शुरू हो रहा है…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "{0} का कंप्यूटर शुरू हो रहा है" msgstr "{0} का कंप्यूटर शुरू हो रहा है"
@ -1019,10 +1052,6 @@ msgstr "बॉट"
msgid "Bot screen" msgid "Bot screen"
msgstr "बॉट स्क्रीन" msgstr "बॉट स्क्रीन"
#: src/pages/shell/computer-panel-preview.tsx
msgid "Bot screen preview"
msgstr "बॉट स्क्रीन प्रीव्यू"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Bot to link" msgid "Bot to link"
msgstr "" 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." msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
msgstr "बॉट डिफ़ॉल्ट रूप से बिना पूछे कार्रवाई करते हैं। अपवाद तभी जोड़ें जब आप किसी प्रकार की कार्रवाई की पहले समीक्षा करना चाहते हों।" msgstr "बॉट डिफ़ॉल्ट रूप से बिना पूछे कार्रवाई करते हैं। अपवाद तभी जोड़ें जब आप किसी प्रकार की कार्रवाई की पहले समीक्षा करना चाहते हों।"
#: src/pages/shell/transcript.tsx
msgid "Bots are working"
msgstr "बॉट काम कर रहे हैं"
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Bow" msgid "Bow"
msgstr "" msgstr ""
@ -1064,7 +1089,6 @@ msgstr ""
msgid "Browse MCP servers" msgid "Browse MCP servers"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Browser" msgid "Browser"
msgstr "" 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." 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 बदलने के बाद स्टैक फिर बनाएँ।" 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 #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "डिप्लॉयमेंट द्वारा कॉन्फ़िगर" msgstr "डिप्लॉयमेंट द्वारा कॉन्फ़िगर"
@ -1584,6 +1612,10 @@ msgstr "{0} कनेक्ट किया गया और उपयोग म
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "कनेक्टेड। इसके टूल आपके अगले संदेश से उपलब्ध हैं।" msgstr "कनेक्टेड। इसके टूल आपके अगले संदेश से उपलब्ध हैं।"
@ -2322,6 +2354,7 @@ msgstr "डिस्कनेक्ट हो रहा है…"
#: src/components/ComputerUpdateProgress.tsx #: src/components/ComputerUpdateProgress.tsx
#: src/components/teach/TeachComputerOverlay.tsx #: src/components/teach/TeachComputerOverlay.tsx
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Dismiss" msgid "Dismiss"
msgstr "" msgstr ""
@ -2381,7 +2414,9 @@ msgstr ""
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/project-overview.tsx #: src/pages/shell/project-overview.tsx
#: src/pages/shell/transcript.tsx
msgid "Done" msgid "Done"
msgstr "पूर्ण" msgstr "पूर्ण"
@ -2394,8 +2429,11 @@ msgstr ""
msgid "Dot" msgid "Dot"
msgstr "" msgstr ""
#. placeholder {0}: activeItem.name
#. placeholder {0}: item.name
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {0}" msgid "Download {0}"
msgstr "{0} डाउनलोड करें" msgstr "{0} डाउनलोड करें"
@ -2403,7 +2441,6 @@ msgstr "{0} डाउनलोड करें"
#: src/components/ArtifactPane.tsx #: src/components/ArtifactPane.tsx
#: src/components/media/MediaCard.tsx #: src/components/media/MediaCard.tsx
#: src/components/media/MediaPlayer.tsx #: src/components/media/MediaPlayer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {name}" msgid "Download {name}"
msgstr "{name} डाउनलोड करें" msgstr "{name} डाउनलोड करें"
@ -2589,6 +2626,8 @@ msgid "Expand"
msgstr "विस्तृत करें" msgstr "विस्तृत करें"
#. placeholder {0}: group.title #. placeholder {0}: group.title
#. placeholder {0}: item.name
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/sidebar-group-header.tsx #: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}" msgid "Expand {0}"
msgstr "" msgstr ""
@ -2660,6 +2699,10 @@ msgstr ""
msgid "Failed to open file" msgid "Failed to open file"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Failed to remove message"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Failed to send message" msgid "Failed to send message"
msgstr "संदेश भेजने में विफल" msgstr "संदेश भेजने में विफल"
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
msgstr "" msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx #: src/pages/VoiceSettingsOverlay.tsx
msgid "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 "नमस्ते, उत्तर पढ़कर सुनाते समय मेरी आवाज़ ऐसी होगी।" msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Hide bots" msgid "Hide bots"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Hide details"
msgstr ""
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Hide password" msgid "Hide password"
msgstr "" msgstr ""
@ -3080,6 +3128,7 @@ msgstr ""
msgid "Import OpenAPI JSON" msgid "Import OpenAPI JSON"
msgstr "OpenAPI JSON इम्पोर्ट करें" msgstr "OpenAPI JSON इम्पोर्ट करें"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "" msgstr ""
@ -3166,6 +3215,10 @@ msgstr "पृथक"
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "इसकी बातचीत, फ़ाइलें और रूटीन स्थायी रूप से डिलीट कर दिए जाएंगे। इसके बनाए बॉट आपकी सूची में बने रहेंगे।" msgstr "इसकी बातचीत, फ़ाइलें और रूटीन स्थायी रूप से डिलीट कर दिए जाएंगे। इसके बनाए बॉट आपकी सूची में बने रहेंगे।"
#: src/pages/shell/transcript.tsx
msgid "joined"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Jump to latest" msgid "Jump to latest"
msgstr "नवीनतम संदेश पर जाएँ" msgstr "नवीनतम संदेश पर जाएँ"
@ -3174,6 +3227,10 @@ msgstr "नवीनतम संदेश पर जाएँ"
msgid "Jump to replied message" msgid "Jump to replied message"
msgstr "जिस संदेश का उत्तर दिया गया उस पर जाएं" 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/ActivityList.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
msgid "just now" msgid "just now"
@ -3774,6 +3831,7 @@ msgid "New space"
msgstr "" msgstr ""
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/pages/shell/message-cards.tsx
msgid "Next" msgid "Next"
msgstr "" msgstr ""
@ -3885,6 +3943,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "कोई प्रोवाइडर नहीं मिला।" msgstr "कोई प्रोवाइडर नहीं मिला।"
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -3899,6 +3961,7 @@ msgid "No results"
msgstr "कोई परिणाम नहीं" msgstr "कोई परिणाम नहीं"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "No runs yet" msgid "No runs yet"
msgstr "" msgstr ""
@ -3957,6 +4020,7 @@ msgid "Notifications"
msgstr "सूचनाएं" msgstr "सूचनाएं"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Now" msgid "Now"
msgstr "अभी" msgstr "अभी"
@ -4027,7 +4091,6 @@ msgid "Oo name"
msgstr "" msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
msgstr "खोलें" msgstr "खोलें"
@ -4056,7 +4119,7 @@ msgstr ""
msgid "Open in browser" msgid "Open in browser"
msgstr "" msgstr ""
#: src/pages/shell/computer-panel-preview.tsx #: src/components/browser/OoComputerBrowser.tsx
msgid "Open in full window" msgid "Open in full window"
msgstr "पूरी विंडो में खोलें" msgstr "पूरी विंडो में खोलें"
@ -4288,10 +4351,6 @@ msgstr ""
msgid "Project" msgid "Project"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Project · sessions & folders"
msgstr ""
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "Project folder" msgid "Project folder"
@ -4399,6 +4458,10 @@ msgstr ""
msgid "Ready" msgid "Ready"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
msgid "Ready"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Reasoning effort" msgid "Reasoning effort"
msgstr "रीज़निंग प्रयास" msgstr "रीज़निंग प्रयास"
@ -4412,6 +4475,7 @@ msgid "Recall only"
msgstr "" msgstr ""
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Recent" msgid "Recent"
msgstr "हाल के" msgstr "हाल के"
@ -4528,6 +4592,10 @@ msgstr "{0} हटाएं"
msgid "Remove folder" msgid "Remove folder"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Remove from queue"
msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
msgid "Remove Git event" msgid "Remove Git event"
msgstr "" msgstr ""
@ -4653,6 +4721,11 @@ msgstr "अंतिम सहेजे गए वर्कस्पेस क
msgid "Restoring your workspace" msgid "Restoring your workspace"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Resume"
msgstr ""
#: src/components/FolderPickerDialog.tsx #: src/components/FolderPickerDialog.tsx
msgid "Retry" msgid "Retry"
msgstr "" msgstr ""
@ -4700,11 +4773,13 @@ msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/routine-event-line.tsx #: src/pages/shell/routine-event-line.tsx
msgid "Routine" msgid "Routine"
msgstr "रूटीन" msgstr "रूटीन"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Routines" msgid "Routines"
msgstr "रूटीन्स" msgstr "रूटीन्स"
@ -4718,6 +4793,7 @@ msgid "Run at"
msgstr "" msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Run history" msgid "Run history"
msgstr "" msgstr ""
@ -4756,10 +4832,6 @@ msgstr "चल रहा है…"
msgid "Runs when this bot receives a verified message from this provider." msgid "Runs when this bot receives a verified message from this provider."
msgstr "" msgstr ""
#: src/pages/shell/side-panel-header.tsx
msgid "Sandbox"
msgstr ""
#: src/components/AskCard.tsx #: src/components/AskCard.tsx
#: src/components/MediaSettings.tsx #: src/components/MediaSettings.tsx
#: src/components/teach/SkillDraftCard.tsx #: src/components/teach/SkillDraftCard.tsx
@ -4909,6 +4981,10 @@ msgstr "उत्तर भेजें"
msgid "Send it" msgid "Send it"
msgstr "इसे भेजें" msgstr "इसे भेजें"
#: src/pages/shell/transcript.tsx
msgid "Send now"
msgstr "अभी भेजें"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Send reset link" msgid "Send reset link"
msgstr "रीसेट लिंक भेजें" msgstr "रीसेट लिंक भेजें"
@ -5021,6 +5097,10 @@ msgstr ""
msgid "Show computer" msgid "Show computer"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Show details"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Show less" msgid "Show less"
msgstr "" msgstr ""
@ -5037,6 +5117,7 @@ msgstr "और दिखाएँ"
msgid "Show password" msgid "Show password"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Show settings" msgid "Show settings"
msgstr "सेटिंग्स दिखाएं" msgstr "सेटिंग्स दिखाएं"
@ -5172,6 +5253,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5286,6 +5368,10 @@ msgstr "सिखाना रोकें"
msgid "Stop timer" msgid "Stop timer"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
msgid "Stopped"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/lib/browser-notifications.ts #: src/lib/browser-notifications.ts
msgid "Stopped." msgid "Stopped."
@ -5398,6 +5484,7 @@ msgstr "सिखाना जारी है। नया संदेश भ
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
#: src/pages/shell/project-overview.tsx
msgid "Team" msgid "Team"
msgstr "टीम" msgstr "टीम"
@ -5512,10 +5599,6 @@ msgstr "यह सब्सक्रिप्शन साइन-इन अभ
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5701,6 +5784,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "" msgstr ""
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
msgstr "" msgstr ""
#: src/components/stage/OoStage.tsx #: src/components/stage/OoStage.tsx
#: src/pages/shell/project-overview.tsx
msgid "Working" msgid "Working"
msgstr "" msgstr ""
@ -6037,6 +6122,7 @@ msgstr "कल"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
#: src/components/voice/VoiceStage.tsx #: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/transcript.tsx
msgid "You" msgid "You"
msgstr "आप" msgstr "आप"
@ -6077,13 +6163,25 @@ msgid "Your name"
msgstr "आपका नाम" msgstr "आपका नाम"
#: src/pages/VoiceSettingsOverlay.tsx #: 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 "" msgstr ""
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom in"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom out"
msgstr ""

View File

@ -161,11 +161,6 @@ msgstr ""
msgid "{0}'s browser appears here when the computer is running." msgid "{0}'s browser appears here when the computer is running."
msgstr "" msgstr ""
#. placeholder {0}: active.name
#: src/pages/shell/computer-panel-preview.tsx
msgid "{0}'s screen"
msgstr "{0}의 화면"
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
msgstr "" msgstr ""
@ -182,10 +177,38 @@ msgstr ""
msgid "{botName} clears the path…" msgid "{botName} clears the path…"
msgstr "" 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 #: src/lib/voice-persona-copy.ts
msgid "{botName} is on it…" msgid "{botName} is on it…"
msgstr "" 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/lib/voice-persona-copy.ts
#: src/pages/CallView.tsx #: src/pages/CallView.tsx
msgid "{botName} is working…" msgid "{botName} is working…"
@ -364,6 +387,7 @@ msgstr "남은 시간 {remaining} · Bot은 화면만 보고 직접 조작하지
msgid "{size} B" msgid "{size} B"
msgstr "{size} B" msgstr "{size} B"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "{stepCount, plural, one {# step} other {# steps}}" msgid "{stepCount, plural, one {# step} other {# steps}}"
msgstr "" msgstr ""
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
msgid "{toolCount, plural, one {# tool} other {# tools}}" msgid "{toolCount, plural, one {# tool} other {# tools}}"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "{workingBotName} is working"
msgstr "{workingBotName} 작업 중"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "/Users/you/projects/acme" msgid "/Users/you/projects/acme"
@ -544,11 +564,16 @@ msgstr ""
msgid "Add as projects" msgid "Add as projects"
msgstr "" msgstr ""
#: src/components/NotificationSettingsPanel.tsx
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
msgstr ""
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
msgid "Add Executor" msgid "Add Executor"
msgstr "Executor 추가" msgstr "Executor 추가"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/project-overview.tsx
msgid "Add goal" msgid "Add goal"
msgstr "" msgstr ""
@ -737,6 +762,14 @@ msgstr "항상 허용됨"
msgid "Analysiert die Aufgabe und führt Tools aus…" msgid "Analysiert die Aufgabe und führt Tools aus…"
msgstr "" 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 #: src/components/AskCard.tsx
msgid "Answer" msgid "Answer"
msgstr "답변" msgstr "답변"
@ -996,7 +1029,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "라이브 데스크톱 시작 중…" msgstr "라이브 데스크톱 시작 중…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "{0}의 컴퓨터를 켜는 중" msgstr "{0}의 컴퓨터를 켜는 중"
@ -1019,10 +1052,6 @@ msgstr "봇"
msgid "Bot screen" msgid "Bot screen"
msgstr "Bot 화면" msgstr "Bot 화면"
#: src/pages/shell/computer-panel-preview.tsx
msgid "Bot screen preview"
msgstr "Bot 화면 미리보기"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Bot to link" msgid "Bot to link"
msgstr "연결할 Bot" 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." msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
msgstr "Bot은 기본적으로 묻지 않고 작업합니다. 먼저 확인하고 싶은 작업만 예외로 추가하세요." msgstr "Bot은 기본적으로 묻지 않고 작업합니다. 먼저 확인하고 싶은 작업만 예외로 추가하세요."
#: src/pages/shell/transcript.tsx
msgid "Bots are working"
msgstr "봇 작업 중"
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Bow" msgid "Bow"
msgstr "" msgstr ""
@ -1064,7 +1089,6 @@ msgstr ""
msgid "Browse MCP servers" msgid "Browse MCP servers"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Browser" msgid "Browser"
msgstr "" 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." 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 변경 후 스택을 다시 만드세요." 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 #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "서버에서 설정됨" msgstr "서버에서 설정됨"
@ -1584,6 +1612,10 @@ msgstr "{0}에 연결하고 기본 모델로 설정했습니다."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "연결되었습니다. 다음 메시지부터 이 도구를 사용할 수 있습니다." msgstr "연결되었습니다. 다음 메시지부터 이 도구를 사용할 수 있습니다."
@ -2322,6 +2354,7 @@ msgstr "연결 해제 중…"
#: src/components/ComputerUpdateProgress.tsx #: src/components/ComputerUpdateProgress.tsx
#: src/components/teach/TeachComputerOverlay.tsx #: src/components/teach/TeachComputerOverlay.tsx
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Dismiss" msgid "Dismiss"
msgstr "닫기" msgstr "닫기"
@ -2381,7 +2414,9 @@ msgstr ""
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/project-overview.tsx #: src/pages/shell/project-overview.tsx
#: src/pages/shell/transcript.tsx
msgid "Done" msgid "Done"
msgstr "완료" msgstr "완료"
@ -2394,8 +2429,11 @@ msgstr ""
msgid "Dot" msgid "Dot"
msgstr "" msgstr ""
#. placeholder {0}: activeItem.name
#. placeholder {0}: item.name
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {0}" msgid "Download {0}"
msgstr "{0} 다운로드" msgstr "{0} 다운로드"
@ -2403,7 +2441,6 @@ msgstr "{0} 다운로드"
#: src/components/ArtifactPane.tsx #: src/components/ArtifactPane.tsx
#: src/components/media/MediaCard.tsx #: src/components/media/MediaCard.tsx
#: src/components/media/MediaPlayer.tsx #: src/components/media/MediaPlayer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {name}" msgid "Download {name}"
msgstr "{name} 다운로드" msgstr "{name} 다운로드"
@ -2589,6 +2626,8 @@ msgid "Expand"
msgstr "펼치기" msgstr "펼치기"
#. placeholder {0}: group.title #. placeholder {0}: group.title
#. placeholder {0}: item.name
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/sidebar-group-header.tsx #: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}" msgid "Expand {0}"
msgstr "{0} 펼치기" msgstr "{0} 펼치기"
@ -2660,6 +2699,10 @@ msgstr ""
msgid "Failed to open file" msgid "Failed to open file"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Failed to remove message"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Failed to send message" msgid "Failed to send message"
msgstr "메시지를 보내지 못했습니다." msgstr "메시지를 보내지 못했습니다."
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
msgstr "" msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx #: src/pages/VoiceSettingsOverlay.tsx
msgid "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 "안녕하세요. 답변을 소리 내어 읽을 때 이런 목소리로 들립니다." msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Hide bots" msgid "Hide bots"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Hide details"
msgstr ""
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Hide password" msgid "Hide password"
msgstr "비밀번호 숨기기" msgstr "비밀번호 숨기기"
@ -3080,6 +3128,7 @@ msgstr ""
msgid "Import OpenAPI JSON" msgid "Import OpenAPI JSON"
msgstr "OpenAPI JSON 가져오기" msgstr "OpenAPI JSON 가져오기"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "" msgstr ""
@ -3166,6 +3215,10 @@ msgstr "이 Bot만 사용"
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "대화, 파일, 자동 실행이 영구 삭제됩니다. 이 Bot이 만든 다른 Bot은 목록에 남습니다." msgstr "대화, 파일, 자동 실행이 영구 삭제됩니다. 이 Bot이 만든 다른 Bot은 목록에 남습니다."
#: src/pages/shell/transcript.tsx
msgid "joined"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Jump to latest" msgid "Jump to latest"
msgstr "최신 메시지로 이동" msgstr "최신 메시지로 이동"
@ -3174,6 +3227,10 @@ msgstr "최신 메시지로 이동"
msgid "Jump to replied message" msgid "Jump to replied message"
msgstr "답장한 메시지로 이동" 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/ActivityList.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
msgid "just now" msgid "just now"
@ -3774,6 +3831,7 @@ msgid "New space"
msgstr "새 스페이스" msgstr "새 스페이스"
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/pages/shell/message-cards.tsx
msgid "Next" msgid "Next"
msgstr "" msgstr ""
@ -3885,6 +3943,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "검색된 AI 서비스가 없습니다." msgstr "검색된 AI 서비스가 없습니다."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -3899,6 +3961,7 @@ msgid "No results"
msgstr "검색 결과가 없습니다" msgstr "검색 결과가 없습니다"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "No runs yet" msgid "No runs yet"
msgstr "아직 실행 기록이 없습니다" msgstr "아직 실행 기록이 없습니다"
@ -3957,6 +4020,7 @@ msgid "Notifications"
msgstr "알림" msgstr "알림"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Now" msgid "Now"
msgstr "지금" msgstr "지금"
@ -4027,7 +4091,6 @@ msgid "Oo name"
msgstr "" msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
msgstr "열기" msgstr "열기"
@ -4056,7 +4119,7 @@ msgstr ""
msgid "Open in browser" msgid "Open in browser"
msgstr "" msgstr ""
#: src/pages/shell/computer-panel-preview.tsx #: src/components/browser/OoComputerBrowser.tsx
msgid "Open in full window" msgid "Open in full window"
msgstr "전체 창으로 열기" msgstr "전체 창으로 열기"
@ -4288,10 +4351,6 @@ msgstr ""
msgid "Project" msgid "Project"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Project · sessions & folders"
msgstr ""
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "Project folder" msgid "Project folder"
@ -4399,6 +4458,10 @@ msgstr ""
msgid "Ready" msgid "Ready"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
msgid "Ready"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Reasoning effort" msgid "Reasoning effort"
msgstr "추론 강도" msgstr "추론 강도"
@ -4412,6 +4475,7 @@ msgid "Recall only"
msgstr "" msgstr ""
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Recent" msgid "Recent"
msgstr "최근" msgstr "최근"
@ -4528,6 +4592,10 @@ msgstr "{0} 삭제"
msgid "Remove folder" msgid "Remove folder"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Remove from queue"
msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
msgid "Remove Git event" msgid "Remove Git event"
msgstr "" msgstr ""
@ -4653,6 +4721,11 @@ msgstr "마지막으로 저장된 작업 공간을 복원합니다. 컴퓨터에
msgid "Restoring your workspace" msgid "Restoring your workspace"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Resume"
msgstr ""
#: src/components/FolderPickerDialog.tsx #: src/components/FolderPickerDialog.tsx
msgid "Retry" msgid "Retry"
msgstr "" msgstr ""
@ -4700,11 +4773,13 @@ msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/routine-event-line.tsx #: src/pages/shell/routine-event-line.tsx
msgid "Routine" msgid "Routine"
msgstr "자동 실행" msgstr "자동 실행"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Routines" msgid "Routines"
msgstr "자동 실행" msgstr "자동 실행"
@ -4718,6 +4793,7 @@ msgid "Run at"
msgstr "실행 시간" msgstr "실행 시간"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Run history" msgid "Run history"
msgstr "실행 기록" msgstr "실행 기록"
@ -4756,10 +4832,6 @@ msgstr "실행 중…"
msgid "Runs when this bot receives a verified message from this provider." msgid "Runs when this bot receives a verified message from this provider."
msgstr "" msgstr ""
#: src/pages/shell/side-panel-header.tsx
msgid "Sandbox"
msgstr ""
#: src/components/AskCard.tsx #: src/components/AskCard.tsx
#: src/components/MediaSettings.tsx #: src/components/MediaSettings.tsx
#: src/components/teach/SkillDraftCard.tsx #: src/components/teach/SkillDraftCard.tsx
@ -4909,6 +4981,10 @@ msgstr "답변 보내기"
msgid "Send it" msgid "Send it"
msgstr "보내기" msgstr "보내기"
#: src/pages/shell/transcript.tsx
msgid "Send now"
msgstr "지금 보내기"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Send reset link" msgid "Send reset link"
msgstr "재설정 링크 보내기" msgstr "재설정 링크 보내기"
@ -5021,6 +5097,10 @@ msgstr ""
msgid "Show computer" msgid "Show computer"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Show details"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Show less" msgid "Show less"
msgstr "" msgstr ""
@ -5037,6 +5117,7 @@ msgstr "더 보기"
msgid "Show password" msgid "Show password"
msgstr "비밀번호 표시" msgstr "비밀번호 표시"
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Show settings" msgid "Show settings"
msgstr "설정 보기" msgstr "설정 보기"
@ -5172,6 +5253,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5286,6 +5368,10 @@ msgstr "가르치기 종료"
msgid "Stop timer" msgid "Stop timer"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
msgid "Stopped"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/lib/browser-notifications.ts #: src/lib/browser-notifications.ts
msgid "Stopped." msgid "Stopped."
@ -5398,6 +5484,7 @@ msgstr "작업을 가르치는 중입니다. 새 메시지를 보내려면 먼
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
#: src/pages/shell/project-overview.tsx
msgid "Team" msgid "Team"
msgstr "팀 공용" msgstr "팀 공용"
@ -5512,10 +5599,6 @@ msgstr "이 구독 로그인 방식은 아직 Rakazo에서 지원되지 않습
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5701,6 +5784,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "최신 상태" msgstr "최신 상태"
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
msgstr "" msgstr ""
#: src/components/stage/OoStage.tsx #: src/components/stage/OoStage.tsx
#: src/pages/shell/project-overview.tsx
msgid "Working" msgid "Working"
msgstr "" msgstr ""
@ -6037,6 +6122,7 @@ msgstr "어제"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
#: src/components/voice/VoiceStage.tsx #: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/transcript.tsx
msgid "You" msgid "You"
msgstr "나" msgstr "나"
@ -6077,13 +6163,25 @@ msgid "Your name"
msgstr "이름" msgstr "이름"
#: src/pages/VoiceSettingsOverlay.tsx #: 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 "" msgstr ""
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom in"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom out"
msgstr ""

View File

@ -161,11 +161,6 @@ msgstr ""
msgid "{0}'s browser appears here when the computer is running." msgid "{0}'s browser appears here when the computer is running."
msgstr "" msgstr ""
#. placeholder {0}: active.name
#: src/pages/shell/computer-panel-preview.tsx
msgid "{0}'s screen"
msgstr ""
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
msgstr "" msgstr ""
@ -182,10 +177,38 @@ msgstr ""
msgid "{botName} clears the path…" msgid "{botName} clears the path…"
msgstr "" 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 #: src/lib/voice-persona-copy.ts
msgid "{botName} is on it…" msgid "{botName} is on it…"
msgstr "" 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/lib/voice-persona-copy.ts
#: src/pages/CallView.tsx #: src/pages/CallView.tsx
msgid "{botName} is working…" msgid "{botName} is working…"
@ -364,6 +387,7 @@ msgstr "{remaining} restantes · o bot está observando, não agindo"
msgid "{size} B" msgid "{size} B"
msgstr "{size} B" msgstr "{size} B"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "{stepCount, plural, one {# step} other {# steps}}" msgid "{stepCount, plural, one {# step} other {# steps}}"
msgstr "" msgstr ""
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
msgid "{toolCount, plural, one {# tool} other {# tools}}" msgid "{toolCount, plural, one {# tool} other {# tools}}"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "{workingBotName} is working"
msgstr "{workingBotName} está trabalhando"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "/Users/you/projects/acme" msgid "/Users/you/projects/acme"
@ -544,11 +564,16 @@ msgstr ""
msgid "Add as projects" msgid "Add as projects"
msgstr "" msgstr ""
#: src/components/NotificationSettingsPanel.tsx
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
msgstr ""
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
msgid "Add Executor" msgid "Add Executor"
msgstr "Adicionar Executor" msgstr "Adicionar Executor"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/project-overview.tsx
msgid "Add goal" msgid "Add goal"
msgstr "" msgstr ""
@ -737,6 +762,14 @@ msgstr "Sempre permitido"
msgid "Analysiert die Aufgabe und führt Tools aus…" msgid "Analysiert die Aufgabe und führt Tools aus…"
msgstr "" 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 #: src/components/AskCard.tsx
msgid "Answer" msgid "Answer"
msgstr "Resposta" msgstr "Resposta"
@ -996,7 +1029,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Iniciando a área de trabalho ao vivo..." msgstr "Iniciando a área de trabalho ao vivo..."
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Inicializando o computador do {0}" msgstr "Inicializando o computador do {0}"
@ -1019,10 +1052,6 @@ msgstr "Bot"
msgid "Bot screen" msgid "Bot screen"
msgstr "Tela do bot" 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 #: src/pages/MessagingSettingsOverlay.tsx
msgid "Bot to link" msgid "Bot to link"
msgstr "" 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." 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." 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 #: src/components/OoAtelier.tsx
msgid "Bow" msgid "Bow"
msgstr "" msgstr ""
@ -1064,7 +1089,6 @@ msgstr ""
msgid "Browse MCP servers" msgid "Browse MCP servers"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Browser" msgid "Browser"
msgstr "" 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." 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." 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 #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Configurado por implantação" msgstr "Configurado por implantação"
@ -1584,6 +1612,10 @@ msgstr "Conectado e usando {0}."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Conectado. Suas ferramentas estão disponíveis na sua próxima mensagem." 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/ComputerUpdateProgress.tsx
#: src/components/teach/TeachComputerOverlay.tsx #: src/components/teach/TeachComputerOverlay.tsx
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Dismiss" msgid "Dismiss"
msgstr "" msgstr ""
@ -2381,7 +2414,9 @@ msgstr ""
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/project-overview.tsx #: src/pages/shell/project-overview.tsx
#: src/pages/shell/transcript.tsx
msgid "Done" msgid "Done"
msgstr "Concluído" msgstr "Concluído"
@ -2394,8 +2429,11 @@ msgstr ""
msgid "Dot" msgid "Dot"
msgstr "" msgstr ""
#. placeholder {0}: activeItem.name
#. placeholder {0}: item.name
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {0}" msgid "Download {0}"
msgstr "Baixar {0}" msgstr "Baixar {0}"
@ -2403,7 +2441,6 @@ msgstr "Baixar {0}"
#: src/components/ArtifactPane.tsx #: src/components/ArtifactPane.tsx
#: src/components/media/MediaCard.tsx #: src/components/media/MediaCard.tsx
#: src/components/media/MediaPlayer.tsx #: src/components/media/MediaPlayer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {name}" msgid "Download {name}"
msgstr "Baixar {name}" msgstr "Baixar {name}"
@ -2589,6 +2626,8 @@ msgid "Expand"
msgstr "Expandir" msgstr "Expandir"
#. placeholder {0}: group.title #. placeholder {0}: group.title
#. placeholder {0}: item.name
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/sidebar-group-header.tsx #: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}" msgid "Expand {0}"
msgstr "" msgstr ""
@ -2660,6 +2699,10 @@ msgstr ""
msgid "Failed to open file" msgid "Failed to open file"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Failed to remove message"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Failed to send message" msgid "Failed to send message"
msgstr "Ocorreu um erro ao enviar a mensagem" msgstr "Ocorreu um erro ao enviar a mensagem"
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
msgstr "" msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx #: src/pages/VoiceSettingsOverlay.tsx
msgid "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 "Olá, é assim que vou soar quando ler as respostas em voz alta." msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Hide bots" msgid "Hide bots"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Hide details"
msgstr ""
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Hide password" msgid "Hide password"
msgstr "" msgstr ""
@ -3080,6 +3128,7 @@ msgstr ""
msgid "Import OpenAPI JSON" msgid "Import OpenAPI JSON"
msgstr "Importar OpenAPI JSON" msgstr "Importar OpenAPI JSON"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "" msgstr ""
@ -3166,6 +3215,10 @@ msgstr "Isolado"
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." 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." 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 #: src/pages/shell/transcript.tsx
msgid "Jump to latest" msgid "Jump to latest"
msgstr "Ir para a mensagem mais recente" msgstr "Ir para a mensagem mais recente"
@ -3174,6 +3227,10 @@ msgstr "Ir para a mensagem mais recente"
msgid "Jump to replied message" msgid "Jump to replied message"
msgstr "Ir para a mensagem respondida" 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/ActivityList.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
msgid "just now" msgid "just now"
@ -3774,6 +3831,7 @@ msgid "New space"
msgstr "" msgstr ""
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/pages/shell/message-cards.tsx
msgid "Next" msgid "Next"
msgstr "" msgstr ""
@ -3885,6 +3943,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "Nenhum provedor encontrado" msgstr "Nenhum provedor encontrado"
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -3899,6 +3961,7 @@ msgid "No results"
msgstr "Nenhum resultado" msgstr "Nenhum resultado"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "No runs yet" msgid "No runs yet"
msgstr "" msgstr ""
@ -3957,6 +4020,7 @@ msgid "Notifications"
msgstr "Notificações" msgstr "Notificações"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Now" msgid "Now"
msgstr "Agora" msgstr "Agora"
@ -4027,7 +4091,6 @@ msgid "Oo name"
msgstr "" msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
msgstr "Abrir" msgstr "Abrir"
@ -4056,7 +4119,7 @@ msgstr ""
msgid "Open in browser" msgid "Open in browser"
msgstr "" msgstr ""
#: src/pages/shell/computer-panel-preview.tsx #: src/components/browser/OoComputerBrowser.tsx
msgid "Open in full window" msgid "Open in full window"
msgstr "Abrir em janela cheia" msgstr "Abrir em janela cheia"
@ -4288,10 +4351,6 @@ msgstr ""
msgid "Project" msgid "Project"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Project · sessions & folders"
msgstr ""
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "Project folder" msgid "Project folder"
@ -4399,6 +4458,10 @@ msgstr ""
msgid "Ready" msgid "Ready"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
msgid "Ready"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Reasoning effort" msgid "Reasoning effort"
msgstr "Esforço de raciocínio" msgstr "Esforço de raciocínio"
@ -4412,6 +4475,7 @@ msgid "Recall only"
msgstr "" msgstr ""
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Recent" msgid "Recent"
msgstr "Recente" msgstr "Recente"
@ -4528,6 +4592,10 @@ msgstr "Remover {0}"
msgid "Remove folder" msgid "Remove folder"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Remove from queue"
msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
msgid "Remove Git event" msgid "Remove Git event"
msgstr "" 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" msgid "Restoring your workspace"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Resume"
msgstr ""
#: src/components/FolderPickerDialog.tsx #: src/components/FolderPickerDialog.tsx
msgid "Retry" msgid "Retry"
msgstr "" msgstr ""
@ -4700,11 +4773,13 @@ msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/routine-event-line.tsx #: src/pages/shell/routine-event-line.tsx
msgid "Routine" msgid "Routine"
msgstr "Rotina" msgstr "Rotina"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Routines" msgid "Routines"
msgstr "Rotinas" msgstr "Rotinas"
@ -4718,6 +4793,7 @@ msgid "Run at"
msgstr "" msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Run history" msgid "Run history"
msgstr "" msgstr ""
@ -4756,10 +4832,6 @@ msgstr "Executando..."
msgid "Runs when this bot receives a verified message from this provider." msgid "Runs when this bot receives a verified message from this provider."
msgstr "" msgstr ""
#: src/pages/shell/side-panel-header.tsx
msgid "Sandbox"
msgstr ""
#: src/components/AskCard.tsx #: src/components/AskCard.tsx
#: src/components/MediaSettings.tsx #: src/components/MediaSettings.tsx
#: src/components/teach/SkillDraftCard.tsx #: src/components/teach/SkillDraftCard.tsx
@ -4909,6 +4981,10 @@ msgstr "Enviar resposta"
msgid "Send it" msgid "Send it"
msgstr "Enviar" msgstr "Enviar"
#: src/pages/shell/transcript.tsx
msgid "Send now"
msgstr "Enviar agora"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Send reset link" msgid "Send reset link"
msgstr "Enviar link de redefinição" msgstr "Enviar link de redefinição"
@ -5021,6 +5097,10 @@ msgstr ""
msgid "Show computer" msgid "Show computer"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Show details"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Show less" msgid "Show less"
msgstr "" msgstr ""
@ -5037,6 +5117,7 @@ msgstr "Mostrar mais"
msgid "Show password" msgid "Show password"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Show settings" msgid "Show settings"
msgstr "Mostrar configurações" msgstr "Mostrar configurações"
@ -5172,6 +5253,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5286,6 +5368,10 @@ msgstr "Parar de ensinar"
msgid "Stop timer" msgid "Stop timer"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
msgid "Stopped"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/lib/browser-notifications.ts #: src/lib/browser-notifications.ts
msgid "Stopped." 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/ProjectSessionTimeline.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
#: src/pages/shell/project-overview.tsx
msgid "Team" msgid "Team"
msgstr "Equipe" 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." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5701,6 +5784,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "" msgstr ""
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
msgstr "" msgstr ""
#: src/components/stage/OoStage.tsx #: src/components/stage/OoStage.tsx
#: src/pages/shell/project-overview.tsx
msgid "Working" msgid "Working"
msgstr "" msgstr ""
@ -6037,6 +6122,7 @@ msgstr "Ontem"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
#: src/components/voice/VoiceStage.tsx #: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/transcript.tsx
msgid "You" msgid "You"
msgstr "Você" msgstr "Você"
@ -6077,13 +6163,25 @@ msgid "Your name"
msgstr "O seu nome" msgstr "O seu nome"
#: src/pages/VoiceSettingsOverlay.tsx #: 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 "" msgstr ""
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom in"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom out"
msgstr ""

View File

@ -161,11 +161,6 @@ msgstr ""
msgid "{0}'s browser appears here when the computer is running." msgid "{0}'s browser appears here when the computer is running."
msgstr "" msgstr ""
#. placeholder {0}: active.name
#: src/pages/shell/computer-panel-preview.tsx
msgid "{0}'s screen"
msgstr "Экран {0}"
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
msgstr "" msgstr ""
@ -182,10 +177,38 @@ msgstr ""
msgid "{botName} clears the path…" msgid "{botName} clears the path…"
msgstr "" 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 #: src/lib/voice-persona-copy.ts
msgid "{botName} is on it…" msgid "{botName} is on it…"
msgstr "" 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/lib/voice-persona-copy.ts
#: src/pages/CallView.tsx #: src/pages/CallView.tsx
msgid "{botName} is working…" msgid "{botName} is working…"
@ -364,6 +387,7 @@ msgstr "Осталось: {remaining} · бот наблюдает, но не д
msgid "{size} B" msgid "{size} B"
msgstr "{size} Б" msgstr "{size} Б"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "{stepCount, plural, one {# step} other {# steps}}" msgid "{stepCount, plural, one {# step} other {# steps}}"
msgstr "" msgstr ""
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
msgid "{toolCount, plural, one {# tool} other {# tools}}" msgid "{toolCount, plural, one {# tool} other {# tools}}"
msgstr "{toolCount, plural, one {# инструмент} few {# инструмента} many {# инструментов} other {# инструмента}}" 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/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "/Users/you/projects/acme" msgid "/Users/you/projects/acme"
@ -544,11 +564,16 @@ msgstr "Добавить еще"
msgid "Add as projects" msgid "Add as projects"
msgstr "" msgstr ""
#: src/components/NotificationSettingsPanel.tsx
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
msgstr ""
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
msgid "Add Executor" msgid "Add Executor"
msgstr "Добавить Executor" msgstr "Добавить Executor"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/project-overview.tsx
msgid "Add goal" msgid "Add goal"
msgstr "" msgstr ""
@ -737,6 +762,14 @@ msgstr "Всегда разрешено"
msgid "Analysiert die Aufgabe und führt Tools aus…" msgid "Analysiert die Aufgabe und führt Tools aus…"
msgstr "" 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 #: src/components/AskCard.tsx
msgid "Answer" msgid "Answer"
msgstr "Ответ" msgstr "Ответ"
@ -996,7 +1029,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Загрузка рабочего стола…" msgstr "Загрузка рабочего стола…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Загрузка компьютера {0}" msgstr "Загрузка компьютера {0}"
@ -1019,10 +1052,6 @@ msgstr "Бот"
msgid "Bot screen" msgid "Bot screen"
msgstr "Экран бота" msgstr "Экран бота"
#: src/pages/shell/computer-panel-preview.tsx
msgid "Bot screen preview"
msgstr "Предварительный просмотр экрана бота"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Bot to link" msgid "Bot to link"
msgstr "Бот для ссылки" 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." msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
msgstr "По умолчанию боты действуют, не спрашивая. Добавляйте исключение только в том случае, если вы хотите сначала просмотреть тип действия." msgstr "По умолчанию боты действуют, не спрашивая. Добавляйте исключение только в том случае, если вы хотите сначала просмотреть тип действия."
#: src/pages/shell/transcript.tsx
msgid "Bots are working"
msgstr "Боты работают"
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Bow" msgid "Bow"
msgstr "" msgstr ""
@ -1064,7 +1089,6 @@ msgstr ""
msgid "Browse MCP servers" msgid "Browse MCP servers"
msgstr "Просмотреть MCP-серверы" msgstr "Просмотреть MCP-серверы"
#: src/pages/Shell.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Browser" msgid "Browser"
msgstr "" 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." 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." 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 #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Настраивается путем развертывания" msgstr "Настраивается путем развертывания"
@ -1584,6 +1612,10 @@ msgstr "Подключено и используется {0}."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Подключено. Его инструменты доступны из вашего следующего сообщения." msgstr "Подключено. Его инструменты доступны из вашего следующего сообщения."
@ -2322,6 +2354,7 @@ msgstr "Отключение…"
#: src/components/ComputerUpdateProgress.tsx #: src/components/ComputerUpdateProgress.tsx
#: src/components/teach/TeachComputerOverlay.tsx #: src/components/teach/TeachComputerOverlay.tsx
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Dismiss" msgid "Dismiss"
msgstr "Закрыть" msgstr "Закрыть"
@ -2381,7 +2414,9 @@ msgstr ""
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/project-overview.tsx #: src/pages/shell/project-overview.tsx
#: src/pages/shell/transcript.tsx
msgid "Done" msgid "Done"
msgstr "Готово" msgstr "Готово"
@ -2394,8 +2429,11 @@ msgstr ""
msgid "Dot" msgid "Dot"
msgstr "" msgstr ""
#. placeholder {0}: activeItem.name
#. placeholder {0}: item.name
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {0}" msgid "Download {0}"
msgstr "Скачать {0}" msgstr "Скачать {0}"
@ -2403,7 +2441,6 @@ msgstr "Скачать {0}"
#: src/components/ArtifactPane.tsx #: src/components/ArtifactPane.tsx
#: src/components/media/MediaCard.tsx #: src/components/media/MediaCard.tsx
#: src/components/media/MediaPlayer.tsx #: src/components/media/MediaPlayer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {name}" msgid "Download {name}"
msgstr "Скачать {name}" msgstr "Скачать {name}"
@ -2589,6 +2626,8 @@ msgid "Expand"
msgstr "Развернуть" msgstr "Развернуть"
#. placeholder {0}: group.title #. placeholder {0}: group.title
#. placeholder {0}: item.name
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/sidebar-group-header.tsx #: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}" msgid "Expand {0}"
msgstr "Развернуть {0}" msgstr "Развернуть {0}"
@ -2660,6 +2699,10 @@ msgstr ""
msgid "Failed to open file" msgid "Failed to open file"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Failed to remove message"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Failed to send message" msgid "Failed to send message"
msgstr "Не удалось отправить сообщение" msgstr "Не удалось отправить сообщение"
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
msgstr "" msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx #: src/pages/VoiceSettingsOverlay.tsx
msgid "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 "Привет, вот как я буду звучать, когда буду читать ответы вслух." msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Hide bots" msgid "Hide bots"
msgstr "Скрыть ботов" msgstr "Скрыть ботов"
#: src/pages/shell/transcript.tsx
msgid "Hide details"
msgstr ""
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Hide password" msgid "Hide password"
msgstr "Скрыть пароль" msgstr "Скрыть пароль"
@ -3080,6 +3128,7 @@ msgstr ""
msgid "Import OpenAPI JSON" msgid "Import OpenAPI JSON"
msgstr "Импортировать OpenAPI JSON" msgstr "Импортировать OpenAPI JSON"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "" msgstr ""
@ -3166,6 +3215,10 @@ msgstr "изолированный"
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "Его диалог, файлы и задачи будут безвозвратно удалены. Созданные им боты останутся в вашем списке." msgstr "Его диалог, файлы и задачи будут безвозвратно удалены. Созданные им боты останутся в вашем списке."
#: src/pages/shell/transcript.tsx
msgid "joined"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Jump to latest" msgid "Jump to latest"
msgstr "Перейти к последнему" msgstr "Перейти к последнему"
@ -3174,6 +3227,10 @@ msgstr "Перейти к последнему"
msgid "Jump to replied message" msgid "Jump to replied message"
msgstr "Перейти к ответному сообщению" 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/ActivityList.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
msgid "just now" msgid "just now"
@ -3774,6 +3831,7 @@ msgid "New space"
msgstr "Новое пространство" msgstr "Новое пространство"
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/pages/shell/message-cards.tsx
msgid "Next" msgid "Next"
msgstr "" msgstr ""
@ -3885,6 +3943,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "Поставщики не найдены." msgstr "Поставщики не найдены."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "Удаленные MCP-серверы не найдены" msgstr "Удаленные MCP-серверы не найдены"
@ -3899,6 +3961,7 @@ msgid "No results"
msgstr "Нет результатов" msgstr "Нет результатов"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "No runs yet" msgid "No runs yet"
msgstr "Запусков пока нет" msgstr "Запусков пока нет"
@ -3957,6 +4020,7 @@ msgid "Notifications"
msgstr "Уведомления" msgstr "Уведомления"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Now" msgid "Now"
msgstr "Сейчас" msgstr "Сейчас"
@ -4027,7 +4091,6 @@ msgid "Oo name"
msgstr "" msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
msgstr "Открыть" msgstr "Открыть"
@ -4056,7 +4119,7 @@ msgstr ""
msgid "Open in browser" msgid "Open in browser"
msgstr "" msgstr ""
#: src/pages/shell/computer-panel-preview.tsx #: src/components/browser/OoComputerBrowser.tsx
msgid "Open in full window" msgid "Open in full window"
msgstr "Открыть в полном окне" msgstr "Открыть в полном окне"
@ -4288,10 +4351,6 @@ msgstr "Приватное пространство со своими ботам
msgid "Project" msgid "Project"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Project · sessions & folders"
msgstr ""
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "Project folder" msgid "Project folder"
@ -4399,6 +4458,10 @@ msgstr ""
msgid "Ready" msgid "Ready"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
msgid "Ready"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Reasoning effort" msgid "Reasoning effort"
msgstr "" msgstr ""
@ -4412,6 +4475,7 @@ msgid "Recall only"
msgstr "" msgstr ""
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Recent" msgid "Recent"
msgstr "Недавние" msgstr "Недавние"
@ -4528,6 +4592,10 @@ msgstr "Удалить {0}"
msgid "Remove folder" msgid "Remove folder"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Remove from queue"
msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
msgid "Remove Git event" msgid "Remove Git event"
msgstr "Удалить событие Git" msgstr "Удалить событие Git"
@ -4653,6 +4721,11 @@ msgstr "Восстановите последнее сохраненное ра
msgid "Restoring your workspace" msgid "Restoring your workspace"
msgstr "Восстановление вашего рабочего пространства" msgstr "Восстановление вашего рабочего пространства"
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Resume"
msgstr ""
#: src/components/FolderPickerDialog.tsx #: src/components/FolderPickerDialog.tsx
msgid "Retry" msgid "Retry"
msgstr "" msgstr ""
@ -4700,11 +4773,13 @@ msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/routine-event-line.tsx #: src/pages/shell/routine-event-line.tsx
msgid "Routine" msgid "Routine"
msgstr "Задача" msgstr "Задача"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Routines" msgid "Routines"
msgstr "Задачи" msgstr "Задачи"
@ -4718,6 +4793,7 @@ msgid "Run at"
msgstr "Запустить в" msgstr "Запустить в"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Run history" msgid "Run history"
msgstr "История запуска" msgstr "История запуска"
@ -4756,10 +4832,6 @@ msgstr "Выполняется…"
msgid "Runs when this bot receives a verified message from this provider." msgid "Runs when this bot receives a verified message from this provider."
msgstr "Запускается, когда этот бот получает проверенное сообщение от этого провайдера." msgstr "Запускается, когда этот бот получает проверенное сообщение от этого провайдера."
#: src/pages/shell/side-panel-header.tsx
msgid "Sandbox"
msgstr ""
#: src/components/AskCard.tsx #: src/components/AskCard.tsx
#: src/components/MediaSettings.tsx #: src/components/MediaSettings.tsx
#: src/components/teach/SkillDraftCard.tsx #: src/components/teach/SkillDraftCard.tsx
@ -4909,6 +4981,10 @@ msgstr "Отправить ответ"
msgid "Send it" msgid "Send it"
msgstr "Отправить" msgstr "Отправить"
#: src/pages/shell/transcript.tsx
msgid "Send now"
msgstr "Отправить сейчас"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Send reset link" msgid "Send reset link"
msgstr "Отправить ссылку для сброса" msgstr "Отправить ссылку для сброса"
@ -5021,6 +5097,10 @@ msgstr "Показать ботов"
msgid "Show computer" msgid "Show computer"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Show details"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Show less" msgid "Show less"
msgstr "" msgstr ""
@ -5037,6 +5117,7 @@ msgstr "Показать ещё"
msgid "Show password" msgid "Show password"
msgstr "Показать пароль" msgstr "Показать пароль"
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Show settings" msgid "Show settings"
msgstr "Показать настройки" msgstr "Показать настройки"
@ -5172,6 +5253,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5286,6 +5368,10 @@ msgstr "Остановить обучение"
msgid "Stop timer" msgid "Stop timer"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
msgid "Stopped"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/lib/browser-notifications.ts #: src/lib/browser-notifications.ts
msgid "Stopped." msgid "Stopped."
@ -5398,6 +5484,7 @@ msgstr "Обучение в процессе. Прекратите обучен
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
#: src/pages/shell/project-overview.tsx
msgid "Team" msgid "Team"
msgstr "Команда" msgstr "Команда"
@ -5512,10 +5599,6 @@ msgstr "Вход по этой подписке пока недоступен в
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5701,6 +5784,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "Актуально" msgstr "Актуально"
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
msgstr "" msgstr ""
#: src/components/stage/OoStage.tsx #: src/components/stage/OoStage.tsx
#: src/pages/shell/project-overview.tsx
msgid "Working" msgid "Working"
msgstr "" msgstr ""
@ -6037,6 +6122,7 @@ msgstr "Вчера"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
#: src/components/voice/VoiceStage.tsx #: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/transcript.tsx
msgid "You" msgid "You"
msgstr "Вы" msgstr "Вы"
@ -6077,13 +6163,25 @@ msgid "Your name"
msgstr "Ваше имя" msgstr "Ваше имя"
#: src/pages/VoiceSettingsOverlay.tsx #: 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 "" msgstr ""
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom in"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom out"
msgstr ""

View File

@ -161,11 +161,6 @@ msgstr ""
msgid "{0}'s browser appears here when the computer is running." msgid "{0}'s browser appears here when the computer is running."
msgstr "" msgstr ""
#. placeholder {0}: active.name
#: src/pages/shell/computer-panel-preview.tsx
msgid "{0}'s screen"
msgstr ""
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
msgstr "" msgstr ""
@ -182,10 +177,38 @@ msgstr ""
msgid "{botName} clears the path…" msgid "{botName} clears the path…"
msgstr "" 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 #: src/lib/voice-persona-copy.ts
msgid "{botName} is on it…" msgid "{botName} is on it…"
msgstr "" 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/lib/voice-persona-copy.ts
#: src/pages/CallView.tsx #: src/pages/CallView.tsx
msgid "{botName} is working…" msgid "{botName} is working…"
@ -364,6 +387,7 @@ msgstr "{remaining} kaldı · bot yalnızca izliyor"
msgid "{size} B" msgid "{size} B"
msgstr "{size} B" msgstr "{size} B"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "{stepCount, plural, one {# step} other {# steps}}" msgid "{stepCount, plural, one {# step} other {# steps}}"
msgstr "" msgstr ""
@ -386,10 +410,6 @@ msgstr "{title}, {label}"
msgid "{toolCount, plural, one {# tool} other {# tools}}" msgid "{toolCount, plural, one {# tool} other {# tools}}"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "{workingBotName} is working"
msgstr "{workingBotName} çalışıyor"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "/Users/you/projects/acme" msgid "/Users/you/projects/acme"
@ -544,11 +564,16 @@ msgstr ""
msgid "Add as projects" msgid "Add as projects"
msgstr "" msgstr ""
#: src/components/NotificationSettingsPanel.tsx
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
msgstr ""
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
msgid "Add Executor" msgid "Add Executor"
msgstr "Executor ekle" msgstr "Executor ekle"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/project-overview.tsx
msgid "Add goal" msgid "Add goal"
msgstr "" msgstr ""
@ -737,6 +762,14 @@ msgstr "Her zaman izinli"
msgid "Analysiert die Aufgabe und führt Tools aus…" msgid "Analysiert die Aufgabe und führt Tools aus…"
msgstr "" 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 #: src/components/AskCard.tsx
msgid "Answer" msgid "Answer"
msgstr "Yanıtla" msgstr "Yanıtla"
@ -996,7 +1029,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Canlı masaüstü başlatılıyor…" msgstr "Canlı masaüstü başlatılıyor…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "{0} adlı botun bilgisayarı başlatılıyor" msgstr "{0} adlı botun bilgisayarı başlatılıyor"
@ -1019,10 +1052,6 @@ msgstr "Bot"
msgid "Bot screen" msgid "Bot screen"
msgstr "Bot ekranı" msgstr "Bot ekranı"
#: src/pages/shell/computer-panel-preview.tsx
msgid "Bot screen preview"
msgstr "Bot ekranı önizlemesi"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Bot to link" msgid "Bot to link"
msgstr "" 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." 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." 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 #: src/components/OoAtelier.tsx
msgid "Bow" msgid "Bow"
msgstr "" msgstr ""
@ -1064,7 +1089,6 @@ msgstr ""
msgid "Browse MCP servers" msgid "Browse MCP servers"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Browser" msgid "Browser"
msgstr "" 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." 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." 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 #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Dağıtım tarafından yapılandırıldı" msgstr "Dağıtım tarafından yapılandırıldı"
@ -1584,6 +1612,10 @@ msgstr "Bağlandı, {0} kullanılıyor."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Bağlandı. Araçları bir sonraki mesajından itibaren kullanılabilir." 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/ComputerUpdateProgress.tsx
#: src/components/teach/TeachComputerOverlay.tsx #: src/components/teach/TeachComputerOverlay.tsx
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Dismiss" msgid "Dismiss"
msgstr "" msgstr ""
@ -2381,7 +2414,9 @@ msgstr ""
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/project-overview.tsx #: src/pages/shell/project-overview.tsx
#: src/pages/shell/transcript.tsx
msgid "Done" msgid "Done"
msgstr "Bitti" msgstr "Bitti"
@ -2394,8 +2429,11 @@ msgstr ""
msgid "Dot" msgid "Dot"
msgstr "" msgstr ""
#. placeholder {0}: activeItem.name
#. placeholder {0}: item.name
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {0}" msgid "Download {0}"
msgstr "{0} indir" msgstr "{0} indir"
@ -2403,7 +2441,6 @@ msgstr "{0} indir"
#: src/components/ArtifactPane.tsx #: src/components/ArtifactPane.tsx
#: src/components/media/MediaCard.tsx #: src/components/media/MediaCard.tsx
#: src/components/media/MediaPlayer.tsx #: src/components/media/MediaPlayer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {name}" msgid "Download {name}"
msgstr "{name} indir" msgstr "{name} indir"
@ -2589,6 +2626,8 @@ msgid "Expand"
msgstr "Genişlet" msgstr "Genişlet"
#. placeholder {0}: group.title #. placeholder {0}: group.title
#. placeholder {0}: item.name
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/sidebar-group-header.tsx #: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}" msgid "Expand {0}"
msgstr "" msgstr ""
@ -2660,6 +2699,10 @@ msgstr ""
msgid "Failed to open file" msgid "Failed to open file"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Failed to remove message"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Failed to send message" msgid "Failed to send message"
msgstr "Mesaj gönderilemedi" msgstr "Mesaj gönderilemedi"
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
msgstr "" msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx #: src/pages/VoiceSettingsOverlay.tsx
msgid "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 "Merhaba, yanıtları sesli okuduğumda böyle duyulacağım." msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Hide bots" msgid "Hide bots"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Hide details"
msgstr ""
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Hide password" msgid "Hide password"
msgstr "" msgstr ""
@ -3080,6 +3128,7 @@ msgstr ""
msgid "Import OpenAPI JSON" msgid "Import OpenAPI JSON"
msgstr "OpenAPI JSON içe aktar" msgstr "OpenAPI JSON içe aktar"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "" 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." 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." 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 #: src/pages/shell/transcript.tsx
msgid "Jump to latest" msgid "Jump to latest"
msgstr "En son mesaja git" msgstr "En son mesaja git"
@ -3174,6 +3227,10 @@ msgstr "En son mesaja git"
msgid "Jump to replied message" msgid "Jump to replied message"
msgstr "Yanıtlanan mesaja git" 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/ActivityList.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
msgid "just now" msgid "just now"
@ -3774,6 +3831,7 @@ msgid "New space"
msgstr "" msgstr ""
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/pages/shell/message-cards.tsx
msgid "Next" msgid "Next"
msgstr "" msgstr ""
@ -3885,6 +3943,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "Sağlayıcı bulunamadı." msgstr "Sağlayıcı bulunamadı."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -3899,6 +3961,7 @@ msgid "No results"
msgstr "Sonuç yok" msgstr "Sonuç yok"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "No runs yet" msgid "No runs yet"
msgstr "" msgstr ""
@ -3957,6 +4020,7 @@ msgid "Notifications"
msgstr "Bildirimler" msgstr "Bildirimler"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Now" msgid "Now"
msgstr "Şimdi" msgstr "Şimdi"
@ -4027,7 +4091,6 @@ msgid "Oo name"
msgstr "" msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
msgstr "Aç" msgstr "Aç"
@ -4056,7 +4119,7 @@ msgstr ""
msgid "Open in browser" msgid "Open in browser"
msgstr "" msgstr ""
#: src/pages/shell/computer-panel-preview.tsx #: src/components/browser/OoComputerBrowser.tsx
msgid "Open in full window" msgid "Open in full window"
msgstr "Tam pencerede aç" msgstr "Tam pencerede aç"
@ -4288,10 +4351,6 @@ msgstr ""
msgid "Project" msgid "Project"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Project · sessions & folders"
msgstr ""
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "Project folder" msgid "Project folder"
@ -4399,6 +4458,10 @@ msgstr ""
msgid "Ready" msgid "Ready"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
msgid "Ready"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Reasoning effort" msgid "Reasoning effort"
msgstr "Akıl yürütme düzeyi" msgstr "Akıl yürütme düzeyi"
@ -4412,6 +4475,7 @@ msgid "Recall only"
msgstr "" msgstr ""
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Recent" msgid "Recent"
msgstr "Son" msgstr "Son"
@ -4528,6 +4592,10 @@ msgstr "{0} kaldır"
msgid "Remove folder" msgid "Remove folder"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Remove from queue"
msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
msgid "Remove Git event" msgid "Remove Git event"
msgstr "" msgstr ""
@ -4653,6 +4721,11 @@ msgstr ""
msgid "Restoring your workspace" msgid "Restoring your workspace"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Resume"
msgstr ""
#: src/components/FolderPickerDialog.tsx #: src/components/FolderPickerDialog.tsx
msgid "Retry" msgid "Retry"
msgstr "" msgstr ""
@ -4700,11 +4773,13 @@ msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/routine-event-line.tsx #: src/pages/shell/routine-event-line.tsx
msgid "Routine" msgid "Routine"
msgstr "Rutin" msgstr "Rutin"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Routines" msgid "Routines"
msgstr "Rutinler" msgstr "Rutinler"
@ -4718,6 +4793,7 @@ msgid "Run at"
msgstr "" msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Run history" msgid "Run history"
msgstr "" msgstr ""
@ -4756,10 +4832,6 @@ msgstr "Çalışıyor…"
msgid "Runs when this bot receives a verified message from this provider." msgid "Runs when this bot receives a verified message from this provider."
msgstr "" msgstr ""
#: src/pages/shell/side-panel-header.tsx
msgid "Sandbox"
msgstr ""
#: src/components/AskCard.tsx #: src/components/AskCard.tsx
#: src/components/MediaSettings.tsx #: src/components/MediaSettings.tsx
#: src/components/teach/SkillDraftCard.tsx #: src/components/teach/SkillDraftCard.tsx
@ -4909,6 +4981,10 @@ msgstr "Yanıtı gönder"
msgid "Send it" msgid "Send it"
msgstr "Gönder" msgstr "Gönder"
#: src/pages/shell/transcript.tsx
msgid "Send now"
msgstr "Şimdi gönder"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Send reset link" msgid "Send reset link"
msgstr "Sıfırlama bağlantısı gönder" msgstr "Sıfırlama bağlantısı gönder"
@ -5021,6 +5097,10 @@ msgstr ""
msgid "Show computer" msgid "Show computer"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Show details"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Show less" msgid "Show less"
msgstr "" msgstr ""
@ -5037,6 +5117,7 @@ msgstr "Daha fazla göster"
msgid "Show password" msgid "Show password"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Show settings" msgid "Show settings"
msgstr "Ayarları göster" msgstr "Ayarları göster"
@ -5172,6 +5253,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5286,6 +5368,10 @@ msgstr "Öğretmeyi durdur"
msgid "Stop timer" msgid "Stop timer"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
msgid "Stopped"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/lib/browser-notifications.ts #: src/lib/browser-notifications.ts
msgid "Stopped." msgid "Stopped."
@ -5398,6 +5484,7 @@ msgstr "Öğretme sürüyor. Yeni mesaj göndermeden önce öğretmeyi durdur."
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
#: src/pages/shell/project-overview.tsx
msgid "Team" msgid "Team"
msgstr "Takım" 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." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5701,6 +5784,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "" msgstr ""
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
msgstr "" msgstr ""
#: src/components/stage/OoStage.tsx #: src/components/stage/OoStage.tsx
#: src/pages/shell/project-overview.tsx
msgid "Working" msgid "Working"
msgstr "" msgstr ""
@ -6037,6 +6122,7 @@ msgstr "Dün"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
#: src/components/voice/VoiceStage.tsx #: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/transcript.tsx
msgid "You" msgid "You"
msgstr "Sen" msgstr "Sen"
@ -6077,13 +6163,25 @@ msgid "Your name"
msgstr "Adın" msgstr "Adın"
#: src/pages/VoiceSettingsOverlay.tsx #: 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 "" msgstr ""
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom in"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom out"
msgstr ""

View File

@ -161,11 +161,6 @@ msgstr ""
msgid "{0}'s browser appears here when the computer is running." msgid "{0}'s browser appears here when the computer is running."
msgstr "" msgstr ""
#. placeholder {0}: active.name
#: src/pages/shell/computer-panel-preview.tsx
msgid "{0}'s screen"
msgstr ""
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}"
msgstr "" msgstr ""
@ -182,10 +177,38 @@ msgstr ""
msgid "{botName} clears the path…" msgid "{botName} clears the path…"
msgstr "" 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 #: src/lib/voice-persona-copy.ts
msgid "{botName} is on it…" msgid "{botName} is on it…"
msgstr "" 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/lib/voice-persona-copy.ts
#: src/pages/CallView.tsx #: src/pages/CallView.tsx
msgid "{botName} is working…" msgid "{botName} is working…"
@ -364,6 +387,7 @@ msgstr "剩余 {remaining} · Bot 只看不操作"
msgid "{size} B" msgid "{size} B"
msgstr "{size} B" msgstr "{size} B"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "{stepCount, plural, one {# step} other {# steps}}" msgid "{stepCount, plural, one {# step} other {# steps}}"
msgstr "" msgstr ""
@ -386,10 +410,6 @@ msgstr "{title},{label}"
msgid "{toolCount, plural, one {# tool} other {# tools}}" msgid "{toolCount, plural, one {# tool} other {# tools}}"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "{workingBotName} is working"
msgstr "{workingBotName} 正在工作"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "/Users/you/projects/acme" msgid "/Users/you/projects/acme"
@ -544,11 +564,16 @@ msgstr ""
msgid "Add as projects" msgid "Add as projects"
msgstr "" msgstr ""
#: src/components/NotificationSettingsPanel.tsx
msgid "Add evOo to your Home Screen via Share to enable push on iOS."
msgstr ""
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
msgid "Add Executor" msgid "Add Executor"
msgstr "添加 Executor" msgstr "添加 Executor"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/project-overview.tsx
msgid "Add goal" msgid "Add goal"
msgstr "" msgstr ""
@ -737,6 +762,14 @@ msgstr "始终允许"
msgid "Analysiert die Aufgabe und führt Tools aus…" msgid "Analysiert die Aufgabe und führt Tools aus…"
msgstr "" 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 #: src/components/AskCard.tsx
msgid "Answer" msgid "Answer"
msgstr "回答" msgstr "回答"
@ -996,7 +1029,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "正在启动实时桌面…" msgstr "正在启动实时桌面…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "正在启动 {0} 的电脑" msgstr "正在启动 {0} 的电脑"
@ -1019,10 +1052,6 @@ msgstr "Bot"
msgid "Bot screen" msgid "Bot screen"
msgstr "Bot 屏幕" msgstr "Bot 屏幕"
#: src/pages/shell/computer-panel-preview.tsx
msgid "Bot screen preview"
msgstr "Bot 屏幕预览"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Bot to link" msgid "Bot to link"
msgstr "要关联的 Bot" 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." msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first."
msgstr "默认情况下 Bot 会直接执行。只有当你想先审核某类操作时,才添加例外。" msgstr "默认情况下 Bot 会直接执行。只有当你想先审核某类操作时,才添加例外。"
#: src/pages/shell/transcript.tsx
msgid "Bots are working"
msgstr "Bot 正在工作"
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Bow" msgid "Bow"
msgstr "" msgstr ""
@ -1064,7 +1089,6 @@ msgstr ""
msgid "Browse MCP servers" msgid "Browse MCP servers"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Browser" msgid "Browser"
msgstr "" 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." 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 后请重建环境。" 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 #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "由部署配置" msgstr "由部署配置"
@ -1584,6 +1612,10 @@ msgstr "已连接并使用 {0}。"
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "已连接,下一条消息起即可使用其工具。" msgstr "已连接,下一条消息起即可使用其工具。"
@ -2322,6 +2354,7 @@ msgstr "正在断开…"
#: src/components/ComputerUpdateProgress.tsx #: src/components/ComputerUpdateProgress.tsx
#: src/components/teach/TeachComputerOverlay.tsx #: src/components/teach/TeachComputerOverlay.tsx
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Dismiss" msgid "Dismiss"
msgstr "" msgstr ""
@ -2381,7 +2414,9 @@ msgstr ""
#: src/components/ThreadCard.tsx #: src/components/ThreadCard.tsx
#: src/pages/HeuteBrief.tsx #: src/pages/HeuteBrief.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/project-overview.tsx #: src/pages/shell/project-overview.tsx
#: src/pages/shell/transcript.tsx
msgid "Done" msgid "Done"
msgstr "完成" msgstr "完成"
@ -2394,8 +2429,11 @@ msgstr ""
msgid "Dot" msgid "Dot"
msgstr "" msgstr ""
#. placeholder {0}: activeItem.name
#. placeholder {0}: item.name
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {0}" msgid "Download {0}"
msgstr "下载 {0}" msgstr "下载 {0}"
@ -2403,7 +2441,6 @@ msgstr "下载 {0}"
#: src/components/ArtifactPane.tsx #: src/components/ArtifactPane.tsx
#: src/components/media/MediaCard.tsx #: src/components/media/MediaCard.tsx
#: src/components/media/MediaPlayer.tsx #: src/components/media/MediaPlayer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Download {name}" msgid "Download {name}"
msgstr "下载 {name}" msgstr "下载 {name}"
@ -2589,6 +2626,8 @@ msgid "Expand"
msgstr "展开" msgstr "展开"
#. placeholder {0}: group.title #. placeholder {0}: group.title
#. placeholder {0}: item.name
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/sidebar-group-header.tsx #: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}" msgid "Expand {0}"
msgstr "展开 {0}" msgstr "展开 {0}"
@ -2660,6 +2699,10 @@ msgstr ""
msgid "Failed to open file" msgid "Failed to open file"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Failed to remove message"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Failed to send message" msgid "Failed to send message"
msgstr "发送消息失败" msgstr "发送消息失败"
@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…"
msgstr "" msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx #: src/pages/VoiceSettingsOverlay.tsx
msgid "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 "你好,这是我朗读回复时的声音。" msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Hide bots" msgid "Hide bots"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Hide details"
msgstr ""
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Hide password" msgid "Hide password"
msgstr "隐藏密码" msgstr "隐藏密码"
@ -3080,6 +3128,7 @@ msgstr ""
msgid "Import OpenAPI JSON" msgid "Import OpenAPI JSON"
msgstr "导入 OpenAPI JSON" msgstr "导入 OpenAPI JSON"
#: src/pages/shell/message-cards.tsx
#: src/pages/shell/run-steps.tsx #: src/pages/shell/run-steps.tsx
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "" msgstr ""
@ -3166,6 +3215,10 @@ msgstr "隔离"
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "它的对话、文件和例行任务将被永久删除。它创建的 Bot 仍留在列表中。" msgstr "它的对话、文件和例行任务将被永久删除。它创建的 Bot 仍留在列表中。"
#: src/pages/shell/transcript.tsx
msgid "joined"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Jump to latest" msgid "Jump to latest"
msgstr "跳到最新" msgstr "跳到最新"
@ -3174,6 +3227,10 @@ msgstr "跳到最新"
msgid "Jump to replied message" msgid "Jump to replied message"
msgstr "跳到被回复的消息" 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/ActivityList.tsx
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
msgid "just now" msgid "just now"
@ -3774,6 +3831,7 @@ msgid "New space"
msgstr "新建工作区" msgstr "新建工作区"
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/pages/shell/message-cards.tsx
msgid "Next" msgid "Next"
msgstr "" msgstr ""
@ -3885,6 +3943,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "未找到提供方。" msgstr "未找到提供方。"
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -3899,6 +3961,7 @@ msgid "No results"
msgstr "无结果" msgstr "无结果"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "No runs yet" msgid "No runs yet"
msgstr "还没有运行记录" msgstr "还没有运行记录"
@ -3957,6 +4020,7 @@ msgid "Notifications"
msgstr "通知" msgstr "通知"
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Now" msgid "Now"
msgstr "现在" msgstr "现在"
@ -4027,7 +4091,6 @@ msgid "Oo name"
msgstr "" msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
msgstr "打开" msgstr "打开"
@ -4056,7 +4119,7 @@ msgstr ""
msgid "Open in browser" msgid "Open in browser"
msgstr "" msgstr ""
#: src/pages/shell/computer-panel-preview.tsx #: src/components/browser/OoComputerBrowser.tsx
msgid "Open in full window" msgid "Open in full window"
msgstr "在完整窗口中打开" msgstr "在完整窗口中打开"
@ -4288,10 +4351,6 @@ msgstr ""
msgid "Project" msgid "Project"
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Project · sessions & folders"
msgstr ""
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
#: src/pages/shell/project-look-dialog.tsx #: src/pages/shell/project-look-dialog.tsx
msgid "Project folder" msgid "Project folder"
@ -4399,6 +4458,10 @@ msgstr ""
msgid "Ready" msgid "Ready"
msgstr "" msgstr ""
#: src/pages/shell/project-overview.tsx
msgid "Ready"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Reasoning effort" msgid "Reasoning effort"
msgstr "推理强度" msgstr "推理强度"
@ -4412,6 +4475,7 @@ msgid "Recall only"
msgstr "" msgstr ""
#: src/pages/ActivityList.tsx #: src/pages/ActivityList.tsx
#: src/pages/shell/project-overview.tsx
msgid "Recent" msgid "Recent"
msgstr "最近" msgstr "最近"
@ -4528,6 +4592,10 @@ msgstr "移除 {0}"
msgid "Remove folder" msgid "Remove folder"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Remove from queue"
msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
msgid "Remove Git event" msgid "Remove Git event"
msgstr "" msgstr ""
@ -4653,6 +4721,11 @@ msgstr "还原上次保存的工作区。电脑上未保存的工作会丢失。
msgid "Restoring your workspace" msgid "Restoring your workspace"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
#: src/pages/shell/message-cards.tsx
msgid "Resume"
msgstr ""
#: src/components/FolderPickerDialog.tsx #: src/components/FolderPickerDialog.tsx
msgid "Retry" msgid "Retry"
msgstr "" msgstr ""
@ -4700,11 +4773,13 @@ msgstr ""
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/routine-event-line.tsx #: src/pages/shell/routine-event-line.tsx
msgid "Routine" msgid "Routine"
msgstr "例行任务" msgstr "例行任务"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Routines" msgid "Routines"
msgstr "例行任务" msgstr "例行任务"
@ -4718,6 +4793,7 @@ msgid "Run at"
msgstr "运行时间" msgstr "运行时间"
#: src/pages/RoutineEditor.tsx #: src/pages/RoutineEditor.tsx
#: src/pages/shell/project-overview.tsx
msgid "Run history" msgid "Run history"
msgstr "运行记录" msgstr "运行记录"
@ -4756,10 +4832,6 @@ msgstr "正在运行…"
msgid "Runs when this bot receives a verified message from this provider." msgid "Runs when this bot receives a verified message from this provider."
msgstr "" msgstr ""
#: src/pages/shell/side-panel-header.tsx
msgid "Sandbox"
msgstr ""
#: src/components/AskCard.tsx #: src/components/AskCard.tsx
#: src/components/MediaSettings.tsx #: src/components/MediaSettings.tsx
#: src/components/teach/SkillDraftCard.tsx #: src/components/teach/SkillDraftCard.tsx
@ -4909,6 +4981,10 @@ msgstr "发送回答"
msgid "Send it" msgid "Send it"
msgstr "发送" msgstr "发送"
#: src/pages/shell/transcript.tsx
msgid "Send now"
msgstr "立即发送"
#: src/pages/Auth.tsx #: src/pages/Auth.tsx
msgid "Send reset link" msgid "Send reset link"
msgstr "发送重置链接" msgstr "发送重置链接"
@ -5021,6 +5097,10 @@ msgstr ""
msgid "Show computer" msgid "Show computer"
msgstr "" msgstr ""
#: src/pages/shell/transcript.tsx
msgid "Show details"
msgstr ""
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Show less" msgid "Show less"
msgstr "" msgstr ""
@ -5037,6 +5117,7 @@ msgstr "显示更多"
msgid "Show password" msgid "Show password"
msgstr "显示密码" msgstr "显示密码"
#: src/pages/shell/project-overview.tsx
#: src/pages/shell/side-panel-header.tsx #: src/pages/shell/side-panel-header.tsx
msgid "Show settings" msgid "Show settings"
msgstr "显示设置" msgstr "显示设置"
@ -5172,6 +5253,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5286,6 +5368,10 @@ msgstr "停止示范"
msgid "Stop timer" msgid "Stop timer"
msgstr "" msgstr ""
#: src/pages/shell/composer.tsx
msgid "Stopped"
msgstr ""
#. js-lingui-explicit-id #. js-lingui-explicit-id
#: src/lib/browser-notifications.ts #: src/lib/browser-notifications.ts
msgid "Stopped." msgid "Stopped."
@ -5398,6 +5484,7 @@ msgstr "正在示范,发送新消息前请先停止示范。"
#: src/pages/ProjectSessionTimeline.tsx #: src/pages/ProjectSessionTimeline.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
#: src/pages/shell/project-overview.tsx
msgid "Team" msgid "Team"
msgstr "团队" msgstr "团队"
@ -5512,10 +5599,6 @@ msgstr "此订阅登录在 Rakazo 中尚不可用。请使用部署凭据或选
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5701,6 +5784,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "已是最新" msgstr "已是最新"
@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead."
msgstr "" msgstr ""
#: src/components/stage/OoStage.tsx #: src/components/stage/OoStage.tsx
#: src/pages/shell/project-overview.tsx
msgid "Working" msgid "Working"
msgstr "" msgstr ""
@ -6037,6 +6122,7 @@ msgstr "昨天"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
#: src/components/voice/VoiceStage.tsx #: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
#: src/pages/shell/transcript.tsx
msgid "You" msgid "You"
msgstr "你" msgstr "你"
@ -6077,13 +6163,25 @@ msgid "Your name"
msgstr "你的名字" msgstr "你的名字"
#: src/pages/VoiceSettingsOverlay.tsx #: 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 "" msgstr ""
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom in"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Zoom out"
msgstr ""

View File

@ -188,6 +188,11 @@ export function UpdatesSettingsPanel({
<div className="space-y-5"> <div className="space-y-5">
<DesktopUpdateSection /> <DesktopUpdateSection />
<SoftwareUpdateSection isDeploymentOwner={isDeploymentOwner} /> <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> </div>
); );
} }

View File

@ -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 ( return (
<div className="mb-2 border-b border-border pb-2"> <div className="mb-2 border-b border-border pb-2">

View File

@ -1,6 +1,11 @@
import { useLingui } from "@lingui/react/macro"; import { useLingui } from "@lingui/react/macro";
import type { ThreadMessage, ThreadSnapshot } from "@rakazo/contracts"; 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 { useEffect, useMemo, useRef, useState } from "react";
import type { VoicePhase, VoiceTranscriptLine } from "../components/voice/VoiceStage"; import type { VoicePhase, VoiceTranscriptLine } from "../components/voice/VoiceStage";
import { VoiceStage } from "../components/voice/VoiceStage"; import { VoiceStage } from "../components/voice/VoiceStage";
@ -417,7 +422,7 @@ export function CallView({
(block) => block.kind === "ask" && block.status !== "answered", (block) => block.kind === "ask" && block.status !== "answered",
); );
const secretAsk = ask && isSecretAskBlock(ask); const secretAsk = ask && isSecretAskBlock(ask);
const rawText = speechFromBlocks(lastBot.blocks); const rawText = spokenSummaryFromBlocks(lastBot.blocks);
const text = rawText const text = rawText
? secretAsk ? secretAsk
? `${rawText}. ${secretPromptRef.current}` ? `${rawText}. ${secretPromptRef.current}`
@ -477,7 +482,7 @@ export function CallView({
hasUnspokenBotMessage: Boolean( hasUnspokenBotMessage: Boolean(
lastBot && lastBot &&
lastBot.id !== spokenMessage.current && lastBot.id !== spokenMessage.current &&
speechFromBlocks(lastBot.blocks).trim(), spokenSummaryFromBlocks(lastBot.blocks).trim(),
), ),
runActive: isActiveRunStatus(snapshot?.run?.status), runActive: isActiveRunStatus(snapshot?.run?.status),
userTurnPending: userTurnPendingRef.current, userTurnPending: userTurnPendingRef.current,

View File

@ -7,6 +7,9 @@ import type {
Group, Group,
HabitToday, HabitToday,
RunActivityRow, RunActivityRow,
Space,
SpaceBot,
SpaceGroup,
} from "@rakazo/contracts"; } from "@rakazo/contracts";
import { runChatTitle } from "@rakazo/core"; import { runChatTitle } from "@rakazo/core";
import { botColors } from "@rakazo/ui-tokens"; import { botColors } from "@rakazo/ui-tokens";
@ -30,6 +33,7 @@ import { rpc } from "../lib/rpc";
type HeuteBriefProps = { type HeuteBriefProps = {
bots: Bot[]; bots: Bot[];
groups: Group[]; groups: Group[];
spaces?: Space[];
onOpenBot: (botId: string, spaceId: string) => void; onOpenBot: (botId: string, spaceId: string) => void;
onOpenGroup: (groupId: string, spaceId: string) => void; onOpenGroup: (groupId: string, spaceId: string) => void;
onOpenRun: (run: RunActivityRow) => void; onOpenRun: (run: RunActivityRow) => void;
@ -41,6 +45,7 @@ const DATE_LABEL: Intl.DateTimeFormatOptions = { weekday: "long", day: "numeric"
export function HeuteBrief({ export function HeuteBrief({
bots, bots,
groups, groups,
spaces = [],
onOpenBot, onOpenBot,
onOpenGroup, onOpenGroup,
onOpenRun, onOpenRun,
@ -135,13 +140,21 @@ export function HeuteBrief({
buildHeuteBrief({ buildHeuteBrief({
bots, bots,
groups, groups,
spaces,
activeRuns, activeRuns,
recentRuns, 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 locale = i18n.locale || "de";
const dateLabel = coach const dateLabel = coach
@ -391,13 +404,13 @@ function NeedsYouRow({
> >
{chatKind === "bot" ? ( {chatKind === "bot" ? (
<BotAvatar <BotAvatar
color={(chat as Bot).color} color={(chat as SpaceBot).color}
identity={chat.id} identity={chat.id}
size={40} 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="min-w-0 flex-1">
<div className="truncate text-ui-body font-semibold text-foreground">{title}</div> <div className="truncate text-ui-body font-semibold text-foreground">{title}</div>

View File

@ -10,7 +10,6 @@ import {
abortableDelay, abortableDelay,
buildFeaturedConnectorTiles, buildFeaturedConnectorTiles,
CONNECTION_CATALOG_PAGE_SIZE, CONNECTION_CATALOG_PAGE_SIZE,
EMPTY_PLUGIN_CATALOG_MESSAGE,
filterConnectionCatalogItems, filterConnectionCatalogItems,
humanizeToolName, humanizeToolName,
isGithubInstall, isGithubInstall,
@ -730,7 +729,7 @@ export function PluginsOverlay({
<div className="mb-6" data-testid="featured-connectors"> <div className="mb-6" data-testid="featured-connectors">
{!loading && catalog.length === 0 ? ( {!loading && catalog.length === 0 ? (
<p className="text-ui-body leading-6 text-muted-foreground/80"> <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> </p>
) : ( ) : (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">

View File

@ -176,7 +176,7 @@ export function SettingsOverlay({
<nav <nav
data-testid="settings-nav" data-testid="settings-nav"
aria-label={t`Settings`} 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) => { {navItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { Trans, useLingui } from "@lingui/react/macro"; import { Trans, useLingui } from "@lingui/react/macro";
import type { VoiceCredential, VoiceInfo, VoiceStatus } from "@rakazo/contracts"; import type { VoiceCredential, VoiceInfo, VoiceStatus } from "@rakazo/contracts";
import { voiceIdForLocale, voicesForLocale } from "@rakazo/core"; import { GERMAN_VOICE_PRESETS, voiceIdForLocale, voicesForLocale } from "@rakazo/core";
import { import {
Button, Button,
cn, cn,
@ -46,7 +46,7 @@ export function VoiceSettingsOverlay({
const [voiceId, setVoiceId] = useState(""); const [voiceId, setVoiceId] = useState("");
const [autoSpeak, setAutoSpeak] = useState(false); const [autoSpeak, setAutoSpeak] = useState(false);
const [loading, setLoading] = useState(true); 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 [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null); const [notice, setNotice] = useState<string | null>(null);
const [soundEnabled, setSoundEnabled] = useState(() => readOoSoundEnabled()); 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); setPending(next);
onBusyChange?.(next !== null); onBusyChange?.(next !== null);
} }
@ -113,17 +113,22 @@ export function VoiceSettingsOverlay({
}, [activeBotId]); }, [activeBotId]);
const voiceOptions = useMemo(() => { 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 (listed.length) return listed;
if (isGermanLocale) return GERMAN_VOICE_PRESETS;
return voiceId ? [{ id: voiceId, label: voiceId }] : []; return voiceId ? [{ id: voiceId, label: voiceId }] : [];
}, [voices, voiceId, uiLocale]); }, [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> { async function ensureLocalVoice(): Promise<boolean> {
if (localConnected) return true; if (localConnected) return true;
markPending("connect");
setError(null);
try { try {
await rpc.voice.connect({ await rpc.voice.connect({
provider: LOCAL_PROVIDER_ID, provider: LOCAL_PROVIDER_ID,
@ -133,16 +138,22 @@ export function VoiceSettingsOverlay({
await refresh(); await refresh();
return true; return true;
} catch (err) { } catch (err) {
if (typeof window !== "undefined" && "speechSynthesis" in window) {
return true;
}
setError(err instanceof Error ? err.message : t`Could not enable speaking`); setError(err instanceof Error ? err.message : t`Could not enable speaking`);
return false; return false;
} finally {
markPending(null);
} }
} }
async function chooseVoice(nextVoiceId: string) { async function chooseVoice(nextVoiceId: string) {
setVoiceId(nextVoiceId); setVoiceId(nextVoiceId);
if (!localConnected) return; if (!localConnected) {
if (activeBotId) {
await rpc.bots.update({ botId: activeBotId, voiceId: nextVoiceId }).catch(() => undefined);
}
return;
}
markPending("voice"); markPending("voice");
setError(null); setError(null);
try { try {
@ -161,6 +172,7 @@ export function VoiceSettingsOverlay({
setError(null); setError(null);
setNotice(null); setNotice(null);
try { try {
let chosenVoice = voiceId;
if (next) { if (next) {
const connected = await ensureLocalVoice(); const connected = await ensureLocalVoice();
if (!connected) { if (!connected) {
@ -168,16 +180,29 @@ export function VoiceSettingsOverlay({
return; return;
} }
const listed = await rpc.voice.voices({ provider: LOCAL_PROVIDER_ID }).catch(() => []); 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) { if (nextVoice) {
await rpc.voice.setVoice({ voiceId: nextVoice, provider: LOCAL_PROVIDER_ID }); chosenVoice = nextVoice;
setVoiceId(nextVoice); setVoiceId(nextVoice);
await rpc.voice
.setVoice({ voiceId: nextVoice, provider: LOCAL_PROVIDER_ID })
.catch(() => undefined);
} }
} }
if (activeBotId) { if (activeBotId) {
await rpc.bots.update({ botId: activeBotId, autoSpeak: next }); await rpc.bots.update({
botId: activeBotId,
autoSpeak: next,
...(chosenVoice ? { voiceId: chosenVoice } : {}),
});
} }
await refresh(); await refresh();
if (!voiceId && chosenVoice) setVoiceId(chosenVoice);
setNotice(next ? t`Speaking is on.` : t`Speaking is off.`); setNotice(next ? t`Speaking is on.` : t`Speaking is off.`);
} catch (err) { } catch (err) {
setAutoSpeak(!next); setAutoSpeak(!next);
@ -193,7 +218,11 @@ export function VoiceSettingsOverlay({
return; return;
} }
setError(null); 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, voiceId: voice.id,
messageId: `${PREVIEW_PREFIX}${voice.id}`, messageId: `${PREVIEW_PREFIX}${voice.id}`,
}); });
@ -234,7 +263,7 @@ export function VoiceSettingsOverlay({
{autoSpeak ? <Trans>Can speak</Trans> : <Trans>Does not speak</Trans>} {autoSpeak ? <Trans>Can speak</Trans> : <Trans>Does not speak</Trans>}
</p> </p>
<p className="mt-1 text-ui-body-sm text-muted-foreground"> <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> </p>
</div> </div>
<Switch <Switch

View File

@ -20,6 +20,7 @@ import {
ModelTierSchema, ModelTierSchema,
normalizeBotTags, normalizeBotTags,
} from "@rakazo/contracts"; } from "@rakazo/contracts";
import { GERMAN_VOICE_PRESETS, germanVoiceForBot, voicesForLocale } from "@rakazo/core";
import { import {
Button, Button,
Input, Input,
@ -37,6 +38,7 @@ import { connectedModelOptions } from "../../lib/connected-models";
import { lazyWithRecovery } from "../../lib/lazy-recovery"; import { lazyWithRecovery } from "../../lib/lazy-recovery";
import { modelTierLabel } from "../../lib/model-tier-label"; import { modelTierLabel } from "../../lib/model-tier-label";
import { rpc } from "../../lib/rpc"; import { rpc } from "../../lib/rpc";
import { readUiLocale } from "../../lib/ui-locale";
import { BotCapabilitiesFields } from "./bot-capabilities"; import { BotCapabilitiesFields } from "./bot-capabilities";
const ScratchpadSection = lazyWithRecovery("ScratchpadSection", () => const ScratchpadSection = lazyWithRecovery("ScratchpadSection", () =>
@ -280,8 +282,8 @@ export function BotSettings({
const [computerMode, setComputerMode] = useState(bot.computerMode); const [computerMode, setComputerMode] = useState(bot.computerMode);
const [memoryScope, setMemoryScope] = useState(bot.memoryScope); const [memoryScope, setMemoryScope] = useState(bot.memoryScope);
const [autoSpeak, setAutoSpeak] = useState(bot.autoSpeak); const [autoSpeak, setAutoSpeak] = useState(bot.autoSpeak);
const [voiceId, setVoiceId] = useState(bot.voiceId ?? ""); const [voiceId, setVoiceId] = useState(bot.voiceId || germanVoiceForBot(bot.id, bot.voiceId));
const [voices, setVoices] = useState<VoiceInfo[]>([]); const [voices, setVoices] = useState<VoiceInfo[]>(GERMAN_VOICE_PRESETS);
const [modelKey, setModelKey] = useState( const [modelKey, setModelKey] = useState(
bot.modelTier bot.modelTier
? tierOptionKey(bot.modelTier) ? tierOptionKey(bot.modelTier)
@ -314,8 +316,15 @@ export function BotSettings({
useEffect(() => { useEffect(() => {
void rpc.voice void rpc.voice
.voices({}) .voices({})
.then(setVoices) .then((listed) => {
.catch(() => setVoices([])); 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 void rpc.bots
.builtinTools({ botId: bot.id }) .builtinTools({ botId: bot.id })
.then(setToolNames) .then(setToolNames)
@ -675,7 +684,7 @@ export function BotSettings({
<NativeSelectOption value="">{t`Account default`}</NativeSelectOption> <NativeSelectOption value="">{t`Account default`}</NativeSelectOption>
{voices.map((voice) => ( {voices.map((voice) => (
<NativeSelectOption key={voice.id} value={voice.id}> <NativeSelectOption key={voice.id} value={voice.id}>
{voice.label} {voice.description ? `${voice.label} (${voice.description})` : voice.label}
</NativeSelectOption> </NativeSelectOption>
))} ))}
</NativeSelect> </NativeSelect>

View 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();
});
});

View File

@ -5,11 +5,14 @@ import { ATTACHMENT_ALLOWED_MIME_TYPES } from "@rakazo/contracts";
import type { ComposerMention, SlashActionId } from "@rakazo/core"; import type { ComposerMention, SlashActionId } from "@rakazo/core";
import { import {
clampMentionHighlightIndex, clampMentionHighlightIndex,
clearComposerDraft,
mentionChipKey, mentionChipKey,
readComposerDraft,
resolveMentionPickerKey, resolveMentionPickerKey,
SLASH_ACTIONS, SLASH_ACTIONS,
serializeComposerPrompt, serializeComposerPrompt,
truncateSlashDescription, truncateSlashDescription,
writeComposerDraft,
} from "@rakazo/core"; } from "@rakazo/core";
import { import {
BotAvatar, BotAvatar,
@ -28,6 +31,7 @@ import {
Play, Play,
Plus, Plus,
Puzzle, Puzzle,
RotateCcw,
Settings, Settings,
Square, Square,
X, X,
@ -37,6 +41,8 @@ import { memo, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } fr
import { VoiceAuroraRim } from "../../components/voice/VoiceAurora"; import { VoiceAuroraRim } from "../../components/voice/VoiceAurora";
import { isFileDrag, isFilePaste } from "../../lib/pending-attachments"; import { isFileDrag, isFilePaste } from "../../lib/pending-attachments";
import { AutopilotDialog } from "./autopilot-dialog"; import { AutopilotDialog } from "./autopilot-dialog";
import type { ComposerActivePlan } from "./message-cards";
import { ComposerTaskPill } from "./message-cards";
import { previewMessageText } from "./message-preview"; import { previewMessageText } from "./message-preview";
import type { PendingAttachment } from "./shell-types"; import type { PendingAttachment } from "./shell-types";
import { FALLBACK_BOT_COLOR } 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"; "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({ export const Composer = memo(function Composer({
draftKey,
running, running,
disabled, disabled,
pendingAttachments, pendingAttachments,
@ -76,7 +83,12 @@ export const Composer = memo(function Composer({
onSlashAction, onSlashAction,
onConnectModel, onConnectModel,
onAutopilot, onAutopilot,
activePlan,
stopped = false,
onResume,
onDismissStopped,
}: { }: {
draftKey?: string;
running: boolean; running: boolean;
disabled?: boolean; disabled?: boolean;
pendingAttachments: PendingAttachment[]; pendingAttachments: PendingAttachment[];
@ -107,15 +119,43 @@ export const Composer = memo(function Composer({
onConnectModel?: () => void; onConnectModel?: () => void;
/** Hand the order to the autopilot; the composer menu offers it only when set. */ /** Hand the order to the autopilot; the composer menu offers it only when set. */
onAutopilot?: (goal: string, until?: string) => Promise<void>; onAutopilot?: (goal: string, until?: string) => Promise<void>;
activePlan?: ComposerActivePlan | null;
stopped?: boolean;
onResume?: () => void | Promise<void>;
onDismissStopped?: () => void;
}) { }) {
const { t } = useLingui(); 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 [autopilotOpen, setAutopilotOpen] = useState(false);
const [mentionQuery, setMentionQuery] = useState<string | null>(null); const [mentionQuery, setMentionQuery] = useState<string | null>(null);
const [mentionHighlightIndex, setMentionHighlightIndex] = useState(0); const [mentionHighlightIndex, setMentionHighlightIndex] = useState(0);
const [slashQuery, setSlashQuery] = useState<string | null>(null); const [slashQuery, setSlashQuery] = useState<string | null>(null);
const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(null); const [selectedSkill, setSelectedSkill] = useState<AgentSkillCatalogEntry | null>(
const [selectedMentions, setSelectedMentions] = useState<ComposerMention[]>([]); () => 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 textareaRef = useRef<HTMLTextAreaElement>(null);
const runErrorRef = useRef<HTMLDivElement>(null); const runErrorRef = useRef<HTMLDivElement>(null);
const submitFingerprint = useRef<string | null>(null); const submitFingerprint = useRef<string | null>(null);
@ -265,6 +305,11 @@ export const Composer = memo(function Composer({
function runSlashAction(action: SlashActionId) { function runSlashAction(action: SlashActionId) {
setDraft(""); setDraft("");
setSlashQuery(null); setSlashQuery(null);
writeComposerDraft(draftKey, {
text: "",
selectedSkill,
selectedMentions,
});
onSlashAction?.(action); onSlashAction?.(action);
} }
@ -300,7 +345,10 @@ export const Composer = memo(function Composer({
const slashSkillOptions = useMemo(() => { const slashSkillOptions = useMemo(() => {
if (slashQuery === null) return []; if (slashQuery === null) return [];
const query = slashQuery.trim().toLowerCase(); 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 return skills
.filter((skill) => { .filter((skill) => {
if (!query) return true; if (!query) return true;
@ -331,6 +379,7 @@ export const Composer = memo(function Composer({
); );
if (submitFingerprint.current === fingerprint) return; if (submitFingerprint.current === fingerprint) return;
submitFingerprint.current = fingerprint; submitFingerprint.current = fingerprint;
clearComposerDraft(draftKey);
setDraft(""); setDraft("");
setMentionQuery(null); setMentionQuery(null);
setMentionHighlightIndex(0); setMentionHighlightIndex(0);
@ -417,11 +466,11 @@ export const Composer = memo(function Composer({
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} 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" : "" 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 ? ( {onConnectModel ? (
<div <div
data-testid="composer-model-needed" 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" 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> <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 <button
type="button" type="button"
aria-label={t`Dismiss error`} aria-label={t`Dismiss error`}
@ -455,6 +516,39 @@ export const Composer = memo(function Composer({
<X strokeWidth={2} className="size-icon-sm" /> <X strokeWidth={2} className="size-icon-sm" />
</button> </button>
</div> </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} ) : null}
{replyTarget ? ( {replyTarget ? (
<div <div
@ -564,7 +658,7 @@ export const Composer = memo(function Composer({
{showSlashPicker ? ( {showSlashPicker ? (
<div <div
data-testid="slash-picker" 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) => ( {slashSkillOptions.map((skill) => (
<button <button
@ -608,6 +702,7 @@ export const Composer = memo(function Composer({
})} })}
</div> </div>
) : null} ) : null}
{activePlan ? <ComposerTaskPill activePlan={activePlan} onResume={onResume} /> : null}
<div <div
data-testid="composer-bar" 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" 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) => { onStart={async (goal, until) => {
await onAutopilot(goal, until); await onAutopilot(goal, until);
setDraft(""); setDraft("");
writeComposerDraft(draftKey, {
text: "",
selectedSkill,
selectedMentions,
});
}} }}
/> />
) : null} ) : null}

View File

@ -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} />
</>
);
}

View File

@ -108,14 +108,20 @@ export function NewSpaceDialog({
<Dialog open onOpenChange={closeUnlessBusy(saving, onCancel)}> <Dialog open onOpenChange={closeUnlessBusy(saving, onCancel)}>
<DialogContent <DialogContent
showCloseButton={false} 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> <DialogHeader
<DialogTitle className="flex items-center gap-2.5"> 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 ? ( {isProject ? (
<> <>
<ProjectIcon icon={icon} color={color} size={30} /> <ProjectIcon icon={icon} color={color} size={30} />
<span> <span className="truncate">
<Trans>New project</Trans> <Trans>New project</Trans>
</span> </span>
</> </>
@ -133,7 +139,13 @@ export function NewSpaceDialog({
</DialogTitle> </DialogTitle>
</DialogHeader> </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"> <label htmlFor={nameId} className="block text-ui-body text-foreground/75">
<Trans>Name</Trans> <Trans>Name</Trans>
<Input <Input
@ -179,7 +191,7 @@ export function NewSpaceDialog({
<div <div
role="radiogroup" role="radiogroup"
aria-label={t`Icon`} 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) => { {PROJECT_ICONS.map((option) => {
const Glyph = PROJECT_ICON_COMPONENTS[option.id]; const Glyph = PROJECT_ICON_COMPONENTS[option.id];
@ -304,8 +316,12 @@ export function NewSpaceDialog({
) : null} ) : null}
</div> </div>
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null} {error ? (
<DialogFooter className="mt-4"> <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}> <Button variant="outline" disabled={saving} onClick={onCancel}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>

View File

@ -84,11 +84,11 @@ export function FullscreenComputerOverlay({
)} )}
{!recordingSkill && hasControl ? ( {!recordingSkill && hasControl ? (
computer?.takeoverRequested ? ( 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> <Trans>Needs you</Trans>
</span> </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> <Trans>You have control</Trans>
</span> </span>
) )

File diff suppressed because it is too large Load Diff

View File

@ -95,16 +95,16 @@ export function ProjectLookDialog({
<DialogContent <DialogContent
showCloseButton={false} showCloseButton={false}
data-testid="project-look-dialog" 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> <DialogHeader className="shrink-0 border-b border-border px-5 py-4">
<DialogTitle className="flex items-center gap-3"> <DialogTitle className="flex min-w-0 items-center gap-3">
<ProjectIcon icon={icon} color={color} size={36} /> <ProjectIcon icon={icon} color={color} size={36} />
<span className="min-w-0 truncate">{name.trim() || initial.name}</span> <span className="min-w-0 truncate">{name.trim() || initial.name}</span>
</DialogTitle> </DialogTitle>
</DialogHeader> </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"> <label htmlFor={nameId} className="block text-ui-body text-foreground/75">
<Trans>Name</Trans> <Trans>Name</Trans>
<Input <Input
@ -147,7 +147,7 @@ export function ProjectLookDialog({
<div <div
role="radiogroup" role="radiogroup"
aria-label={t`Icon`} 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) => { {PROJECT_ICONS.map((option) => {
const Glyph = PROJECT_ICON_COMPONENTS[option.id]; const Glyph = PROJECT_ICON_COMPONENTS[option.id];
@ -243,8 +243,8 @@ export function ProjectLookDialog({
</label> </label>
</div> </div>
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null} {error ? <p className="px-5 pt-2 text-ui-body text-destructive">{error}</p> : null}
<DialogFooter className="mt-4"> <DialogFooter className="mx-0 mb-0 mt-0 shrink-0 px-5 py-3.5">
<Button variant="outline" disabled={saving} onClick={onCancel}> <Button variant="outline" disabled={saving} onClick={onCancel}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>

Some files were not shown because too many files have changed in this diff Show More