diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b689f6..12c2572 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -477,6 +477,9 @@ jobs: eas-version: latest packager: pnpm token: ${{ secrets.EXPO_TOKEN }} + # Its default caches go to the GitHub cache service; keep them on the runner. + eas-cache: false + expo-cache: false - name: Install dependencies if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true' run: pnpm install --frozen-lockfile diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index fa61f56..5611d49 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -29,6 +29,7 @@ import { createMediaRuntime, createMessagingContextLoader, createMessagingTeamChatSender, + createPostgresReconciliationLeadership, createProfileConsolidationScheduler, createRunExecutor, createRunSandbox, @@ -360,7 +361,7 @@ export async function createApp( "https://172.*:*", "http://*.local:*", "https://*.local:*", - ...(process.env.EXTRA_TRUSTED_ORIGINS?.split(",").map((s) => s.trim()) ?? []), + ...(env.extraTrustedOrigins ?? []), ], beforeDeleteUser: async (userId) => { const bots = await prisma.bot.findMany({ @@ -474,26 +475,30 @@ export async function createApp( cloudAgent, media, }); + let graphileWorkerHost: GraphileJobWorkerHost | undefined; if (inMemoryJobs) { await inMemoryJobs.start(jobHandlers); } else if (jobPool) { - const graphileWorkerHost = new GraphileJobWorkerHost(jobPool, { - concurrency: 4, + graphileWorkerHost = new GraphileJobWorkerHost(jobPool, { + concurrency: parsePositiveInteger(process.env.GRAPHILE_WORKER_CONCURRENCY, 4), pollInterval: 500, }); await graphileWorkerHost.start(jobHandlers); } - const reconciler = inMemoryJobs - ? createJobReconciler({ - prisma, - jobs, - reconcileCloudAgents: () => reconcileCloudAgents({ prisma, jobs, cloudAgent }), - reconcileComputerUpdates: () => reconcileComputerUpdates({ prisma, jobs }), - reconcileProfileConsolidation: createProfileConsolidationScheduler({ prisma, jobs }), - reconcileDelegatedTasks: () => reconcileDelegatedTasks({ prisma, jobs, events }), - reconcileMediaJobs: () => media.reconcile(), - }) - : undefined; + const reconciler = + inMemoryJobs || !prismaOverride + ? createJobReconciler({ + prisma, + jobs, + events, + leadership: jobPool ? createPostgresReconciliationLeadership(jobPool) : undefined, + reconcileCloudAgents: () => reconcileCloudAgents({ prisma, jobs, cloudAgent }), + reconcileComputerUpdates: () => reconcileComputerUpdates({ prisma, jobs }), + reconcileProfileConsolidation: createProfileConsolidationScheduler({ prisma, jobs }), + reconcileDelegatedTasks: () => reconcileDelegatedTasks({ prisma, jobs, events }), + reconcileMediaJobs: () => media.reconcile(), + }) + : undefined; reconciler?.start(); const router = createRouter({ @@ -612,7 +617,7 @@ export async function createApp( actor, signal: c.req.raw.signal, userAgent: c.req.header("user-agent"), - webOrigin: requestWebOrigin(c.req.header("origin"), env.webOrigin), + webOrigin: requestWebOrigin(c.req.header("origin"), env.webOrigin, env.extraTrustedOrigins), }, }); if (matched) return c.newResponse(response.body, response); @@ -1006,6 +1011,7 @@ export async function createApp( await teamChatBridge?.stop(); await email?.drain?.(); await reconciler?.stop(); + await graphileWorkerHost?.stop(); await jobs.close(); await realtime.close(); await connector.stop(); @@ -1021,6 +1027,7 @@ export async function createApp( function isTrustedOrigin(origin: string, env: AppEnv) { if (!origin) return true; if (origin === env.webOrigin || origin === env.apiUrl || origin === env.authUrl) return true; + if (env.extraTrustedOrigins?.includes(origin)) return true; if (origin.startsWith("rakazo://") || origin.startsWith("exp://")) return true; try { const host = new URL(origin).hostname; diff --git a/apps/api/src/env.ts b/apps/api/src/env.ts index b260c21..92284a7 100644 --- a/apps/api/src/env.ts +++ b/apps/api/src/env.ts @@ -20,6 +20,7 @@ export interface AppEnv { authSecret: string; authUrl: string; webOrigin: string; + extraTrustedOrigins: string[]; privacyPolicyUrl?: string; apiUrl: string; apiHost: string; @@ -109,6 +110,10 @@ export function loadEnv(source: NodeJS.ProcessEnv = process.env): AppEnv { authSecret, authUrl: source.BETTER_AUTH_URL ?? source.WEB_ORIGIN ?? "http://127.0.0.1:5173", webOrigin: source.WEB_ORIGIN ?? "http://127.0.0.1:5173", + extraTrustedOrigins: (source.EXTRA_TRUSTED_ORIGINS ?? "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean), privacyPolicyUrl: optional(source.PRIVACY_POLICY_URL), apiUrl: source.API_URL ?? "http://127.0.0.1:3100", apiHost: source.API_HOST ?? "127.0.0.1", diff --git a/apps/api/src/router.ts b/apps/api/src/router.ts index bf00631..12de279 100644 --- a/apps/api/src/router.ts +++ b/apps/api/src/router.ts @@ -135,6 +135,7 @@ import { createProjectGoalRepos, createRepos, createSpaceForMember, + DuplicateSpaceNameError, deleteEmptySpaceForMember, deleteUnreferencedCredentialSecret, ensureMainConversation, @@ -270,7 +271,10 @@ import { followUpGroupThreadMessage, groupDefaultBotId, reactToThreadMessage, + removeQueuedMessage, resolveThreadTarget, + resumeThreadRuns, + sendQueuedThreadMessagesNow, sendThreadMessage, setThreadUnreadState, stopThreadRuns, @@ -783,15 +787,26 @@ export function createRouter(deps: RouterDeps) { const goal = input.goal?.trim() || (workspacePath ? await readProjectSummary(workspacePath) : null); if (goal) { - await deps.prisma.memoryDocument.create({ - data: { - spaceId: space.id, - userId: context.actor.userId, - scope: "user", - path: "PROJECT_GOAL.md", - content: goal, - }, + const existingGoal = await deps.prisma.memoryDocument.findFirst({ + where: { spaceId: space.id, scope: "user", path: "PROJECT_GOAL.md" }, + select: { id: true }, }); + if (existingGoal) { + await deps.prisma.memoryDocument.update({ + where: { id: existingGoal.id }, + data: { content: goal }, + }); + } else { + await deps.prisma.memoryDocument.create({ + data: { + spaceId: space.id, + userId: context.actor.userId, + scope: "user", + path: "PROJECT_GOAL.md", + content: goal, + }, + }); + } } // A project starts as a conversation with its own main agent. await ensureMainConversation(deps.prisma, context.actor.userId, space.id); @@ -873,7 +888,7 @@ export function createRouter(deps: RouterDeps) { throw new ORPCError("NOT_FOUND", { message: error.message }); } if (error instanceof IsolationError) throw new ORPCError("FORBIDDEN"); - if (error instanceof InvalidSpaceNameError) { + if (error instanceof InvalidSpaceNameError || error instanceof DuplicateSpaceNameError) { throw new ORPCError("BAD_REQUEST", { message: error.message }); } throw error; @@ -2050,6 +2065,42 @@ export function createRouter(deps: RouterDeps) { await stopThreadRuns(deps, context.actor, target); return { ok: true as const }; }), + resume: authed.threads.resume.handler(async ({ context, input }) => { + if ((await modelSetup(deps, context.actor)).needsModel) { + throw new ORPCError("BAD_REQUEST", { message: "Connect a model to start a run." }); + } + const target = await resolveThreadTarget(deps.prisma, context.actor, input); + if (target.kind === "bot") { + await assertTeachingSendAllowed(deps.prisma, context.actor.spaceId, target.botId); + } + return resumeThreadRuns(deps, context.actor, target); + }), + sendNow: authed.threads.sendNow.handler(async ({ context, input }) => { + if ((await modelSetup(deps, context.actor)).needsModel) { + throw new ORPCError("BAD_REQUEST", { message: "Connect a model to start a run." }); + } + const target = await resolveThreadTarget(deps.prisma, context.actor, input); + if (target.kind === "bot") { + await assertTeachingSendAllowed(deps.prisma, context.actor.spaceId, target.botId); + } + return sendQueuedThreadMessagesNow(deps, context.actor, target, { + messageId: input.messageId, + }); + }), + removeQueuedMessage: authed.threads.removeQueuedMessage.handler( + async ({ context, input }) => { + const target = await resolveThreadTarget(deps.prisma, context.actor, input); + if (target.kind === "bot") { + await assertTeachingSendAllowed(deps.prisma, context.actor.spaceId, target.botId); + } + return removeQueuedMessage( + { prisma: deps.prisma, events: deps.events }, + context.actor, + target, + input.messageId, + ); + }, + ), clear: authed.threads.clear.handler(async ({ context, input }) => { const target = await resolveThreadTarget(deps.prisma, context.actor, input); const contextBotId = target.kind === "bot" ? target.botId : groupDefaultBotId(target); @@ -5623,13 +5674,13 @@ async function spaceNavigationDto( // The user's own main agent of a project is part of the project, not content to clear. deps.prisma.bot.findMany({ where: { spaceId: { in: spaceIds }, NOT: ownProjectMain }, - select: { spaceId: true }, - distinct: ["spaceId"], + select: { spaceId: true, userId: true }, + distinct: ["spaceId", "userId"], }), deps.prisma.chatGroup.findMany({ where: { spaceId: { in: spaceIds }, NOT: ownProjectMain }, - select: { spaceId: true }, - distinct: ["spaceId"], + select: { spaceId: true, userId: true }, + distinct: ["spaceId", "userId"], }), deps.prisma.memoryDocument.findMany({ where: { spaceId: { in: spaceIds }, scope: "user", path: "PROJECT_GOAL.md" }, @@ -5640,6 +5691,11 @@ async function spaceNavigationDto( ...contentBots.map((row) => row.spaceId), ...contentGroups.map((row) => row.spaceId), ]); + const spacesBlockedByOtherMember = new Set( + [...contentBots, ...contentGroups] + .filter((row) => row.userId !== actor.userId) + .map((row) => row.spaceId), + ); const goalBySpaceId = new Map(goalDocuments.map((doc) => [doc.spaceId, doc.content])); const currentMembership = memberships.find((membership) => membership.spaceId === actor.spaceId); if (!currentMembership) throw new IsolationError(); @@ -5676,7 +5732,7 @@ async function spaceNavigationDto( membership.role === "owner" && !membership.space.isDefault && memberships.length > 1 && - !spacesWithContent.has(membership.spaceId) && + !spacesBlockedByOtherMember.has(membership.spaceId) && (membership.space.deletingAt === null || membership.space.deletingAt < staleClaimBefore), canEdit: membership.role === "owner", // A linked folder is on the owner's host; other members see neither its path nor remote. diff --git a/apps/api/src/search.ts b/apps/api/src/search.ts index e37b858..7e615c6 100644 --- a/apps/api/src/search.ts +++ b/apps/api/src/search.ts @@ -1,6 +1,7 @@ import type { Actor, MessageBlock, SearchHit } from "@rakazo/contracts"; import { extractLinksFromText, matchesSearchQuery, snippetAroundMatch } from "@rakazo/core"; -import { Prisma, type PrismaClient } from "@rakazo/db"; +import type { PrismaClient } from "@rakazo/db"; +import { Prisma } from "@rakazo/db"; const SEARCH_LIMIT = 25; /** Cap name matches so content hits (messages/files/links/routines) keep most of the budget. */ @@ -64,19 +65,65 @@ export async function querySpaceSearch( bucket.push(hit); } - const bots = await prisma.bot.findMany({ - where: { - spaceId: actor.spaceId, - userId: actor.userId, - archivedAt: null, - OR: [ - { name: { contains: query, mode: "insensitive" } }, - { title: { contains: query, mode: "insensitive" } }, - { description: { contains: query, mode: "insensitive" } }, - ], - }, - take: CONVERSATION_HIT_LIMIT, - }); + const [bots, groups, artifacts, groupArtifacts, routines] = await Promise.all([ + prisma.bot.findMany({ + where: { + spaceId: actor.spaceId, + userId: actor.userId, + archivedAt: null, + OR: [ + { name: { contains: query, mode: "insensitive" } }, + { title: { contains: query, mode: "insensitive" } }, + { description: { contains: query, mode: "insensitive" } }, + ], + }, + take: CONVERSATION_HIT_LIMIT, + }), + prisma.chatGroup.findMany({ + where: { + spaceId: actor.spaceId, + userId: actor.userId, + name: { contains: query, mode: "insensitive" }, + }, + take: CONVERSATION_HIT_LIMIT, + }), + prisma.artifact.findMany({ + where: { + spaceId: actor.spaceId, + userId: actor.userId, + groupId: null, + botId: { not: null }, + name: { contains: query, mode: "insensitive" }, + bot: { archivedAt: null }, + }, + include: { bot: { select: { name: true } } }, + take: SEARCH_LIMIT, + }), + prisma.artifact.findMany({ + where: { + spaceId: actor.spaceId, + userId: actor.userId, + groupId: { not: null }, + name: { contains: query, mode: "insensitive" }, + }, + include: { group: { select: { name: true } } }, + take: SEARCH_LIMIT, + }), + prisma.routine.findMany({ + where: { + spaceId: actor.spaceId, + userId: actor.userId, + OR: [ + { name: { contains: query, mode: "insensitive" } }, + { prompt: { contains: query, mode: "insensitive" } }, + ], + bot: { archivedAt: null }, + }, + include: { bot: { select: { name: true } } }, + take: SEARCH_LIMIT, + }), + ]); + for (const bot of bots) { pushInto( conversationHits, @@ -91,14 +138,6 @@ export async function querySpaceSearch( ); } - const groups = await prisma.chatGroup.findMany({ - where: { - spaceId: actor.spaceId, - userId: actor.userId, - name: { contains: query, mode: "insensitive" }, - }, - take: CONVERSATION_HIT_LIMIT, - }); for (const group of groups) { pushInto( conversationHits, @@ -114,27 +153,72 @@ export async function querySpaceSearch( } const contentBudget = Math.max(0, SEARCH_LIMIT - conversationHits.length); + const pattern = `%${query}%`; + + const [botArtifactMessages, groupArtifactMessages, messageRows, groupMessageRows] = + await Promise.all([ + findArtifactMessages( + prisma, + actor, + artifacts.flatMap((artifact) => + artifact.botId && artifact.bot ? [{ id: artifact.id, targetId: artifact.botId }] : [], + ), + "bot", + ), + findArtifactMessages( + prisma, + actor, + groupArtifacts.flatMap((artifact) => + artifact.groupId && artifact.group + ? [{ id: artifact.id, targetId: artifact.groupId }] + : [], + ), + "group", + ), + prisma.$queryRaw< + Array<{ + id: string; + threadId: string; + seq: number; + blocks: Prisma.JsonValue; + botId: string; + botName: string; + }> + >` + SELECT m.id, m."threadId", m.seq, m.blocks, b.id AS "botId", b.name AS "botName" + FROM messages m + INNER JOIN threads t ON t.id = m."threadId" + INNER JOIN bots b ON b.id = t."botId" + WHERE t."spaceId" = ${actor.spaceId} + AND t."userId" = ${actor.userId} + AND b."archivedAt" IS NULL + AND m.blocks::text ILIKE ${pattern} + ORDER BY m."createdAt" DESC + LIMIT ${SEARCH_LIMIT} + `, + prisma.$queryRaw< + Array<{ + id: string; + threadId: string; + seq: number; + blocks: Prisma.JsonValue; + groupId: string; + groupName: string; + }> + >` + SELECT m.id, m."threadId", m.seq, m.blocks, g.id AS "groupId", g.name AS "groupName" + FROM messages m + INNER JOIN threads t ON t.id = m."threadId" + INNER JOIN chat_groups g ON g.id = t."groupId" + WHERE t."spaceId" = ${actor.spaceId} + AND t."userId" = ${actor.userId} + AND t."groupId" IS NOT NULL + AND m.blocks::text ILIKE ${pattern} + ORDER BY m."createdAt" DESC + LIMIT ${SEARCH_LIMIT} + `, + ]); - const artifacts = await prisma.artifact.findMany({ - where: { - spaceId: actor.spaceId, - userId: actor.userId, - groupId: null, - botId: { not: null }, - name: { contains: query, mode: "insensitive" }, - bot: { archivedAt: null }, - }, - include: { bot: { select: { name: true } } }, - take: SEARCH_LIMIT, - }); - const botArtifactMessages = await findArtifactMessages( - prisma, - actor, - artifacts.flatMap((artifact) => - artifact.botId && artifact.bot ? [{ id: artifact.id, targetId: artifact.botId }] : [], - ), - "bot", - ); for (const artifact of artifacts) { if (!artifact.botId || !artifact.bot) continue; const message = botArtifactMessages.get(artifact.id); @@ -155,24 +239,6 @@ export async function querySpaceSearch( ); } - const groupArtifacts = await prisma.artifact.findMany({ - where: { - spaceId: actor.spaceId, - userId: actor.userId, - groupId: { not: null }, - name: { contains: query, mode: "insensitive" }, - }, - include: { group: { select: { name: true } } }, - take: SEARCH_LIMIT, - }); - const groupArtifactMessages = await findArtifactMessages( - prisma, - actor, - groupArtifacts.flatMap((artifact) => - artifact.groupId && artifact.group ? [{ id: artifact.id, targetId: artifact.groupId }] : [], - ), - "group", - ); for (const artifact of groupArtifacts) { if (!artifact.groupId || !artifact.group) continue; const message = groupArtifactMessages.get(artifact.id); @@ -193,19 +259,6 @@ export async function querySpaceSearch( ); } - const routines = await prisma.routine.findMany({ - where: { - spaceId: actor.spaceId, - userId: actor.userId, - OR: [ - { name: { contains: query, mode: "insensitive" } }, - { prompt: { contains: query, mode: "insensitive" } }, - ], - bot: { archivedAt: null }, - }, - include: { bot: { select: { name: true } } }, - take: SEARCH_LIMIT, - }); for (const routine of routines) { pushInto( contentHits, @@ -221,29 +274,6 @@ export async function querySpaceSearch( ); } - const pattern = `%${query}%`; - const messageRows = await prisma.$queryRaw< - Array<{ - id: string; - threadId: string; - seq: number; - blocks: Prisma.JsonValue; - botId: string; - botName: string; - }> - >` - SELECT m.id, m."threadId", m.seq, m.blocks, b.id AS "botId", b.name AS "botName" - FROM messages m - INNER JOIN threads t ON t.id = m."threadId" - INNER JOIN bots b ON b.id = t."botId" - WHERE t."spaceId" = ${actor.spaceId} - AND t."userId" = ${actor.userId} - AND b."archivedAt" IS NULL - AND m.blocks::text ILIKE ${pattern} - ORDER BY m."createdAt" DESC - LIMIT ${SEARCH_LIMIT} - `; - for (const row of messageRows) { pushMessageHits(row.blocks as MessageBlock[], { botId: row.botId, @@ -255,28 +285,6 @@ export async function querySpaceSearch( }); } - const groupMessageRows = await prisma.$queryRaw< - Array<{ - id: string; - threadId: string; - seq: number; - blocks: Prisma.JsonValue; - groupId: string; - groupName: string; - }> - >` - SELECT m.id, m."threadId", m.seq, m.blocks, g.id AS "groupId", g.name AS "groupName" - FROM messages m - INNER JOIN threads t ON t.id = m."threadId" - INNER JOIN chat_groups g ON g.id = t."groupId" - WHERE t."spaceId" = ${actor.spaceId} - AND t."userId" = ${actor.userId} - AND t."groupId" IS NOT NULL - AND m.blocks::text ILIKE ${pattern} - ORDER BY m."createdAt" DESC - LIMIT ${SEARCH_LIMIT} - `; - for (const row of groupMessageRows) { pushMessageHits(row.blocks as MessageBlock[], { groupId: row.groupId, diff --git a/apps/api/src/team-chat-judge.test.ts b/apps/api/src/team-chat-judge.test.ts index d6ea68b..a7048de 100644 --- a/apps/api/src/team-chat-judge.test.ts +++ b/apps/api/src/team-chat-judge.test.ts @@ -88,4 +88,77 @@ describe("model team chat engagement judge", () => { expect(instructions).toContain("(Europe/Berlin, the user's time zone)"); expect(instructions).toContain("low-cost engagement judge"); }); + + it("prefers the space fast model tier when mapped and no explicit judge override is set", async () => { + let selectedModel: { provider: string; id: string; apiKey?: string } | undefined; + const runtime = { + run: vi.fn((request: { model: { provider: string; id: string; apiKey?: string } }) => { + selectedModel = request.model; + return (async function* () { + yield { type: "done" as const, text: '{"act":true,"reason":"Direct mention."}' }; + })(); + }), + } as unknown as AgentRuntime; + const prisma = { + deploymentSettings: { findUnique: vi.fn(async () => null) }, + spaceModelPreference: { findFirst: vi.fn(async () => null) }, + spaceModelTier: { + findUnique: vi.fn(async () => ({ + tier: "fast", + credentialId: "cred-fast", + modelId: "gemma4:latest", + credential: { + id: "cred-fast", + userId: "user-1", + provider: "openai-compatible", + secretId: "sec-fast", + preferences: [{ isDefault: false, modelId: null }], + }, + })), + }, + secret: { + findFirst: vi.fn(async () => ({ id: "sec-fast", ciphertext: "cipher-fast" })), + }, + userProfile: { findUnique: vi.fn(async () => ({ timeZone: "UTC" })) }, + } as unknown as PrismaClient; + const secrets = { + load: vi.fn(() => + JSON.stringify({ + kind: "openai_compatible", + apiKey: "local-key", + baseUrl: "http://127.0.0.1:11434/v1", + }), + ), + } as unknown as EncryptedSecretStore; + + const judge = new ModelTeamChatEngagementJudge({ + prisma, + runtime, + secrets, + deploymentProvider: "openrouter", + deploymentModel: "expensive-default", + }); + + const decision = await judge.decide({ + bot: { + id: "bot-1", + spaceId: "space-1", + userId: "user-1", + name: "Arthur", + modelProvider: "anthropic", + modelId: "claude-opus", + }, + channelId: "C1", + rules: "", + messages: [], + }); + + expect(decision).toEqual({ act: true, reason: "Direct mention.", askedByEventId: undefined }); + expect(selectedModel).toMatchObject({ + provider: "openai-compatible", + id: "gemma4:latest", + apiKey: "local-key", + baseUrl: "http://127.0.0.1:11434/v1", + }); + }); }); diff --git a/apps/api/src/team-chat-judge.ts b/apps/api/src/team-chat-judge.ts index bb8f79a..c8deb7b 100644 --- a/apps/api/src/team-chat-judge.ts +++ b/apps/api/src/team-chat-judge.ts @@ -1,14 +1,15 @@ import { randomUUID } from "node:crypto"; import type { AgentModelOAuthCredential, AgentRuntime } from "@rakazo/adapter-kit"; +import type { EncryptedSecretStore } from "@rakazo/adapters"; import { - type EncryptedSecretStore, formatCurrentTimeInstruction, loadUserTimeZone, resolveModelAuth, serializeModelSecret, toOAuthCredential, } from "@rakazo/adapters"; -import { findDefaultModelCredential, findModelCredential, type PrismaClient } from "@rakazo/db"; +import type { PrismaClient } from "@rakazo/db"; +import { findDefaultModelCredential, findModelCredential, findSpaceModelTier } from "@rakazo/db"; import { getLogger } from "@rakazo/logging"; const MAX_RULES_CHARS = 4_000; @@ -191,27 +192,39 @@ export class ModelTeamChatEngagementJudge implements TeamChatEngagementJudge { }; }; } | null> { - const settings = await this.deps.prisma.deploymentSettings.findUnique({ - where: { id: "default" }, - }); const scope = { userId: bot.userId, spaceId: bot.spaceId }; - const defaultCredential = await findDefaultModelCredential(this.deps.prisma, scope); + const explicitJudgeOverride = Boolean(this.deps.providerOverride && this.deps.modelOverride); + const fastTier = + !explicitJudgeOverride && this.deps.prisma.spaceModelTier + ? await findSpaceModelTier(this.deps.prisma, scope, "fast") + : null; + const defaultCredential = fastTier + ? null + : await findDefaultModelCredential(this.deps.prisma, scope); const requestedProvider = this.deps.providerOverride ?? bot.modelProvider; const requestedModel = this.deps.modelOverride ?? bot.modelId; - const overrideCredential = requestedProvider - ? await findModelCredential(this.deps.prisma, scope, requestedProvider) - : null; - const explicitJudgeOverride = Boolean(this.deps.providerOverride && this.deps.modelOverride); + const overrideCredential = + !fastTier && requestedProvider + ? await findModelCredential(this.deps.prisma, scope, requestedProvider) + : null; const useOverride = Boolean( requestedProvider && requestedModel && (explicitJudgeOverride || overrideCredential), ); - const credential = useOverride ? overrideCredential : defaultCredential; + const credential = + fastTier?.credential ?? (useOverride ? overrideCredential : defaultCredential); + const settings = fastTier + ? null + : await this.deps.prisma.deploymentSettings.findUnique({ + where: { id: "default" }, + }); const provider = + fastTier?.credential.provider ?? (useOverride ? requestedProvider : null) ?? credential?.provider ?? settings?.defaultModelProvider ?? this.deps.deploymentProvider; const modelId = + fastTier?.modelId ?? (useOverride ? requestedModel : null) ?? credential?.defaultModel ?? settings?.defaultModelId ?? diff --git a/apps/api/src/thread-target.test.ts b/apps/api/src/thread-target.test.ts index 2027d31..1ee5174 100644 --- a/apps/api/src/thread-target.test.ts +++ b/apps/api/src/thread-target.test.ts @@ -2,15 +2,17 @@ import type { SandboxProvider } from "@rakazo/adapter-kit"; import type { Actor } from "@rakazo/contracts"; import type { PrismaClient } from "@rakazo/db"; import { describe, expect, it, vi } from "vitest"; +import type { ThreadTarget } from "./thread-target.js"; import { cancelSupersededQueuedRuns, followUpGroupThreadMessage, groupDefaultBotId, reactToThreadMessage, resolveGroupFollowUpBotId, + resumeThreadRuns, + sendQueuedThreadMessagesNow, sendThreadMessage, stopThreadRuns, - type ThreadTarget, threadHead, threadSnapshot, } from "./thread-target.js"; @@ -2118,4 +2120,192 @@ describe("stopThreadRuns", () => { expect.objectContaining({ runId: "run-b" }), ); }); + + it("sendQueuedThreadMessagesNow cancels the active run, preserves pending steering, and enqueues an immediate continuation run", async () => { + const queuedCreatedAt = new Date("2026-01-01T10:00:05.000Z"); + const transaction = { + $queryRaw: vi.fn(), + steeringMessage: { + findMany: vi.fn().mockResolvedValue([ + { + id: "steer-1", + botId: "bot-a", + userId: "user-1", + messageId: "msg-queued-1", + claimedAt: null, + message: { + id: "msg-queued-1", + blocks: [{ kind: "text", text: "Send this right now" }], + seq: 2, + createdAt: queuedCreatedAt, + }, + }, + ]), + deleteMany: vi.fn().mockResolvedValue({ count: 0 }), + updateMany: vi.fn().mockResolvedValue({ count: 1 }), + }, + run: { + updateManyAndReturn: vi.fn().mockResolvedValue([{ id: "run-old", taskId: "task-old" }]), + create: vi.fn().mockResolvedValue({ id: "run-new", status: "queued" }), + }, + task: { + updateMany: vi.fn().mockResolvedValue({ count: 1 }), + create: vi.fn().mockResolvedValue({ id: "task-new" }), + }, + message: { + updateMany: vi.fn().mockResolvedValue({ count: 1 }), + }, + computerExecutionLease: { + findMany: vi.fn().mockResolvedValue([]), + }, + computer: { + findMany: vi.fn().mockResolvedValue([]), + }, + event: { + findFirst: vi.fn().mockResolvedValue({ seq: 42 }), + }, + }; + const prisma = { + $transaction: vi.fn(async (callback: (client: typeof transaction) => unknown) => + callback(transaction), + ), + computerExecutionLease: { + updateMany: vi.fn().mockResolvedValue({ count: 0 }), + }, + computer: { + updateMany: vi.fn().mockResolvedValue({ count: 0 }), + }, + event: { + deleteMany: vi.fn().mockResolvedValue({ count: 0 }), + }, + } as unknown as PrismaClient; + const jobs = { + enqueue: vi.fn().mockResolvedValue(undefined), + cancel: vi.fn().mockResolvedValue(undefined), + }; + const events = { + notify: vi.fn().mockResolvedValue(undefined), + }; + const actor = { spaceId: "workspace-1", userId: "user-1" } as Actor; + const target = { + kind: "bot", + botId: "bot-a", + threadId: "thread-1", + bot: {} as never, + } satisfies ThreadTarget; + + const result = await sendQueuedThreadMessagesNow( + { + prisma, + sandbox: {} as SandboxProvider, + events: events as never, + jobs: jobs as never, + }, + actor, + target, + { messageId: "msg-queued-1" }, + ); + + expect(result).toEqual({ ok: true, runIds: ["run-new"] }); + expect(transaction.run.updateManyAndReturn).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + threadId: "thread-1", + botId: { in: ["bot-a"] }, + }), + data: expect.objectContaining({ status: "cancelled" }), + }), + ); + expect(transaction.steeringMessage.updateMany).toHaveBeenCalledWith({ + where: { id: { in: ["steer-1"] } }, + data: { runId: "run-new", claimedAt: null }, + }); + expect(jobs.cancel).toHaveBeenCalledWith("run:run-old"); + expect(jobs.enqueue).toHaveBeenCalledWith({ + name: "run.continue", + payload: { runId: "run-new" }, + replaceKey: "run:run-new", + }); + expect(events.notify).toHaveBeenCalledWith("thread-1", 42); + }); +}); + +describe("resumeThreadRuns", () => { + it("re-queues the latest cancelled or failed run without creating a duplicate message", async () => { + const transaction = { + $queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]), + run: { + findMany: vi + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { + id: "run-cancelled", + taskId: "task-cancelled", + botId: "bot-a", + status: "cancelled", + createdAt: new Date("2026-04-20T10:00:00.000Z"), + completedAt: new Date("2026-04-20T10:02:00.000Z"), + }, + ]), + update: vi.fn().mockResolvedValue({}), + }, + task: { + updateMany: vi.fn().mockResolvedValue({ count: 1 }), + }, + event: { + findFirst: vi.fn().mockResolvedValue({ seq: 19 }), + }, + }; + const prisma = { + $transaction: vi.fn(async (fn: (tx: typeof transaction) => Promise) => + 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); + }); }); diff --git a/apps/api/src/thread-target.ts b/apps/api/src/thread-target.ts index 0c26e27..45ed32b 100644 --- a/apps/api/src/thread-target.ts +++ b/apps/api/src/thread-target.ts @@ -1,5 +1,10 @@ import { ORPCError } from "@orpc/server"; -import { type JobPublisher, runContinueJob, type SandboxProvider } from "@rakazo/adapter-kit"; +import { + type JobPublisher, + runContinueJob, + runJobKey, + type SandboxProvider, +} from "@rakazo/adapter-kit"; import { cancelComputerRunWork, screenLeaseIdForRun, toComputerRef } from "@rakazo/adapters"; import { type Actor, @@ -1151,63 +1156,50 @@ export async function reactToThreadMessage( }); } -export async function stopThreadRuns( +async function snapshotCancelledRunTeardown(tx: Prisma.TransactionClient, runIds: string[]) { + // Snapshot teardown coordinates before commit. Once cancellation becomes + // visible, a worker can release its lease / execution columns immediately; a + // later lookup would then miss the sandbox work this request must stop. + // Team ownership lives on ComputerExecutionLease; Computer.executionRunId is + // only a legacy secondary path and is not written by current acquisition. + const leases = runIds.length + ? await tx.computerExecutionLease.findMany({ + where: { runId: { in: runIds } }, + select: { computerId: true, botId: true, runId: true, fence: true }, + }) + : []; + const leaseComputerIds = [...new Set(leases.map((lease) => lease.computerId))]; + const computers = runIds.length + ? await tx.computer.findMany({ + where: + leaseComputerIds.length > 0 + ? { + OR: [{ id: { in: leaseComputerIds } }, { executionRunId: { in: runIds } }], + } + : { executionRunId: { in: runIds } }, + select: { + id: true, + homeKey: true, + kind: true, + providerRef: true, + executionBotId: true, + executionRunId: true, + }, + }) + : []; + return { runIds, computers, leases }; +} + +async function teardownCancelledRuns( deps: { prisma: PrismaClient; sandbox: SandboxProvider; }, actor: Actor, - target: ThreadTarget, + snapshot: Awaited>, ) { - const { runIds, computers, leases } = await deps.prisma.$transaction(async (tx) => { - await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`; - const cancelled = await tx.run.updateManyAndReturn({ - where: { - threadId: target.threadId, - status: { in: [...ACTIVE_RUN_STATUSES] }, - }, - data: { status: "cancelled", completedAt: new Date() }, - select: { id: true }, - }); - const ids = cancelled.map((run) => run.id); - await tx.steeringMessage.deleteMany({ - where: { - botId: { in: target.kind === "bot" ? [target.botId] : target.memberBotIds }, - message: { threadId: target.threadId }, - }, - }); - // Snapshot teardown coordinates before commit. Once cancellation becomes - // visible, a worker can release its lease / execution columns immediately; a - // later lookup would then miss the sandbox work this request must stop. - // Team ownership lives on ComputerExecutionLease; Computer.executionRunId is - // only a legacy secondary path and is not written by current acquisition. - const leases = ids.length - ? await tx.computerExecutionLease.findMany({ - where: { runId: { in: ids } }, - select: { computerId: true, botId: true, runId: true, fence: true }, - }) - : []; - const leaseComputerIds = [...new Set(leases.map((lease) => lease.computerId))]; - const computers = ids.length - ? await tx.computer.findMany({ - where: - leaseComputerIds.length > 0 - ? { - OR: [{ id: { in: leaseComputerIds } }, { executionRunId: { in: ids } }], - } - : { executionRunId: { in: ids } }, - select: { - id: true, - homeKey: true, - kind: true, - providerRef: true, - executionBotId: true, - executionRunId: true, - }, - }) - : []; - return { runIds: ids, computers, leases }; - }); + const { runIds, computers, leases } = snapshot; + if (runIds.length === 0) return; // Keep the DB lease until after teardown so a replacement run cannot claim the // screen while we still need the cancelled run's screenLeaseId to release it. const computerById = new Map(computers.map((computer) => [computer.id, computer])); @@ -1285,6 +1277,381 @@ export async function stopThreadRuns( }); } +export async function stopThreadRuns( + deps: { + prisma: PrismaClient; + sandbox: SandboxProvider; + }, + actor: Actor, + target: ThreadTarget, +) { + const snapshot = await deps.prisma.$transaction(async (tx) => { + await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`; + const cancelled = await tx.run.updateManyAndReturn({ + where: { + threadId: target.threadId, + status: { in: [...ACTIVE_RUN_STATUSES] }, + }, + data: { status: "cancelled", completedAt: new Date() }, + select: { id: true }, + }); + const ids = cancelled.map((run) => run.id); + await tx.steeringMessage.deleteMany({ + where: { + botId: { in: target.kind === "bot" ? [target.botId] : target.memberBotIds }, + message: { threadId: target.threadId }, + }, + }); + return snapshotCancelledRunTeardown(tx, ids); + }); + await teardownCancelledRuns(deps, actor, snapshot); +} + +/** + * Resume the latest stopped (`cancelled`) or interrupted (`failed`) run in the thread + * without creating a duplicate user message row. + */ +export async function resumeThreadRuns( + deps: { + prisma: PrismaClient; + events: ThreadEvents; + jobs: JobPublisher; + }, + actor: Actor, + target: ThreadTarget, +) { + const botIds = target.kind === "bot" ? [target.botId] : target.memberBotIds; + const committed = await deps.prisma.$transaction(async (tx) => { + await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`; + const activeRuns = await tx.run.findMany({ + where: { + threadId: target.threadId, + botId: { in: botIds }, + status: { in: [...ACTIVE_RUN_STATUSES] }, + }, + select: { id: true, status: true }, + }); + if (activeRuns.length > 0) { + return { + runs: activeRuns, + eventSeq: null as number | null, + }; + } + + const recentTerminals = await tx.run.findMany({ + where: { + threadId: target.threadId, + botId: { in: botIds }, + trigger: { not: "bot_message" }, + status: { in: ["failed", "completed", "cancelled"] }, + }, + orderBy: [{ updatedAt: "desc" }, { id: "desc" }], + take: 20, + select: { + id: true, + botId: true, + taskId: true, + status: true, + createdAt: true, + completedAt: true, + }, + }); + const latestTerminal = pickLatestTerminalRun(recentTerminals); + if ( + !latestTerminal || + (latestTerminal.status !== "cancelled" && latestTerminal.status !== "failed") + ) { + return { + runs: [] as Array<{ id: string; status: string }>, + eventSeq: null as number | null, + }; + } + + await tx.task.updateMany({ + where: { id: latestTerminal.taskId }, + data: { status: "queued" }, + }); + await tx.run.update({ + where: { id: latestTerminal.id }, + data: { + status: "queued", + error: null, + completedAt: null, + leaseOwner: null, + leaseExpiresAt: null, + }, + }); + if (target.kind === "group") { + await touchGroupUpdatedAt(tx, target.groupId); + } + const latestEvent = await tx.event.findFirst({ + where: { threadId: target.threadId }, + orderBy: { seq: "desc" }, + select: { seq: true }, + }); + return { + runs: [{ id: latestTerminal.id, status: "queued" }], + eventSeq: latestEvent?.seq ?? null, + }; + }); + + if (committed.eventSeq !== null) { + await deps.events.notify(target.threadId, committed.eventSeq).catch((error) => { + getLogger().error("thread resume realtime notification", error); + }); + } + await enqueueRunsNeedingContinue(deps.jobs, committed.runs); + return { + ok: true as const, + runIds: committed.runs.map((run) => run.id), + }; +} + +/** + * Promote queued follow-up messages immediately: cancel any currently active runs + * holding up the queue without discarding unclaimed steering messages, create a + * new continuation run for the pending steering messages, and enqueue it right away. + */ +export async function sendQueuedThreadMessagesNow( + deps: { + prisma: PrismaClient; + sandbox: SandboxProvider; + events: ThreadEvents; + jobs: JobPublisher; + }, + actor: Actor, + target: ThreadTarget, + input?: { messageId?: string }, +) { + const botIds = target.kind === "bot" ? [target.botId] : target.memberBotIds; + const committed = await deps.prisma.$transaction(async (tx) => { + await tx.$queryRaw`SELECT id FROM threads WHERE id = ${target.threadId} FOR UPDATE`; + const pendingSteering = await tx.steeringMessage.findMany({ + where: { + botId: { in: botIds }, + claimedAt: null, + message: { threadId: target.threadId }, + }, + include: { + message: { select: { id: true, blocks: true, seq: true, createdAt: true } }, + }, + orderBy: [{ message: { seq: "asc" } }, { id: "asc" }], + }); + + const pendingByBotId = new Map(); + for (const item of pendingSteering) { + const list = pendingByBotId.get(item.botId) ?? []; + list.push(item); + pendingByBotId.set(item.botId, list); + } + + if (pendingByBotId.size === 0 && input?.messageId) { + const targetMsg = await tx.message.findFirst({ + where: { id: input.messageId, threadId: target.threadId, role: "user" }, + select: { id: true, blocks: true, seq: true, createdAt: true }, + }); + if (targetMsg) { + const activeRuns = await tx.run.findMany({ + where: { + threadId: target.threadId, + botId: { in: botIds }, + status: { in: [...ACTIVE_RUN_STATUSES] }, + createdAt: { lt: targetMsg.createdAt }, + }, + select: { id: true, botId: true }, + }); + const fallbackBotId = + activeRuns[0]?.botId ?? + (target.kind === "bot" ? target.botId : groupDefaultBotId(target)); + if (fallbackBotId && activeRuns.length > 0) { + const steering = await tx.steeringMessage.create({ + data: { + messageId: targetMsg.id, + botId: fallbackBotId, + userId: actor.userId, + runId: activeRuns[0]?.id ?? null, + }, + }); + pendingByBotId.set(fallbackBotId, [{ ...steering, message: targetMsg }]); + } + } + } + + if (pendingByBotId.size === 0) { + return { + teardown: { runIds: [] as string[], computers: [], leases: [] }, + runs: [] as Array<{ id: string; status: string }>, + eventSeq: null as number | null, + }; + } + + const affectedBotIds = [...pendingByBotId.keys()]; + const now = new Date(); + const cancelled = await tx.run.updateManyAndReturn({ + where: { + threadId: target.threadId, + botId: { in: affectedBotIds }, + status: { in: [...ACTIVE_RUN_STATUSES] }, + }, + data: { status: "cancelled", completedAt: now }, + select: { id: true, taskId: true }, + }); + const cancelledRunIds = cancelled.map((run) => run.id); + if (cancelled.length > 0) { + await tx.task.updateMany({ + where: { id: { in: cancelled.map((run) => run.taskId) } }, + data: { status: "cancelled" }, + }); + await tx.steeringMessage.deleteMany({ + where: { + runId: { in: cancelledRunIds }, + claimedAt: { not: null }, + }, + }); + } + const teardown = await snapshotCancelledRunTeardown(tx, cancelledRunIds); + + const createdRuns: Array<{ id: string; status: string }> = []; + for (const [botId, items] of pendingByBotId) { + const last = items.at(-1)!; + const task = await tx.task.create({ + data: { + spaceId: actor.spaceId, + botId, + threadId: target.threadId, + userId: items[0]!.userId, + prompt: "Respond to the user's steering context.", + status: "queued", + }, + }); + const runCreatedAt = new Date(Math.max(Date.now(), last.message.createdAt.getTime() + 1)); + const run = await tx.run.create({ + data: { + spaceId: actor.spaceId, + botId, + threadId: target.threadId, + taskId: task.id, + userId: items[0]!.userId, + status: "queued", + trigger: "follow_up", + sourceMessageId: last.message.id, + createdAt: runCreatedAt, + }, + select: { id: true, status: true }, + }); + await tx.steeringMessage.updateMany({ + where: { id: { in: items.map((item) => item.id) } }, + data: { runId: run.id, claimedAt: null }, + }); + await tx.message.updateMany({ + where: { id: { in: items.map((item) => item.message.id) } }, + data: { runId: run.id }, + }); + createdRuns.push(run); + } + + if (target.kind === "group") { + await touchGroupUpdatedAt(tx, target.groupId); + } + const latestEvent = await tx.event.findFirst({ + where: { threadId: target.threadId }, + orderBy: { seq: "desc" }, + select: { seq: true }, + }); + return { + teardown, + runs: createdRuns, + eventSeq: latestEvent?.seq ?? null, + }; + }); + + if (committed.teardown.runIds.length > 0) { + await Promise.all([ + teardownCancelledRuns(deps, actor, committed.teardown), + ...committed.teardown.runIds.map((runId) => + deps.jobs.cancel(runJobKey(runId)).catch(() => undefined), + ), + ]); + } + if (committed.runs.length > 0) { + await enqueueRunsNeedingContinue(deps.jobs, committed.runs); + if (committed.eventSeq != null) { + await deps.events.notify(target.threadId, committed.eventSeq).catch((error) => { + getLogger().error("thread sendNow realtime notification", error); + }); + } + } + return { + ok: true as const, + runIds: committed.runs.map((run) => run.id), + }; +} + +export async function removeQueuedMessage( + deps: { + prisma: PrismaClient; + events: ThreadEvents; + }, + actor: Actor, + target: ThreadTarget, + messageId: string, +) { + const botIds = target.kind === "bot" ? [target.botId] : target.memberBotIds; + const result = await deps.prisma.$transaction(async (tx) => { + // Verify the message exists and belongs to this thread + const message = await tx.message.findFirst({ + where: { + id: messageId, + threadId: target.threadId, + role: "user", + }, + }); + if (!message) { + return { removed: false as const }; + } + + // Check if this message is queued (has an unclaimed steering message) + const steering = await tx.steeringMessage.findFirst({ + where: { + messageId: messageId, + botId: { in: botIds }, + claimedAt: null, + }, + }); + + if (!steering) { + // No queued steering entry — nothing to remove + return { removed: false as const }; + } + + // Delete the steering message (unclaims it from the queue) + await tx.steeringMessage.delete({ + where: { id: steering.id }, + }); + + // Also clean up the user message if it's exclusively queued + // (no run attached to it) + if (!message.runId) { + await tx.message.delete({ + where: { id: messageId }, + }); + } + + return { removed: true as const }; + }); + + if (result.removed) { + // Notify subscribers so the UI updates + const latest = await deps.prisma.event.findFirst({ + where: { threadId: target.threadId }, + orderBy: { seq: "desc" }, + select: { seq: true }, + }); + await deps.events.notify(target.threadId, latest?.seq ?? 0).catch(() => undefined); + } + + return { ok: true as const, removed: result.removed }; +} export async function setThreadUnreadState( prisma: PrismaClient, actor: Actor, diff --git a/apps/api/src/voice.ts b/apps/api/src/voice.ts index b5e9a8c..55351a0 100644 --- a/apps/api/src/voice.ts +++ b/apps/api/src/voice.ts @@ -147,7 +147,11 @@ export async function persistVoiceCredential( let voiceId = input.voiceId?.trim() ?? ""; if (!voiceId) { const voices = await provider.listVoices(input.plaintext, voiceContext(actor, input.signal)); - voiceId = voices[0]?.id ?? ""; + const germanVoice = + input.provider === "kokoro" + ? voices.find((v) => v.id === "df_eva" || /^d[fm]_/i.test(v.id)) + : undefined; + voiceId = germanVoice?.id ?? voices[0]?.id ?? ""; } const stored = await deps.secrets.put(input.plaintext, voiceContext(actor, input.signal)); const cred = await withSerializableRetry(() => diff --git a/apps/api/src/web-origin.test.ts b/apps/api/src/web-origin.test.ts index 943d75b..a61bd0d 100644 --- a/apps/api/src/web-origin.test.ts +++ b/apps/api/src/web-origin.test.ts @@ -25,4 +25,12 @@ describe("requestWebOrigin", () => { "https://evoo.example", ); }); + + it("accepts origins listed in extraTrustedOrigins", () => { + expect( + requestWebOrigin("https://host.example.ts.net:8443", configured, [ + "https://host.example.ts.net:8443", + ]), + ).toBe("https://host.example.ts.net:8443"); + }); }); diff --git a/apps/api/src/web-origin.ts b/apps/api/src/web-origin.ts index 3a210fa..dfef37c 100644 --- a/apps/api/src/web-origin.ts +++ b/apps/api/src/web-origin.ts @@ -14,15 +14,20 @@ export function isLoopbackHost(host: string): boolean { } /** The web origin that browser-facing URLs (OAuth returns, screen links) should - * point at for this request. A browser opened over a loopback or private-network - * address gets its own origin back so a phone on the LAN is not sent to - * 127.0.0.1; any other caller gets the configured WEB_ORIGIN. */ -export function requestWebOrigin(origin: string | undefined, webOrigin: string): string { + * point at for this request. A browser opened over a loopback, private-network, + * or explicitly trusted origin gets its own origin back so a phone on the LAN + * or Tailscale is not sent to 127.0.0.1; any other caller gets WEB_ORIGIN. */ +export function requestWebOrigin( + origin: string | undefined, + webOrigin: string, + extraTrustedOrigins: readonly string[] = [], +): string { if (!origin) return webOrigin; try { const url = new URL(origin); if (url.protocol !== "http:" && url.protocol !== "https:") return webOrigin; if (url.origin === new URL(webOrigin).origin) return url.origin; + if (extraTrustedOrigins.includes(url.origin)) return url.origin; return isPrivateOrLocalHost(url.hostname) ? url.origin : webOrigin; } catch { return webOrigin; diff --git a/apps/mobile/app/group-settings.tsx b/apps/mobile/app/group-settings.tsx index bd667a6..e2c7e43 100644 --- a/apps/mobile/app/group-settings.tsx +++ b/apps/mobile/app/group-settings.tsx @@ -3,8 +3,15 @@ import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; import { Alert, Pressable, ScrollView, Text, TextInput } from "react-native"; import { BotMemberPicker } from "../components/bot-member-picker"; -import { type MobileBot, type MobileGroup, rpc } from "../lib/api"; +import { + type MobileBot, + type MobileGroup, + type MobileSpace, + type MobileSpaceNavigation, + rpc, +} from "../lib/api"; import { useI18n } from "../lib/i18n"; +import { canDeleteInboxSpace, removeInboxSpace } from "../lib/inbox-spaces"; import { useMobileTokens } from "../lib/native"; export default function GroupSettingsScreen() { @@ -13,12 +20,16 @@ export default function GroupSettingsScreen() { const router = useRouter(); const { groupId } = useLocalSearchParams<{ groupId: string }>(); const [group, setGroup] = useState(null); + const [projectSpace, setProjectSpace] = useState(null); const [bots, setBots] = useState([]); const [name, setName] = useState(""); const [selected, setSelected] = useState([]); const [error, setError] = useState(null); const [pending, setPending] = useState(false); + const isMainConversation = group?.kind === "main"; + const isProjectMain = isMainConversation && projectSpace !== null && !projectSpace.isDefault; + useEffect(() => { if (!groupId) return; void Promise.all([ @@ -26,11 +37,19 @@ export default function GroupSettingsScreen() { (groups) => groups.find((row) => row.id === groupId) ?? null, ), rpc("bots/list"), + rpc("spaces/list").catch(() => null), ]) - .then(([nextGroup, nextBots]) => { + .then(([nextGroup, nextBots, navigation]) => { if (!nextGroup) throw new Error(t("Group not found")); setGroup(nextGroup); - setName(nextGroup.name); + const space = + navigation?.spaces.find( + (item) => item.id === nextGroup.spaceId || item.id === navigation.current.id, + ) ?? null; + setProjectSpace(space); + setName( + nextGroup.kind === "main" && space && !space.isDefault ? space.name : nextGroup.name, + ); setSelected(nextGroup.members.map((member) => member.botId)); setBots(nextBots.filter((bot) => !bot.archivedAt)); }) @@ -42,10 +61,20 @@ export default function GroupSettingsScreen() { setPending(true); setError(null); try { + const trimmed = name.trim(); + if (isProjectMain && projectSpace) { + if (trimmed && trimmed !== projectSpace.name) { + await rpc("spaces/update", { spaceId: projectSpace.id, name: trimmed }); + } + router.back(); + return; + } const input: { groupId: string; name?: string; botIds?: string[] } = { groupId }; - if (name.trim() !== group.name) input.name = name.trim(); - const memberIds = group.members.map((member) => member.botId).join(","); - if (selected.join(",") !== memberIds) input.botIds = selected; + if (trimmed !== group.name) input.name = trimmed; + if (!isMainConversation) { + const memberIds = group.members.map((member) => member.botId).join(","); + if (selected.join(",") !== memberIds) input.botIds = selected; + } if (input.name || input.botIds) await rpc("groups/update", input); router.back(); } catch (err) { @@ -57,6 +86,29 @@ export default function GroupSettingsScreen() { function remove() { if (!groupId || !group) return; + if (isProjectMain && projectSpace) { + Alert.alert( + t("Delete {name}?", { name: projectSpace.name }), + t("This removes the empty space for everyone."), + [ + { text: t("Cancel"), style: "cancel" }, + { + text: t("Delete"), + style: "destructive", + onPress: () => + void removeInboxSpace(projectSpace.id, async () => {}) + .then(() => router.replace("/")) + .catch((err) => + Alert.alert( + t("Could not delete space"), + err instanceof Error ? err.message : t("Try again."), + ), + ), + }, + ], + ); + return; + } Alert.alert(group.name, t("Delete this group? Bots and their solo threads are kept."), [ { text: t("Cancel"), style: "cancel" }, { @@ -75,6 +127,12 @@ export default function GroupSettingsScreen() { ]); } + const saveDisabled = + !name.trim() || + pending || + (!isMainConversation && + (selected.length < GROUP_MEMBER_MIN || selected.length > GROUP_MEMBER_MAX)); + return ( <> @@ -97,34 +155,27 @@ export default function GroupSettingsScreen() { fontSize: 16, }} /> - - {t("Members")} - - + {!isMainConversation ? ( + <> + + {t("Members")} + + + + ) : null} {error ? {error} : null} void save()} - disabled={ - !name.trim() || - selected.length < GROUP_MEMBER_MIN || - selected.length > GROUP_MEMBER_MAX || - pending - } + disabled={saveDisabled} style={{ marginTop: 24, backgroundColor: tokens.primary, - opacity: - !name.trim() || - selected.length < GROUP_MEMBER_MIN || - selected.length > GROUP_MEMBER_MAX || - pending - ? 0.5 - : 1, + opacity: saveDisabled ? 0.5 : 1, borderRadius: 11, padding: 14, alignItems: "center", @@ -134,19 +185,35 @@ export default function GroupSettingsScreen() { {pending ? t("Saving…") : t("Save")} - - {t("Delete group")} - + {isProjectMain && projectSpace && canDeleteInboxSpace(projectSpace) ? ( + + {t("Delete space")} + + ) : !isMainConversation ? ( + + {t("Delete group")} + + ) : null} ); diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx index 361d4b4..220c6d5 100644 --- a/apps/mobile/app/index.tsx +++ b/apps/mobile/app/index.tsx @@ -597,7 +597,21 @@ export default function Home() { }} disabled={spaceBusy} onPress={() => { - if (item.space) void chooseInboxSpace(item.space.id); + if (!item.space) return; + const targetSpace = item.space; + const main = !targetSpace.isDefault + ? targetSpace.groups.find((group) => group.kind === "main") + : undefined; + if (main) { + void openMobileSpace(targetSpace.id, () => + router.push({ + pathname: "/group-thread", + params: { groupId: main.id, name: targetSpace.name }, + }), + ); + return; + } + void chooseInboxSpace(targetSpace.id); }} style={({ pressed }) => [styles.spaceSelect, pressed && styles.rowPressed]} > diff --git a/apps/mobile/app/thread.tsx b/apps/mobile/app/thread.tsx index efc92ba..71ef8ac 100644 --- a/apps/mobile/app/thread.tsx +++ b/apps/mobile/app/thread.tsx @@ -6,22 +6,19 @@ import type { Connection, ConnectionCatalogItem, MessageBlock, + MessageReaction, Routine, } from "@rakazo/contracts"; -import { - AUTOPILOT_LIMITS, - canReactToThreadMessage, - MESSAGE_REACTIONS, - type MessageReaction, -} from "@rakazo/contracts"; -import type { AutopilotUntilChoice } from "@rakazo/core"; +import { AUTOPILOT_LIMITS, canReactToThreadMessage, MESSAGE_REACTIONS } from "@rakazo/contracts"; +import type { AutopilotUntilChoice, ComposerMention, SlashActionId } from "@rakazo/core"; import { abortableDelay, attachmentsForThread, autopilotUntil, buildComposerMentionOptions, - type ComposerMention, + clearComposerDraft, cloudAgentHttpsUrl, + composerDraftThreadKey, deriveWorkspaceFocus, foldRunNarration, isApprovalAskBlock, @@ -32,13 +29,15 @@ import { liveScreenRunId, mentionChipKey, projectMessageReactions, + queuedFollowUps, + readComposerDraft, resolveComposerSendPlan, SLASH_ACTIONS, - type SlashActionId, selectedAskActionLabel, serializeComposerPrompt, truncateSlashDescription, userVisibleMessages, + writeComposerDraft, } from "@rakazo/core"; import * as Clipboard from "expo-clipboard"; import { useFocusEffect, useLocalSearchParams, useNavigation, useRouter } from "expo-router"; @@ -289,6 +288,8 @@ function Thread() { const mentionBotsAppliedGeneration = useRef(0); const readVisibleTarget = useRef(null); const threadKey = groupId ?? botId; + const draftKey = composerDraftThreadKey({ botId, groupId }); + const initialDraft = useMemo(() => readComposerDraft(draftKey), [draftKey]); const [threadScrollState, setThreadScrollState] = useState(() => scrollBehavior.current.state(), ); @@ -310,7 +311,7 @@ function Thread() { /** Run that last pointed at the bot's screen, so the live screen is offered only during real screen work. */ const [screenRunId, setScreenRunId] = useState(null); const activeThreadId = useRef(undefined); - const [draft, setDraft] = useState(""); + const [draft, setDraft] = useState(() => initialDraft.text); const [mentionQuery, setMentionQuery] = useState(null); const [slashQuery, setSlashQuery] = useState(null); const [agentSkills, setAgentSkills] = useState([]); @@ -325,12 +326,17 @@ function Thread() { connectionId?: string; }> >([]); - const [selectedMentions, setSelectedMentions] = useState([]); - const [selectedSkill, setSelectedSkill] = useState(null); + const [selectedMentions, setSelectedMentions] = useState( + () => initialDraft.selectedMentions, + ); + const [selectedSkill, setSelectedSkill] = useState( + () => initialDraft.selectedSkill, + ); const [pendingAttachments, setPendingAttachments] = useState([]); const [replyTarget, setReplyTarget] = useState(null); const [attachmentNotice, setAttachmentNotice] = useState(null); const [sending, setSending] = useState(false); + const [stopped, setStopped] = useState(false); const [error, setError] = useState(null); const [loadingOlder, setLoadingOlder] = useState(false); const [markdownPreview, setMarkdownPreview] = useState( @@ -353,6 +359,9 @@ function Thread() { () => reactionView.visibleMessages.filter((message) => !runNarration.hidden.has(message.id)), [reactionView.visibleMessages, runNarration], ); + const [taskPillExpanded, setTaskPillExpanded] = useState(false); + const [dismissedPlanKey, setDismissedPlanKey] = useState(null); + const [taskPillSubStepsOpen, setTaskPillSubStepsOpen] = useState(false); const latestMessageId = visibleMessages.at(-1)?.id ?? null; const activePendingAttachments = attachmentsForThread(pendingAttachments, threadKey); const composerMentionTargets = useMemo( @@ -431,6 +440,106 @@ function Thread() { }); }, [inGroup, snap?.activeRuns, snap?.members, snap?.run]); const working = inGroup ? workingGroupBots.length > 0 : isWorkingStatus(currentBotStatus); + const activeMobilePlan = useMemo(() => { + const msgs = reactionView.visibleMessages; + let lastUserIdx = -1; + for (let i = msgs.length - 1; i >= 0; i--) { + if (msgs[i]?.role === "user") { + lastUserIdx = i; + break; + } + } + let planIdx = -1; + let planBlock: Extract | 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(); + const stepIds = new Set(); + const steps: MobileMessage[] = []; + for (let i = Math.max(0, lastUserIdx + 1, planIdx); i < msgs.length; i++) { + const m = msgs[i]; + if (!m) continue; + const folded = runNarration.steps.get(m.id); + if (folded && folded.length > 0) { + absorbedIds.add(m.id); + for (const s of folded) { + if (!stepIds.has(s.id)) { + stepIds.add(s.id); + steps.push(s); + } + } + } + } + const doneCount = planBlock.steps.filter( + (s) => s.status === "completed" || s.status === "skipped", + ).length; + const isCompleted = + planBlock.status === "completed" || + (planBlock.steps.length > 0 && doneCount === planBlock.steps.length && !working); + return { + block: planBlock, + steps, + absorbedIds, + doneCount, + isCompleted, + completionKey: `${planBlock.planId}:${isCompleted ? "done" : "active"}`, + }; + }, [reactionView.visibleMessages, runNarration.steps, working]); + + useEffect(() => { + if (!activeMobilePlan?.isCompleted) { + setDismissedPlanKey(null); + return; + } + const key = activeMobilePlan.completionKey; + const collapseTimer = setTimeout(() => setTaskPillExpanded(false), 1800); + const hideTimer = setTimeout(() => setDismissedPlanKey(key), 3500); + return () => { + clearTimeout(collapseTimer); + clearTimeout(hideTimer); + }; + }, [activeMobilePlan?.completionKey, activeMobilePlan?.isCompleted]); + const activeRunCreatedAt = useMemo(() => { + const candidates = snap?.activeRuns ?? (snap?.run ? [snap.run] : []); + return candidates.find((run) => isWorkingStatus(run.status))?.createdAt; + }, [snap?.activeRuns, snap?.run]); + const queuedIds = useMemo( + () => + new Set( + (working + ? queuedFollowUps(snap?.messages ?? [], activeRunCreatedAt, reactionView.reactions) + : [] + ).map((message) => message.id), + ), + [activeRunCreatedAt, reactionView.reactions, snap?.messages, working], + ); useEffect(() => { void rpc("agentSkills/list") @@ -1002,16 +1111,31 @@ function Thread() { ); }, [botId, groupId, messageId]); + const loadedDraftKeyRef = useRef(draftKey); + useLayoutEffect(() => { + if (loadedDraftKeyRef.current !== draftKey) { + loadedDraftKeyRef.current = draftKey; + const stored = readComposerDraft(draftKey); + setDraft(stored.text); + setSelectedSkill(stored.selectedSkill); + setSelectedMentions(stored.selectedMentions); + return; + } + writeComposerDraft(draftKey, { + text: draft, + selectedSkill, + selectedMentions, + }); + }, [draft, draftKey, selectedMentions, selectedSkill]); + useEffect(() => { setPendingAttachments((current) => attachmentsForThread(current, threadKey)); - setDraft(""); setMentionQuery(null); setSlashQuery(null); - setSelectedSkill(null); - setSelectedMentions([]); setReplyTarget(null); setAttachmentNotice(null); setError(null); + setStopped(false); }, [threadKey]); function updateDraft(value: string) { @@ -1053,6 +1177,11 @@ function Thread() { function runSlashAction(action: SlashActionId) { setDraft(""); setSlashQuery(null); + writeComposerDraft(draftKey, { + text: "", + selectedSkill, + selectedMentions, + }); if (action === "chat-settings") { if (inGroup && groupId) { router.push({ pathname: "/group-settings", params: { groupId } }); @@ -1078,6 +1207,10 @@ function Thread() { const initialGroupTarget = groupId; if ((!initialBotTarget && !initialGroupTarget) || sending) return; const originThreadKey = initialGroupTarget ?? initialBotTarget; + const originDraftKey = composerDraftThreadKey({ + botId: initialBotTarget, + groupId: initialGroupTarget, + }); const attachments = attachmentsForThread(pendingAttachments, originThreadKey); const plan = resolveComposerSendPlan({ text: serializeComposerPromptText(), @@ -1097,6 +1230,7 @@ function Thread() { if (initialBotTarget) cancelFocusPrompt(initialBotTarget); }; setSending(true); + setStopped(false); setError(null); try { if (plan.shouldRunRoutines) { @@ -1111,6 +1245,7 @@ function Thread() { ); } const clearOriginComposer = () => { + clearComposerDraft(originDraftKey); setPendingAttachments((current) => current.filter((attachment) => attachment.threadKey !== originThreadKey), ); @@ -1205,12 +1340,16 @@ function Thread() { const targetGroupId = groupId; if ((!targetBotId && !targetGroupId) || sending) return; setSending(true); + setStopped(false); setError(null); try { await rpc( "threads/stop", targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }, ); + if (isCurrentTarget(targetBotId, targetGroupId)) { + setStopped(true); + } } catch (err) { if (isCurrentTarget(targetBotId, targetGroupId)) { setError(err instanceof Error ? err.message : t("Failed to stop work")); @@ -1230,6 +1369,80 @@ function Thread() { } } + async function resume() { + const targetBotId = botId; + const targetGroupId = groupId; + if ((!targetBotId && !targetGroupId) || sending) return; + setSending(true); + setStopped(false); + setError(null); + try { + await rpc( + "threads/resume", + targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }, + ); + if (isCurrentTarget(targetBotId, targetGroupId)) { + await refresh(); + } + } catch (err) { + if (isCurrentTarget(targetBotId, targetGroupId)) { + setError(err instanceof Error ? err.message : t("Could not continue")); + } + } finally { + setSending(false); + } + } + + const [sendingNowId, setSendingNowId] = useState(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(null); + + async function removeQueuedNow(message: MobileMessage) { + const targetBotId = botId; + const targetGroupId = groupId; + const messageId = message.id.startsWith("temp-") ? undefined : message.id; + if (!messageId || (!targetBotId && !targetGroupId) || removingQueuedId) return; + setRemovingQueuedId(message.id); + setError(null); + try { + await rpc("threads/removeQueuedMessage", { + ...(targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }), + messageId, + }); + if (isCurrentTarget(targetBotId, targetGroupId)) { + await refresh(); + } + } catch (err) { + if (isCurrentTarget(targetBotId, targetGroupId)) { + setError(err instanceof Error ? err.message : t("Failed to remove message")); + } + } finally { + setRemovingQueuedId(null); + } + } const answerMessage = useCallback( async (message: MobileMessage, answer: string) => { const targetBotId = botId; @@ -1313,6 +1526,10 @@ function Thread() { const targetGroupId = groupId; if (!targetBotId && !targetGroupId) return; const target = targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! }; + const targetDraftKey = composerDraftThreadKey({ + botId: targetBotId, + groupId: targetGroupId, + }); const until = autopilotUntil(choice, new Date()); void rpc("autopilot/start", { ...target, @@ -1320,8 +1537,12 @@ function Thread() { ...(until ? { until: until.toISOString() } : {}), }) .then(async () => { - setDraft(""); - if (isCurrentTarget(targetBotId, targetGroupId)) await refresh(); + const existing = readComposerDraft(targetDraftKey); + writeComposerDraft(targetDraftKey, { ...existing, text: "" }); + if (isCurrentTarget(targetBotId, targetGroupId)) { + setDraft(""); + await refresh(); + } }) .catch((err) => Alert.alert(t("Autopilot"), err instanceof Error ? err.message : t("Try again.")), @@ -1553,7 +1774,9 @@ function Thread() { flexShrink: 1, }} > - {foldedSteps ? : null} + {foldedSteps && !activeMobilePlan?.absorbedIds.has(message.id) ? ( + + ) : null} + {queuedIds.has(message.id) ? ( + + {t("Queued")} + void sendQueuedNow(message)} + > + + {t("Send now")} + + + void removeQueuedNow(message)} + > + + {t("Remove")} + + + + ) : null} {messageReactions ? ( {error ? {error} : null} {runError ? ( - {runError} + + {runError} + {!working ? ( + void resume()} + style={{ + borderRadius: 999, + borderWidth: 1, + borderColor: tokens.border, + backgroundColor: tokens.card, + paddingHorizontal: 12, + paddingVertical: 6, + opacity: sending ? 0.5 : 1, + }} + > + + {t("Resume")} + + + ) : null} + + ) : stopped && !working ? ( + + {t("Stopped")} + void resume()} + style={{ + borderRadius: 999, + borderWidth: 1, + borderColor: tokens.border, + backgroundColor: tokens.card, + paddingHorizontal: 12, + paddingVertical: 6, + opacity: sending ? 0.5 : 1, + }} + > + + {t("Resume")} + + + ) : null} {showPinnedPage ? ( @@ -2025,6 +2360,191 @@ function Thread() { ))} ) : null} + {activeMobilePlan && + !(activeMobilePlan.isCompleted && dismissedPlanKey === activeMobilePlan.completionKey) ? ( + + setTaskPillExpanded((current) => !current)} + style={{ gap: 6 }} + > + + + + + {activeMobilePlan.isCompleted ? "✓" : "▸"} + + + {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)} + + + + + {activeMobilePlan.isCompleted + ? t("Completed") + : `${activeMobilePlan.doneCount}/${activeMobilePlan.block.steps.length}`} + + + + + + {taskPillExpanded ? ( + + + {activeMobilePlan.block.title} + + {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 ( + + + {glyph} + + + + {step.label} + + {step.note ? ( + + {step.note} + + ) : null} + + + ); + })} + {activeMobilePlan.steps.length > 0 ? ( + + setTaskPillSubStepsOpen((current) => !current)} + > + + {t("{count} steps", { count: activeMobilePlan.steps.length })}{" "} + {taskPillSubStepsOpen ? "▴" : "▾"} + + + {taskPillSubStepsOpen + ? activeMobilePlan.steps.map((s) => ( + + {blockText(s)} + + )) + : null} + + ) : null} + + ) : null} + + ) : null} ); } + const plans = message.blocks.filter( + (block): block is Extract => block.kind === "plan", + ); + if (plans.length) { + return ( + + {plans.map((plan) => { + const doneCount = plan.steps.filter( + (s) => s.status === "completed" || s.status === "skipped", + ).length; + const isCompleted = plan.status === "completed"; + return ( + + + + {plan.title} + + + {isCompleted ? t("Completed") : `${doneCount}/${plan.steps.length}`} + + + + {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 ( + + + {glyph} + + + + {step.label} + + {step.note ? ( + + {step.note} + + ) : null} + + + ); + })} + + + ); + })} + + ); + } if (isRoutineEventMessage(message.blocks)) { return ( diff --git a/apps/mobile/lib/api.ts b/apps/mobile/lib/api.ts index 66b196b..e09e112 100644 --- a/apps/mobile/lib/api.ts +++ b/apps/mobile/lib/api.ts @@ -784,7 +784,7 @@ export type MobileGroup = Pick< | "updatedAt" | "members" > & - Partial>; + Partial>; export type MobileSpace = Space; export type MobileSpaceNavigation = SpaceNavigation; @@ -797,8 +797,14 @@ export type MobileSnapshot = { cursor?: number; messages: MobileMessage[]; olderCursor: number | null; - run: { id: string; botId?: string; status: string; error?: string | null } | null; - activeRuns?: Array<{ id: string; botId?: string; status: string }>; + run: { + id: string; + botId?: string; + status: string; + error?: string | null; + createdAt?: string; + } | null; + activeRuns?: Array<{ id: string; botId?: string; status: string; createdAt?: string }>; members?: MobileGroup["members"]; computer?: { kind?: string; diff --git a/apps/mobile/lib/inbox-spaces.test.ts b/apps/mobile/lib/inbox-spaces.test.ts index 42e9472..e0cb18a 100644 --- a/apps/mobile/lib/inbox-spaces.test.ts +++ b/apps/mobile/lib/inbox-spaces.test.ts @@ -147,12 +147,18 @@ describe("spaceInboxItems", () => { expect(canDeleteInboxSpace(space({ canDelete }))).toBe(false); }); - it("offers deletion for an explicitly authorized empty non-default space", () => { + it("offers deletion for an explicitly authorized non-default space", () => { expect(canDeleteInboxSpace(space())).toBe(true); + expect(canDeleteInboxSpace(space({ hasContent: true, canDelete: true }))).toBe(true); }); - it("hides deletion while a space still has content", () => { - expect(canDeleteInboxSpace(space({ hasContent: true }))).toBe(false); + it("deduplicates same-named projects into a single project entry", () => { + const first = space({ id: "space-work-1", name: "Work", hasContent: false }); + const second = space({ id: "space-work-2", name: " work ", hasContent: true, bots: [bot] }); + const items = spaceInboxItems([first, second]); + const headings = items.filter((item) => item.type === "heading" && item.space); + expect(headings).toHaveLength(1); + expect(headings[0]).toMatchObject({ key: "space-work-2", title: " work " }); }); }); diff --git a/apps/mobile/lib/inbox-spaces.ts b/apps/mobile/lib/inbox-spaces.ts index 532c081..dbb44e8 100644 --- a/apps/mobile/lib/inbox-spaces.ts +++ b/apps/mobile/lib/inbox-spaces.ts @@ -24,22 +24,47 @@ export type InboxSpaceItem = | { type: "heading"; key: string; title: string; space?: InboxSpace }; export function canDeleteInboxSpace( - space: Pick, + space: Pick & Partial>, ) { - return !space.isDefault && !space.hasContent && space.canDelete === true; + return !space.isDefault && space.canDelete === true; } export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] { - return spaces.flatMap((space): InboxSpaceItem[] => { + const seenProjectNames = new Set(); + const canonicalProjectIds = new Set(); + const orderedProjects = [...spaces] + .filter((space) => !space.isDefault) + .sort((left, right) => Number(Boolean(right.hasContent)) - Number(Boolean(left.hasContent))); + for (const space of orderedProjects) { + const normalized = space.name.trim().toLowerCase(); + if (normalized && seenProjectNames.has(normalized)) continue; + if (normalized) seenProjectNames.add(normalized); + canonicalProjectIds.add(space.id); + } + + const visibleSpaces = spaces.filter( + (space) => space.isDefault || canonicalProjectIds.has(space.id), + ); + + return visibleSpaces.flatMap((space): InboxSpaceItem[] => { + const normalizedSpaceName = space.name.trim().toLowerCase(); const chats = [ // A main bot is reached through its space's main conversation, not listed again. ...space.bots .filter((chat) => chat.kind !== "main") .map((chat) => ({ type: "bot" as const, bot: chat, ...chat })), - ...space.groups.map((chat) => ({ type: "group" as const, group: chat, ...chat })), + ...space.groups + .filter((chat) => { + const normalizedGroupName = chat.name.trim().toLowerCase(); + if (!space.isDefault && normalizedGroupName === normalizedSpaceName) return false; + if (space.isDefault && chat.kind !== "main" && seenProjectNames.has(normalizedGroupName)) + return false; + return true; + }) + .map((chat) => ({ type: "group" as const, group: chat, ...chat })), ]; const items: InboxSpaceItem[] = []; - if (spaces.length > 1 || !space.isDefault || chats.length === 0) { + if (visibleSpaces.length > 1 || !space.isDefault || chats.length === 0) { items.push({ type: "heading", key: space.id, title: space.name, space }); } for (const group of groupBotsForSidebar(chats, space.botSections)) { diff --git a/apps/mobile/lib/locales/de.ts b/apps/mobile/lib/locales/de.ts index d105756..aea56e1 100644 --- a/apps/mobile/lib/locales/de.ts +++ b/apps/mobile/lib/locales/de.ts @@ -261,6 +261,9 @@ export const DE_MESSAGES: Record = { Eyeshadow: "Lidschatten", Failed: "Fehlgeschlagen", "Failed to refresh": "Aktualisieren fehlgeschlagen", + Resume: "Fortsetzen", + Stopped: "Gestoppt", + "Failed to remove message": "Nachricht konnte nicht entfernt werden", "Failed to send message": "Nachricht konnte nicht gesendet werden", "Failed to stop work": "Arbeit konnte nicht gestoppt werden", "Fewer {label}": "Weniger {label}", @@ -497,6 +500,7 @@ export const DE_MESSAGES: Record = { "Section name": "Abschnittsname", Send: "Senden", "Send answer": "Antwort senden", + "Send now": "Jetzt gleich senden", "Send reset link": "Link zum Zurücksetzen senden", "Sending…": "Wird gesendet…", Server: "Server", diff --git a/apps/mobile/lib/locales/ru.ts b/apps/mobile/lib/locales/ru.ts index 9394f53..93b41f6 100644 --- a/apps/mobile/lib/locales/ru.ts +++ b/apps/mobile/lib/locales/ru.ts @@ -274,6 +274,9 @@ export const RU_MESSAGES: Record = { "Executor token": "Токен Executor", Failed: "Ошибка", "Failed to refresh": "Не удалось обновить", + Resume: "Продолжить", + Stopped: "Остановлено", + "Failed to remove message": "Не удалось удалить сообщение", "Failed to send message": "Не удалось отправить сообщение", "Failed to stop work": "Не удалось остановить работу", File: "Файл", @@ -429,6 +432,7 @@ export const RU_MESSAGES: Record = { Send: "Отправить", "Send reset link": "Отправить ссылку для сброса", "Send answer": "Отправить ответ", + "Send now": "Отправить сейчас", "Sending…": "Отправка…", Server: "Сервер", "Server URL": "URL-адрес сервера", diff --git a/apps/mobile/lib/locales/zh.ts b/apps/mobile/lib/locales/zh.ts index a2af302..01f96f7 100644 --- a/apps/mobile/lib/locales/zh.ts +++ b/apps/mobile/lib/locales/zh.ts @@ -259,6 +259,9 @@ export const ZH_MESSAGES: Record = { "Executor token": "Executor 令牌", Failed: "失败", "Failed to refresh": "刷新失败", + Resume: "继续", + Stopped: "已停止", + "Failed to remove message": "删除消息失败", "Failed to send message": "发送消息失败", "Failed to stop work": "停止工作失败", File: "文件", @@ -410,6 +413,7 @@ export const ZH_MESSAGES: Record = { Send: "发送", "Send reset link": "发送重置链接", "Send answer": "发送回答", + "Send now": "立即发送", "Sending…": "正在发送…", Server: "服务器", "Server URL": "服务器地址", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index fa9ab15..4a7ed97 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -18,14 +18,14 @@ }, "dependencies": { "@expo/metro-runtime": "57.0.17", - "@expo/ui": "57.0.25", + "@expo/ui": "57.0.26", "@rakazo/chat-ui": "workspace:*", "@rakazo/contracts": "workspace:*", "@rakazo/core": "workspace:*", "@rakazo/ui-tokens": "workspace:*", "@react-native-vector-icons/ionicons": "13.1.4", "@react-native-vector-icons/material-design-icons": "13.1.4", - "expo": "57.0.30", + "expo": "57.0.31", "expo-application": "57.0.3", "expo-audio": "57.0.5", "expo-clipboard": "57.0.2", @@ -37,11 +37,11 @@ "expo-keep-awake": "57.0.2", "expo-linking": "57.0.13", "expo-localization": "57.0.2", - "expo-modules-core": "57.0.23", - "expo-notifications": "57.0.23", - "expo-router": "57.0.28", + "expo-modules-core": "57.0.24", + "expo-notifications": "57.0.24", + "expo-router": "57.0.29", "expo-secure-store": "57.0.4", - "expo-sharing": "57.0.23", + "expo-sharing": "57.0.24", "expo-splash-screen": "57.0.10", "expo-status-bar": "57.0.1", "expo-symbols": "57.0.3", diff --git a/apps/web/e2e/adaptive-workspace.spec.ts b/apps/web/e2e/adaptive-workspace.spec.ts index 5b85370..16b66f0 100644 --- a/apps/web/e2e/adaptive-workspace.spec.ts +++ b/apps/web/e2e/adaptive-workspace.spec.ts @@ -31,7 +31,7 @@ test("the bot's screen opens beside the chat while it works and steps back after await expect(sidePanel).toHaveAttribute("data-panel", "computer"); await expect(sidePanel).toHaveAttribute("data-auto", ""); - await expect(page.getByTestId("computer-preview").locator("iframe")).toHaveAttribute( + await expect(page.getByTestId("oo-computer-browser-viewport").locator("iframe")).toHaveAttribute( "src", SCREEN_URL, ); diff --git a/apps/web/e2e/agent-cursor.spec.ts b/apps/web/e2e/agent-cursor.spec.ts index 052a821..07b7933 100644 --- a/apps/web/e2e/agent-cursor.spec.ts +++ b/apps/web/e2e/agent-cursor.spec.ts @@ -26,16 +26,28 @@ test("the bot's cursor glides to where it clicks on its live screen", async ({ ); await page.getByTitle("Agent computer").click(); - const preview = page.getByTestId("computer-preview"); + const preview = page.getByTestId("oo-computer-browser-viewport"); await expect(preview.locator("iframe")).toHaveAttribute("src", SCREEN_URL); const cursor = preview.getByTestId("agent-cursor"); await expect(cursor).toHaveCSS("opacity", "0"); + // The click and typing last about a second, so record every action rather than racing a poll. + await cursor.evaluate((node) => { + const seen: string[] = []; + (window as unknown as { cursorActions: string[] }).cursorActions = seen; + new MutationObserver(() => seen.push(node.getAttribute("data-action") ?? "")).observe(node, { + attributeFilter: ["data-action"], + }); + }); // The scripted runtime clicks the screen center (640, 400) and types. await rpc(page, "threads/send", { botId, text: "use your screen and type cursor-demo" }); await expect(cursor).toHaveCSS("opacity", "1"); - await expect(cursor).toHaveAttribute("data-action", /click|type/); + await expect + .poll(() => + page.evaluate(() => (window as unknown as { cursorActions: string[] }).cursorActions), + ) + .toEqual(expect.arrayContaining(["click", "type"])); const layer = (await preview.getByTestId("agent-cursor-layer").boundingBox())!; await expect .poll(async () => { diff --git a/apps/web/e2e/artifact-preview.spec.ts b/apps/web/e2e/artifact-preview.spec.ts index 8fa54ce..e39ae03 100644 --- a/apps/web/e2e/artifact-preview.spec.ts +++ b/apps/web/e2e/artifact-preview.spec.ts @@ -12,14 +12,12 @@ test("agent-attached files appear as downloadable cards", async ({ page }, testI await composer.fill("write notes/result.txt and attach it to the thread"); await page.keyboard.press("Enter"); - // Scope to ArtifactFileCard text (name + mime); sidebar bot status can also - // include the prompt path notes/result.txt once status syncs immediately. - const fileCard = page.getByRole("button", { name: /result\.txt text\/plain/ }); + const fileCard = page.getByRole("button", { name: "Preview result.txt" }); await expect(fileCard).toBeVisible({ timeout: 30_000 }); await captureScreenshot(page, testInfo, "current-file-card"); const downloadPromise = page.waitForEvent("download"); - await fileCard.click(); + await page.getByRole("button", { name: "Download result.txt" }).click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe("result.txt"); }); diff --git a/apps/web/e2e/bot-crud.spec.ts b/apps/web/e2e/bot-crud.spec.ts index e0581de..f26d732 100644 --- a/apps/web/e2e/bot-crud.spec.ts +++ b/apps/web/e2e/bot-crud.spec.ts @@ -114,12 +114,12 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = await captureScreenshot(page, testInfo, "27a-bot-settings-model"); await page.getByRole("button", { name: "Show computer" }).click(); const sidePanel = page.getByTestId("side-panel"); - await expect(sidePanel).toHaveAttribute("data-panel", "computer"); + await expect(sidePanel).toHaveAttribute("data-panel", /^(computer|browser)$/); await expect(page.getByRole("button", { name: "Show settings" })).toBeVisible(); // Overlay may flash during boot or never appear (already ready/asleep/stopped). Assert panel // chrome, then wait until any overlay has cleared — avoid Locator.or() strict-mode multi-hits. const bootOverlay = page.getByText(/Booting up .* computer/); - await expect(sidePanel.getByTestId("computer-preview")).toBeVisible(); + await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible(); await expect(bootOverlay).toBeHidden(); await expect(sidePanel.getByText("Teach a task")).toHaveCount(0); await expect(sidePanel.getByTestId("teach-start-button")).toHaveCount(0); diff --git a/apps/web/e2e/computer-screen-error.spec.ts b/apps/web/e2e/computer-screen-error.spec.ts index 94f3dc1..5fd2aff 100644 --- a/apps/web/e2e/computer-screen-error.spec.ts +++ b/apps/web/e2e/computer-screen-error.spec.ts @@ -39,9 +39,9 @@ test("screen connection failures stay visible and can be retried", async ({ page ); await page.getByTitle("Agent computer").click(); - const preview = page.getByTestId("computer-preview"); + const preview = page.getByTestId("oo-computer-browser-viewport"); await expect(preview.getByRole("alert")).toContainText("temporarily busy"); - await expect(preview.getByTestId("computer-preview-open")).toHaveCount(0); + await expect(page.getByTestId("computer-open")).toHaveCount(0); await captureScreenshot(page, testInfo, "computer-screen-connection-error"); failScreen = false; @@ -50,14 +50,14 @@ test("screen connection failures stay visible and can be retried", async ({ page await expect(preview.getByRole("alert")).toHaveCount(0); failScreen = true; - await preview.hover(); - await preview.getByTestId("computer-preview-open").click(); + await page.getByTestId("computer-open").click(); await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); - await expect(page.getByRole("alert")).toContainText("temporarily busy"); + const fullScreen = page.getByTestId("computer-viewport"); + await expect(fullScreen.getByRole("alert")).toContainText("temporarily busy"); await captureScreenshot(page, testInfo, "computer-full-screen-connection-error"); failScreen = false; - await page.getByRole("button", { name: "Retry screen" }).click(); + await fullScreen.getByRole("button", { name: "Retry screen" }).click(); await expect(page.locator('iframe[title="Bot screen"]')).toHaveAttribute("src", screenUrl); await expect(page.getByRole("alert")).toHaveCount(0); }); diff --git a/apps/web/e2e/computer-update.spec.ts b/apps/web/e2e/computer-update.spec.ts index bd3d2c1..c07e3e9 100644 --- a/apps/web/e2e/computer-update.spec.ts +++ b/apps/web/e2e/computer-update.spec.ts @@ -59,9 +59,8 @@ test("computer maintenance shows durable background progress and failure recover }); }); await page.getByTitle("Agent computer").click(); - await expect(page.getByTestId("computer-preview")).toBeVisible(); - await page.getByTestId("computer-preview").hover(); - await page.getByTestId("computer-preview-open").click(); + await expect(page.getByTestId("oo-computer-browser-viewport")).toBeVisible(); + await page.getByTestId("computer-open").click(); await page.getByTestId("computer-more-button").click(); await page.getByRole("menuitem", { name: "Update computer", exact: true }).click(); const dialog = page.getByTestId("computer-update-dialog"); diff --git a/apps/web/e2e/first-run-onboarding-ux.spec.ts b/apps/web/e2e/first-run-onboarding-ux.spec.ts index 73fea3b..37245d1 100644 --- a/apps/web/e2e/first-run-onboarding-ux.spec.ts +++ b/apps/web/e2e/first-run-onboarding-ux.spec.ts @@ -67,7 +67,7 @@ test("main conversation does not show a group subtitle for one Oo", async ({ pag await signup(page, `main-1-1-${stamp}@rakazo.test`, "password12", "Solo"); await completeOnboarding(page, undefined, { mainConversation: true }); await expect(page.getByTestId("bot-settings-trigger")).not.toContainText("Gruppenunterhaltung"); - await expect(page.getByTestId("oo-group-stage")).not.toContainText("Gruppenunterhaltung"); + await expect(page.getByTestId("oo-group-stage")).toHaveCount(0); }); test.describe("mobile shell", () => { @@ -94,10 +94,11 @@ test("main conversation opens its lead's computer", async ({ page }, testInfo) = await signup(page, `main-computer-${stamp}@rakazo.test`, "password12", "Screen"); await completeOnboarding(page, undefined, { mainConversation: true }); await expect(page).toHaveURL(/\/app\/g\//); + // The lead's computer opens beside the main conversation without leaving it. await page.getByTitle("Agent computer").click(); - await expect(page).not.toHaveURL(/\/app\/g\//); + await expect(page).toHaveURL(/\/app\/g\//); const sidePanel = page.getByTestId("side-panel"); await expect(sidePanel).toHaveAttribute("data-panel", "computer"); - await expect(sidePanel.getByTestId("computer-preview")).toBeVisible(); + await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible(); await captureScreenshot(page, testInfo, "main-conversation-computer"); }); diff --git a/apps/web/e2e/golden.spec.ts b/apps/web/e2e/golden.spec.ts index 8487097..1e4341b 100644 --- a/apps/web/e2e/golden.spec.ts +++ b/apps/web/e2e/golden.spec.ts @@ -80,8 +80,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te expect(mainBox).not.toBeNull(); expect(panelBox).not.toBeNull(); expect((mainBox?.x ?? 0) + (mainBox?.width ?? 0)).toBeLessThanOrEqual(panelBox?.x ?? 0); - await sidePanel.getByTestId("computer-preview").hover(); - await sidePanel.getByTestId("computer-preview-open").click(); + await sidePanel.getByTestId("computer-open").click(); await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); await expect(page.getByRole("button", { name: "Skip", exact: true }).last()).toBeVisible(); await expect(page.getByRole("button", { name: "I’m done", exact: true }).last()).toBeVisible(); @@ -107,8 +106,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te } await page.getByTitle("Agent computer").click(); await expect(sidePanel).toHaveAttribute("data-panel", "computer"); - await sidePanel.getByTestId("computer-preview").hover(); - const openComputer = sidePanel.getByTestId("computer-preview-open"); + const openComputer = sidePanel.getByTestId("computer-open"); await expect(openComputer).toBeVisible({ timeout: 30_000 }); await openComputer.click(); await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); @@ -140,6 +138,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te ); await page.getByTitle("Agent computer").click(); await routinesListed; + await sidePanel.locator('[data-tab="overview"]').click(); await expect( sidePanel.getByTestId("routine-list-row").getByText("Monday briefing"), ).toBeVisible(); @@ -316,7 +315,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te await page.getByTitle("Agent computer").click(); } await expect(sidePanel).toHaveAttribute("data-panel", "computer"); - const gear = page.getByRole("button", { name: "Show settings" }); + const gear = page.getByRole("button", { name: "Show settings" }).first(); await gear.click(); const downloadPromise = page.waitForEvent("download"); await page.getByRole("button", { name: "Export" }).click(); diff --git a/apps/web/e2e/group-chats.spec.ts b/apps/web/e2e/group-chats.spec.ts index 762432a..30f23bf 100644 --- a/apps/web/e2e/group-chats.spec.ts +++ b/apps/web/e2e/group-chats.spec.ts @@ -63,6 +63,10 @@ test("create group from + and see two bots in one transcript", async ({ page }, const researcherId = await createBot(page, "Researcher"); const writerId = await createBot(page, "Research Writer"); + // Replies read themselves aloud by default; keep that off so only the Speak click below talks. + for (const botId of [researcherId, writerId]) { + await rpc(page, "bots/update", { botId, autoSpeak: false }); + } await openNewGroup(page); await page.locator("label:has-text('Name') input").fill("Draft team"); @@ -169,18 +173,26 @@ test("create group from + and see two bots in one transcript", async ({ page }, await expect(desktopSettings.getByRole("button", { name: "Save", exact: true })).toBeEnabled(); await page.unroute("**/rpc/groups/update"); await desktopSettings.getByRole("button", { name: "Save", exact: true }).click(); + // The panel closes once the save lands; switching chats before that races the layout change. + await expect(desktopSettings).toHaveAttribute("data-panel", "closed"); await page.getByRole("combobox", { name: "Message" }).fill("@Researcher unfinished draft"); await projekte .getByRole("button", { name: /^Review team/ }) .first() .click(); + await expect(page).toHaveURL(new RegExp(`/app/g/${reviewGroup.id}`)); await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(""); + // Each chat keeps its own unsent draft. await projekte .getByRole("button", { name: /^Draft team/ }) .first() .click(); - await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(""); + await expect(page).toHaveURL(new RegExp(`/app/g/${draftGroupId}`)); + await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue( + "@Researcher unfinished draft", + ); + await page.getByRole("combobox", { name: "Message" }).fill(""); const composer = page.getByRole("combobox", { name: "Message" }); await composer.fill("@Res"); diff --git a/apps/web/e2e/helpers.ts b/apps/web/e2e/helpers.ts index 4769201..3816781 100644 --- a/apps/web/e2e/helpers.ts +++ b/apps/web/e2e/helpers.ts @@ -61,8 +61,12 @@ export async function createTestRoutine( }); } -export async function openAgentComputerPanel(page: Page) { - await page.getByTitle("Agent computer").click(); +/** Opens the side panel on the agent's overview, where its routines are listed. */ +export async function openAgentOverview(page: Page, title = "Agent computer") { + const overview = page.getByTestId("side-panel").locator('[data-tab="overview"]'); + // The header button toggles the panel, so only press it while the panel is closed. + if (!(await overview.isVisible())) await page.getByTitle(title).click(); + await overview.click(); } export async function rpc(page: Page, procedure: string, body: unknown): Promise { diff --git a/apps/web/e2e/heute-brief.spec.ts b/apps/web/e2e/heute-brief.spec.ts index 4caac78..2035f54 100644 --- a/apps/web/e2e/heute-brief.spec.ts +++ b/apps/web/e2e/heute-brief.spec.ts @@ -1,7 +1,14 @@ import type { Page } from "@playwright/test"; import { expect, test } from "@playwright/test"; import type { CoachRhythm, CoachToday, Goal, Habit, Routine } from "@rakazo/contracts"; -import { captureScreenshot, completeOnboarding, openMainBotThread, rpc, signup } from "./helpers"; +import { + captureScreenshot, + completeOnboarding, + openAgentOverview, + openMainBotThread, + rpc, + signup, +} from "./helpers"; /** The nav button reads "Today" or "Today, N need you". */ function openHeute(page: Page) { @@ -142,7 +149,7 @@ test("Heute shows the coach's brief, habits and goals, checks habits off and set // The main bot's routine list names the check-ins by their key, in the UI language. await openMainBotThread(page); - await page.getByTitle("Agent computer").click(); + await openAgentOverview(page); await expect(page.getByText("Morning brief", { exact: true })).toBeVisible(); await expect(page.getByText("Weekly review", { exact: true })).toBeVisible(); }); diff --git a/apps/web/e2e/in-app-browser.spec.ts b/apps/web/e2e/in-app-browser.spec.ts index e8f4c98..b2d62fb 100644 --- a/apps/web/e2e/in-app-browser.spec.ts +++ b/apps/web/e2e/in-app-browser.spec.ts @@ -15,7 +15,7 @@ test("the in-app browser previews a page and follows it back to the browser", as }), ); - await page.getByRole("button", { name: "Browser" }).click(); + await page.getByTitle("Agent computer").click(); const browser = page.getByTestId("in-app-browser"); await browser.getByTestId("in-app-browser-mode-local").click(); const address = browser.getByRole("textbox", { name: "Address" }); @@ -39,10 +39,10 @@ test("the in-app browser previews a page and follows it back to the browser", as await browser.getByRole("button", { name: "Open in browser" }).click(); expect((await popup).url()).toBe("https://preview.example/app"); - await browser.getByRole("button", { name: "Close panel" }).click(); + await page.getByTestId("side-panel").getByRole("button", { name: "Close panel" }).click(); await expect(browser).toHaveCount(0); // The last address comes back when the browser reopens. - await page.getByRole("button", { name: "Browser" }).click(); + await page.getByTitle("Agent computer").click(); await page.getByTestId("in-app-browser-mode-local").click(); await expect(page.getByTestId("in-app-browser-frame")).toHaveAttribute( "src", @@ -113,7 +113,7 @@ test("on desktop the page is a native view glued to the panel", async ({ page }) () => (window as unknown as { __browserCalls: Array<[string, unknown?]> }).__browserCalls, ); - await page.getByRole("button", { name: "Browser" }).click(); + await page.getByTitle("Agent computer").click(); const browser = page.getByTestId("in-app-browser"); await browser.getByTestId("in-app-browser-mode-local").click(); await browser.getByRole("textbox", { name: "Address" }).fill("localhost:5173"); @@ -157,6 +157,6 @@ test("on desktop the page is a native view glued to the panel", async ({ page }) await page.keyboard.press("Escape"); await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("show"); - await browser.getByRole("button", { name: "Close panel" }).click(); + await page.getByTestId("side-panel").getByRole("button", { name: "Close panel" }).click(); await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("hide"); }); diff --git a/apps/web/e2e/main-conversation.spec.ts b/apps/web/e2e/main-conversation.spec.ts index 1f68767..23ac2e8 100644 --- a/apps/web/e2e/main-conversation.spec.ts +++ b/apps/web/e2e/main-conversation.spec.ts @@ -26,7 +26,8 @@ test("onboarding lands in the main conversation, its lead answers, and addressed await expect(mainEntry).toBeVisible(); // The main bot is reached through the main conversation, not listed again. await expect(sidebar.locator(`[data-roster-bot-id="${chief.id}"]`)).toHaveCount(0); - await expect(page.getByTestId("oo-group-stage")).toBeVisible(); + await expect(page.getByTestId("oo-group-stage")).toHaveCount(0); + await expect(page.getByTestId("bot-settings-trigger")).toBeVisible(); await captureScreenshot(page, testInfo, "main-conversation-landing"); const snapshot = () => rpc(page, "threads/get", { groupId }); diff --git a/apps/web/e2e/media-pipeline.spec.ts b/apps/web/e2e/media-pipeline.spec.ts index eefc3a4..03dd28d 100644 --- a/apps/web/e2e/media-pipeline.spec.ts +++ b/apps/web/e2e/media-pipeline.spec.ts @@ -73,7 +73,7 @@ test("an image in the main conversation opens and downloads from the artifact ro await expect(thumbnail).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/); await thumbnail.click(); - const dialog = page.getByRole("dialog", { name: "pixel.png" }); + const dialog = page.getByRole("dialog", { name: "Pixel" }); const download = dialog.getByRole("link", { name: "Download pixel.png" }); await expect(download).toHaveAttribute("href", /\/api\/artifacts\/[^/?]+\?download=1$/); const saved = page.waitForEvent("download"); diff --git a/apps/web/e2e/message-hover-actions.spec.ts b/apps/web/e2e/message-hover-actions.spec.ts index 37c49f5..e4be06d 100644 --- a/apps/web/e2e/message-hover-actions.spec.ts +++ b/apps/web/e2e/message-hover-actions.spec.ts @@ -92,7 +92,13 @@ test("message hover shows below-bubble actions; reply links to parent", async ({ ); const botText = page.getByText(/i handled: hover-bot-/); await expect(transcript.getByText(/i handled: hover-bot-/)).toBeVisible({ timeout: 20_000 }); - const botRow = transcript.locator(`[data-message-id]`).filter({ has: botText }).first(); + // Pin the row by id: once the run settles, long replies fold their details away. + const botMessageId = await transcript + .locator(`[data-message-id]`) + .filter({ has: botText }) + .first() + .getAttribute("data-message-id"); + const botRow = transcript.locator(`[data-message-id="${botMessageId}"]`); await expect(botRow).toBeVisible(); await expectRailAtRest(page, botRow); await captureScreenshot(page, testInfo, "message-actions-rest-desktop"); diff --git a/apps/web/e2e/oo-computer-browser.spec.ts b/apps/web/e2e/oo-computer-browser.spec.ts index 949b7c4..e4e7c5d 100644 --- a/apps/web/e2e/oo-computer-browser.spec.ts +++ b/apps/web/e2e/oo-computer-browser.spec.ts @@ -69,7 +69,7 @@ test("Oo browser shows live screen, cursor, cowork click, and user context", asy }); }); - await page.getByRole("button", { name: "Browser", exact: true }).click(); + await page.getByTitle("Agent computer").click(); const browser = page.getByTestId("in-app-browser"); await expect(browser.getByTestId("oo-computer-browser-screen")).toHaveAttribute( "src", diff --git a/apps/web/e2e/page-browser.spec.ts b/apps/web/e2e/page-browser.spec.ts index 2ffb90c..7c869ec 100644 --- a/apps/web/e2e/page-browser.spec.ts +++ b/apps/web/e2e/page-browser.spec.ts @@ -18,7 +18,8 @@ test("live page helper preserves identity, masks passwords, and reports partial res.setHeader("content-type", "text/html"); res.end(`Fixture

Readable article content

- + + `); }); @@ -83,9 +84,18 @@ test("live page helper preserves identity, masks passwords, and reports partial const refreshed = await command("snapshot"); expect(ref(refreshed, "Replacement")).not.toBe(ref(stale, "Save")); expect( - await command("act", { actions: [{ kind: "click", ref: ref(refreshed, "Agree") }] }), - ).toMatchObject({ ok: true, completed: 1 }); + await command("act", { + actions: [ + { kind: "click", ref: ref(refreshed, "Agree") }, + { kind: "select", ref: ref(refreshed, "Plan"), text: "Pro Plan" }, + { kind: "press", ref: ref(refreshed, "Name"), key: "Enter" }, + { kind: "scroll", direction: "down", amount: 2 }, + { kind: "wait", ms: 10 }, + ], + }), + ).toMatchObject({ ok: true, completed: 5 }); await expect(page.getByRole("checkbox")).toBeChecked(); + await expect(page.locator("#plan")).toHaveValue("pro"); expect( await command("navigate", { url: `http://127.0.0.1:${address.port}/next` }), ).toMatchObject({ ok: true, title: "Fixture" }); diff --git a/apps/web/e2e/routine-crud.spec.ts b/apps/web/e2e/routine-crud.spec.ts index e4c138b..f73451c 100644 --- a/apps/web/e2e/routine-crud.spec.ts +++ b/apps/web/e2e/routine-crud.spec.ts @@ -6,7 +6,7 @@ import { completeOnboarding, createNamedBot, createTestRoutine, - openAgentComputerPanel, + openAgentOverview, rpc, signup, } from "./helpers"; @@ -33,7 +33,7 @@ test("routine active switch keeps its thumb inside the track", async ({ page }, await completeOnboarding(page); const botId = activeBotId(page); await createTestRoutine(page, botId, { name: "Toggle styling" }); - await openAgentComputerPanel(page); + await openAgentOverview(page); await page.getByRole("button", { name: /Toggle styling/ }).click(); const toggle = page.getByRole("switch", { name: "Active" }); @@ -89,7 +89,7 @@ test("routine editing updates in place, preserves timezone, and deletion persist expect(created.nextRunAt).not.toBeNull(); expect(localSchedule(created.nextRunAt!, created.timezone)).toMatchObject({ hour: 9, minute: 0 }); await page.reload(); - await page.getByTitle("Agent computer").click(); + await openAgentOverview(page); await page.getByRole("button", { name: /Tokyo check-in/ }).click(); await page.locator("label:has-text('Name') input").fill("Weekday check-in"); @@ -133,7 +133,7 @@ test("routine editing updates in place, preserves timezone, and deletion persist expect(await rpc(page, "routines/list", { botId })).toEqual([]); await page.reload(); - await page.getByTitle("Agent computer").click(); + await openAgentOverview(page); await expect(updatedButton).toHaveCount(0); }); @@ -172,7 +172,7 @@ test("a successful routine create is not reported as failed when refresh fails", name: "Persisted routine", prompt: "Run once each morning", }); - await openAgentComputerPanel(page); + await openAgentOverview(page); await page.getByRole("button", { name: /Persisted routine/ }).click(); await page .locator("label:has-text('Instruction') textarea") @@ -240,7 +240,7 @@ test("switching bots while a routine save is pending does not reopen stale state ]); await page.reload(); - await page.getByTitle("Agent computer").click(); + await openAgentOverview(page); await page.getByRole("button", { name: /First routine/ }).click(); await page.locator("label:has-text('Name') input").fill("First routine updated"); @@ -280,7 +280,7 @@ test("switching bots while a routine save is pending does not reopen stale state await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed"); await expect(page.getByRole("alert")).toHaveCount(0); - await page.getByTitle("Agent computer").click(); + await openAgentOverview(page); await expect(page.getByRole("button", { name: /Second routine/ })).toHaveCount(1); await expect(page.getByRole("button", { name: /First routine/ })).toHaveCount(0); diff --git a/apps/web/e2e/routine-execution.spec.ts b/apps/web/e2e/routine-execution.spec.ts index fa3dcf5..18c5e92 100644 --- a/apps/web/e2e/routine-execution.spec.ts +++ b/apps/web/e2e/routine-execution.spec.ts @@ -5,7 +5,7 @@ import { captureScreenshot, completeOnboarding, createTestRoutine, - openAgentComputerPanel, + openAgentOverview, rpc, signup, } from "./helpers"; @@ -38,7 +38,7 @@ test("Slack message trigger uses the mounted messaging provider and persists", a githubEnabled: false, messageProvider: "slack", }); - await openAgentComputerPanel(page); + await openAgentOverview(page); const panel = page.getByTestId("side-panel"); await expect(panel.getByText("Slack message", { exact: true })).toBeVisible(); const [routine] = await rpc(page, "routines/list", { botId }); @@ -72,7 +72,7 @@ test("GitHub event trigger exposes signed delivery settings and persists", async githubEnabled: true, messageProvider: null, }); - await openAgentComputerPanel(page); + await openAgentOverview(page); await page.getByRole("button", { name: /Review repository events/ }).click(); await expect( page.getByTestId("side-panel").getByText("Git event", { exact: true }), @@ -111,7 +111,7 @@ test("Korean webhook routine keeps technical field labels in English", async ({ crons: [], webhookEnabled: true, }); - await page.getByTitle("Agent 컴퓨터").click(); + await openAgentOverview(page, "Agent 컴퓨터"); await page.getByRole("button", { name: /한국어 웹훅 확인/ }).click(); await expect(page.getByText("웹훅이 실행될 때", { exact: true })).toBeVisible(); @@ -127,7 +127,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo await completeOnboarding(page); const botId = activeBotId(page); - await openAgentComputerPanel(page); + await openAgentOverview(page); await expect(page.getByRole("button", { name: "Test run" })).toHaveCount(0); await createTestRoutine(page, botId, { name: "Daily verification", @@ -135,7 +135,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo crons: ["0 9 * * 1-5"], }); await page.reload(); - await openAgentComputerPanel(page); + await openAgentOverview(page); const routine = page.getByRole("button", { name: /Daily verification/ }); await expect(routine).toContainText("Weekdays at 9:00 AM"); await captureScreenshot(page, testInfo, "33-routine-scheduled"); @@ -148,7 +148,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo await page.reload(); await expect(page.getByText(/routine-run-now-ok/i).first()).toBeVisible(); - await page.getByTitle("Agent computer").click(); + await openAgentOverview(page); await expect(page.getByRole("button", { name: /Daily verification/ })).toContainText( "Weekdays at 9:00 AM", ); diff --git a/apps/web/e2e/routine-list-toggle.spec.ts b/apps/web/e2e/routine-list-toggle.spec.ts index 244071c..66d256b 100644 --- a/apps/web/e2e/routine-list-toggle.spec.ts +++ b/apps/web/e2e/routine-list-toggle.spec.ts @@ -4,7 +4,7 @@ import { activeBotId, completeOnboarding, createTestRoutine, - openAgentComputerPanel, + openAgentOverview, rpc, signup, } from "./helpers"; @@ -17,14 +17,14 @@ test("routines section is hidden until the Oo has one, then list toggle persists await completeOnboarding(page); const botId = activeBotId(page); - await openAgentComputerPanel(page); + await openAgentOverview(page); const panel = page.getByTestId("side-panel"); await expect(panel.getByText("Routines", { exact: true })).toHaveCount(0); await expect(page.getByTestId("routine-create-button")).toHaveCount(0); await createTestRoutine(page, botId, { name: "Morning check-in" }); await page.reload(); - await openAgentComputerPanel(page); + await openAgentOverview(page); await expect(panel.getByText("Routines", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: /Morning check-in/ })).toBeVisible(); diff --git a/apps/web/e2e/teach-task.spec.ts b/apps/web/e2e/teach-task.spec.ts index 835d31c..5436989 100644 --- a/apps/web/e2e/teach-task.spec.ts +++ b/apps/web/e2e/teach-task.spec.ts @@ -16,18 +16,16 @@ test("teach a task records interaction and saves a draft", async ({ page }, test await expect(sidePanel.getByRole("button", { name: "Update computer" })).toHaveCount(0); await expect(sidePanel.getByRole("button", { name: "Take control" })).toHaveCount(0); await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0); - await expect(sidePanel.getByTestId("computer-preview")).toBeVisible(); + await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible(); // Showing the panel never boots the computer; its Open action is visible without hover. - const openButton = sidePanel.getByTestId("computer-preview-open"); + const openButton = sidePanel.getByTestId("computer-open"); await expect(openButton).toBeVisible(); const botId = activeBotId(page); expect((await rpc<{ state: string }>(page, "computer/status", { botId })).state).toBe("stopped"); await captureScreenshot(page, testInfo, "teach-sidepanel-simple"); - const preview = sidePanel.getByTestId("computer-preview"); - await preview.hover(); + const preview = sidePanel.getByTestId("oo-computer-browser-viewport"); await expect(openButton).toBeVisible(); - await expect(openButton.getByText("Open", { exact: true })).toBeVisible(); await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover"); await openButton.click(); diff --git a/apps/web/e2e/teaching-composer.spec.ts b/apps/web/e2e/teaching-composer.spec.ts index a6f0f27..a49f007 100644 --- a/apps/web/e2e/teaching-composer.spec.ts +++ b/apps/web/e2e/teaching-composer.spec.ts @@ -10,7 +10,7 @@ test("blocks the composer while a skill is being taught", async ({ page }) => { await page.getByTitle("Agent computer").click(); const sidePanel = page.getByTestId("side-panel"); await expect(sidePanel).toHaveAttribute("data-panel", "computer"); - const openButton = sidePanel.getByTestId("computer-preview-open"); + const openButton = sidePanel.getByTestId("computer-open"); await expect(openButton).toBeVisible(); await openButton.click(); const chrome = page.getByTestId("computer-chrome"); diff --git a/apps/web/e2e/team-computer.spec.ts b/apps/web/e2e/team-computer.spec.ts index f0413f8..7ad40df 100644 --- a/apps/web/e2e/team-computer.spec.ts +++ b/apps/web/e2e/team-computer.spec.ts @@ -51,7 +51,7 @@ test("Team Computer gives bots a home folder plus shared space while Private sta const privateId = await createBot(page, "Private Writer", "dedicated"); await openComputerPanel(page); - await expect(page.getByText(/Private Writer['’]s screen/).last()).toBeVisible(); + await expect(page.getByText(/Private Writer['’]s browser/).last()).toBeVisible(); await captureScreenshot(page, testInfo, "43-private-computer"); await expect(readFileResponse(page, privateId, "notes/result.txt")).resolves.toMatchObject({ ok: false, @@ -94,8 +94,7 @@ test("user control leaves another Team bot's screen available", async ({ page }, // The sidebar entry for the main bot opens the main conversation; its computer is in the 1:1. await openMainBotThread(page); await page.getByTitle("Agent computer").click(); - await page.getByTestId("computer-preview").hover(); - await page.getByTestId("computer-preview-open").click(); + await page.getByTestId("computer-open").click(); await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); await page.getByRole("button", { name: "Close computer" }).click(); await expect(page.getByRole("button", { name: "Close computer" })).toHaveCount(0); @@ -132,8 +131,7 @@ test("a failed control release keeps the computer open for retry", async ({ page await signup(page, `team-release-${Date.now()}@rakazo.test`, "password12", "Team Release"); await completeOnboarding(page); await page.getByTitle("Agent computer").click(); - await page.getByTestId("computer-preview").hover(); - await page.getByTestId("computer-preview-open").click(); + await page.getByTestId("computer-open").click(); const chrome = page.getByTestId("computer-chrome"); const release = chrome.getByRole("button", { name: "Release", exact: true }); await expect(release).toBeVisible(); @@ -192,10 +190,8 @@ test("an active Team bot must be stopped before user takeover", async ({ page }, await page.getByTitle("Agent computer").click(); const sidePanel = page.getByTestId("side-panel"); await expect(sidePanel.getByRole("button", { name: /Take control/i })).toHaveCount(0); - await page.getByTestId("computer-preview").hover(); - const openBusy = sidePanel.getByTestId("computer-preview-open"); + const openBusy = sidePanel.getByTestId("computer-open"); await expect(openBusy).toBeVisible(); - await expect(openBusy.getByText("Open", { exact: true })).toBeVisible(); await openBusy.click(); const chrome = page.getByTestId("computer-chrome"); await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); @@ -221,8 +217,7 @@ test("an active Team bot must be stopped before user takeover", async ({ page }, .toBeNull(); // After stop, Open via hover is the takeover path (no Take control button). - await page.getByTestId("computer-preview").hover(); - await page.getByTestId("computer-preview-open").click(); + await page.getByTestId("computer-open").click(); await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); await expect(chrome.getByText("You have control", { exact: true })).toBeVisible(); await expect(chrome.getByRole("button", { name: /Take control/i })).toHaveCount(0); @@ -287,8 +282,8 @@ async function expectComposerReady(page: Page) { async function openComputerPanel(page: Page) { await page.getByTitle("Agent computer").click(); - await expect(page.getByTestId("computer-preview")).toBeVisible(); - await expect(page.getByTestId("computer-preview-open")).toHaveCount(1); + await expect(page.getByTestId("oo-computer-browser-viewport")).toBeVisible(); + await expect(page.getByTestId("computer-open")).toHaveCount(1); } async function sendAndWait(page: Page, botId: string, text: string) { diff --git a/apps/web/e2e/unterhaltungen-sidebar.spec.ts b/apps/web/e2e/unterhaltungen-sidebar.spec.ts index b3a6713..e9e7a59 100644 --- a/apps/web/e2e/unterhaltungen-sidebar.spec.ts +++ b/apps/web/e2e/unterhaltungen-sidebar.spec.ts @@ -14,6 +14,13 @@ test("conversations and projects stay separate; + opens bot picker", async ({ pa await expect(unterhaltungen.getByText("Talk to your Oos", { exact: true })).toBeVisible(); await expect(projekte.getByText("Projects", { exact: true })).toBeVisible(); await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toBeVisible(); + const [projekteBox, unterhaltungenBox] = await Promise.all([ + projekte.boundingBox(), + unterhaltungen.boundingBox(), + ]); + expect(projekteBox).not.toBeNull(); + expect(unterhaltungenBox).not.toBeNull(); + expect(projekteBox!.y).toBeLessThan(unterhaltungenBox!.y); await unterhaltungen.getByTestId("conversation-picker-trigger").click(); const picker = page.getByTestId("conversation-bot-picker"); @@ -40,14 +47,14 @@ test("conversations and projects stay separate; + opens bot picker", async ({ pa await expect(projekte.getByText("Side project", { exact: true })).toBeVisible(); await expect(unterhaltungenOnly(page).getByText("Side project", { exact: true })).toHaveCount(0); - // The main agent leads the picker, even after other bots exist. + // The new project's main agent leads the picker, even after other bots exist. await unterhaltungen.getByTestId("conversation-picker-trigger").click(); await expect( page .getByTestId("conversation-bot-picker") .locator("[data-testid^='conversation-picker-bot-']") .first(), - ).toHaveText("Chief"); + ).toHaveText("Projektmanager"); await page.keyboard.press("Escape"); // Creating a project that already exists opens it instead of listing it twice. diff --git a/apps/web/e2e/voice.spec.ts b/apps/web/e2e/voice.spec.ts index 9005470..0458bf6 100644 --- a/apps/web/e2e/voice.spec.ts +++ b/apps/web/e2e/voice.spec.ts @@ -7,7 +7,15 @@ async function turnOnSpeaking(page: Page) { await expect(voice).toBeVisible(); await expect(voice.getByRole("button", { name: "Other voice providers" })).toHaveCount(0); await expect(voice.getByLabel("API key", { exact: true })).toHaveCount(0); - await voice.getByRole("switch", { name: "Speaking on or off" }).click(); + // The app turns the local voice on by itself the first time it starts, and the switch stays + // disabled while that runs; only click once it settled off. + const speaking = voice.getByRole("switch", { name: "Speaking on or off" }); + await expect(async () => { + if ((await speaking.getAttribute("aria-checked")) !== "true" && (await speaking.isEnabled())) { + await speaking.click(); + } + await expect(speaking).toHaveAttribute("aria-checked", "true", { timeout: 3_000 }); + }).toPass({ timeout: 30_000 }); await expect(voice.getByTestId("voice-carousel")).toBeVisible(); return voice; } @@ -25,11 +33,6 @@ test("voice settings pick a voice from the carousel, speak a reply, and open a c ).toHaveCount(1); await captureScreenshot(page, testInfo, "voice-composer"); - const preparedOff = await rpc<{ ready: boolean }>(page, "voice/prepare", { - text: "Hello there.", - }); - expect(preparedOff.ready).toBe(false); - const voice = await turnOnSpeaking(page); const cards = voice.getByTestId("voice-card"); // English UI lists only the English voices of the local server. diff --git a/apps/web/index.html b/apps/web/index.html index 2bd5e9f..c95b295 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -5,6 +5,9 @@ + + + diff --git a/apps/web/public/site.webmanifest b/apps/web/public/site.webmanifest index c57083f..9404728 100644 --- a/apps/web/public/site.webmanifest +++ b/apps/web/public/site.webmanifest @@ -1,13 +1,16 @@ { + "id": "/", "name": "evOo", "short_name": "evOo", "description": "Deine Oos — immer da, immer wach.", "start_url": "/", + "scope": "/", "display": "standalone", - "background_color": "#0f1218", - "theme_color": "#0f1218", + "orientation": "any", + "background_color": "#07080a", + "theme_color": "#07080a", "icons": [ - { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" } + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } ] } diff --git a/apps/web/public/sw.js b/apps/web/public/sw.js index 374172c..23efcd4 100644 --- a/apps/web/public/sw.js +++ b/apps/web/public/sw.js @@ -34,6 +34,7 @@ self.addEventListener("push", (event) => { // Same tag as the in-app notification, so one replaces the other. tag: typeof data.tag === "string" && data.tag ? data.tag : undefined, icon: "/icon-192.png", + badge: "/icon-192.png", data: { url: appPath(data.url) }, }, ); diff --git a/apps/web/src/components/ArtifactFileCard.tsx b/apps/web/src/components/ArtifactFileCard.tsx index 50b29ec..bdba834 100644 --- a/apps/web/src/components/ArtifactFileCard.tsx +++ b/apps/web/src/components/ArtifactFileCard.tsx @@ -1,8 +1,10 @@ import { Trans, useLingui } from "@lingui/react/macro"; import { ChatMarkdown } from "@rakazo/chat-ui/web"; +import { isPaneArtifactMimeType } from "@rakazo/core"; import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web"; -import { Download, FileText, X } from "lucide-react"; -import { type RefObject, useMemo, useRef, useState } from "react"; +import { Download, Eye, FileText, X } from "lucide-react"; +import type { RefObject } from "react"; +import { useMemo, useRef, useState } from "react"; import { useArtifactBytes } from "../hooks/useArtifactBytes"; import { downloadArtifact } from "../lib/artifact-open"; import { formatBytes } from "../lib/format-bytes"; @@ -16,80 +18,104 @@ type ArtifactFileCardProps = { onOpenBeside?: () => void; }; +const INLINE_DIALOG_TEXT_MIME_TYPES = new Set(["text/markdown", "text/plain", "application/json"]); + +function shortFileTypeLabel(name: string, mimeType: string): string { + const dot = name.lastIndexOf("."); + if (dot > 0 && dot < name.length - 1) { + const ext = name + .slice(dot + 1) + .trim() + .toUpperCase(); + if (ext.length <= 6) return ext; + } + const sub = mimeType.split("/")[1]?.split("+")[0]?.toUpperCase(); + return sub && sub.length <= 8 ? sub : "FILE"; +} + export function ArtifactFileCard(props: ArtifactFileCardProps) { const { t } = useLingui(); - const markdown = props.mimeType === "text/markdown"; + const canOpenBeside = Boolean(props.onOpenBeside) && isPaneArtifactMimeType(props.mimeType); + const canOpenDialog = INLINE_DIALOG_TEXT_MIME_TYPES.has(props.mimeType); + const canPreview = canOpenBeside || canOpenDialog; const previewButton = useRef(null); const closeButton = useRef(null); const [previewOpen, setPreviewOpen] = useState(false); const startDownload = () => downloadArtifact(props.artifactId, props.name); - - if (!markdown) { - return ( -
- -
- ); - } + const badgeLabel = shortFileTypeLabel(props.name, props.mimeType); return ( <> -
-
+
+
- - - - - + {canOpenDialog ? ( + + + + + + ) : null} ); } -function MarkdownPreview({ +function TextFilePreview({ artifactId, name, + mimeType, closeButtonRef, }: ArtifactFileCardProps & { closeButtonRef: RefObject }) { const { t } = useLingui(); @@ -97,8 +123,8 @@ function MarkdownPreview({ const state = useMemo(() => { if (loaded.status !== "ready") return loaded; try { - const markdown = new TextDecoder("utf-8", { fatal: true }).decode(loaded.bytes); - return { status: "ready" as const, markdown }; + const text = new TextDecoder("utf-8", { fatal: true }).decode(loaded.bytes); + return { status: "ready" as const, text }; } catch { return { status: "error" as const, message: t`This file is not valid UTF-8 Markdown.` }; } @@ -140,8 +166,12 @@ function MarkdownPreview({
{state.message}
+ ) : mimeType === "text/markdown" ? ( + {state.text} ) : ( - {state.markdown} +
+              {state.text}
+            
)}
diff --git a/apps/web/src/components/AskCard.test.tsx b/apps/web/src/components/AskCard.test.tsx index fb657f3..aff88de 100644 --- a/apps/web/src/components/AskCard.test.tsx +++ b/apps/web/src/components/AskCard.test.tsx @@ -1,17 +1,115 @@ -import { selectedAskActionLabel } from "@rakazo/core"; -import { describe, expect, it } from "vitest"; +// @vitest-environment jsdom -describe("selectedAskActionLabel", () => { - it("maps a choice answer id to its user-facing label", () => { - expect( - selectedAskActionLabel("choice-2", [ - { id: "choice-1", label: "Berlin" }, - { id: "choice-2", label: "Seoul" }, - ]), - ).toBe("Seoul"); - }); +import type { ReactNode } from "react"; +import { act } from "react"; +import type { Root } from "react-dom/client"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; - it("falls back to the answer when an action is unavailable", () => { - expect(selectedAskActionLabel("custom", undefined)).toBe("custom"); - }); +vi.mock("@lingui/core/macro", () => ({ + t: (parts: TemplateStringsArray, ...values: unknown[]) => + parts.reduce((acc, part, index) => acc + part + String(values[index] ?? ""), ""), +})); + +vi.mock("@lingui/react/macro", () => { + const t = (parts: TemplateStringsArray, ...values: unknown[]) => + parts.reduce((acc, part, index) => acc + part + String(values[index] ?? ""), ""); + return { useLingui: () => ({ t }), Trans: ({ children }: { children: ReactNode }) => children }; +}); + +vi.mock("@rakazo/chat-ui/web", () => ({ + ChatMarkdown: ({ children }: { children: ReactNode }) =>
{children}
, +})); + +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(); + }); + + 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(); + }); + + 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"); + } }); diff --git a/apps/web/src/components/AskCard.tsx b/apps/web/src/components/AskCard.tsx index 4bd4ceb..68937e2 100644 --- a/apps/web/src/components/AskCard.tsx +++ b/apps/web/src/components/AskCard.tsx @@ -174,34 +174,47 @@ export function AskCard({ ) : askActions?.length ? (
- {askActions.map((action) => ( - - ))} + disabled={submitting} + onClick={() => void submitAnswer(action.id)} + > + + {pendingAction === action.id ? ( + Sending… + ) : approvalActions ? ( + approvalActionLabel(action.id, action.label, action.outcome) + ) : ( + action.label + )} + + + + ); + })} {!approvalActions ? (
)} - {error ?

{error}

: null} + {error ? ( +

+ {error} +

+ ) : null}
); } diff --git a/apps/web/src/components/CloudAgentCard.tsx b/apps/web/src/components/CloudAgentCard.tsx index fefc6be..e5f24d3 100644 --- a/apps/web/src/components/CloudAgentCard.tsx +++ b/apps/web/src/components/CloudAgentCard.tsx @@ -3,6 +3,7 @@ import type { MessageBlock } from "@rakazo/contracts"; import { cloudAgentHttpsUrl } from "@rakazo/core"; import { Badge } from "@rakazo/ui-web/components/ui/badge"; import { Card, CardContent } from "@rakazo/ui-web/components/ui/card"; +import { ArrowUpRight } from "lucide-react"; export function CloudAgentCard({ block, @@ -15,8 +16,14 @@ export function CloudAgentCard({
- + {block.title} + {href ? ( + ); return href ? ( - + {content} ) : ( diff --git a/apps/web/src/components/DualSurfaceDivider.tsx b/apps/web/src/components/DualSurfaceDivider.tsx index d4b4869..718188e 100644 --- a/apps/web/src/components/DualSurfaceDivider.tsx +++ b/apps/web/src/components/DualSurfaceDivider.tsx @@ -74,7 +74,7 @@ export function DualSurfaceDivider({ onDoubleClick={handleDoubleClick} onKeyDown={handleKeyDown} className={cn( - "group relative flex w-3 md:w-4 shrink-0 cursor-col-resize select-none touch-none items-center justify-center transition-colors bg-border/0 hover:bg-accent/40 active:bg-primary/20 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", + "group relative flex w-3 md:w-3 shrink-0 cursor-col-resize select-none touch-none items-center justify-center transition-colors bg-transparent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", disabled && "pointer-events-none cursor-default opacity-50", className, )} @@ -82,8 +82,8 @@ export function DualSurfaceDivider({ >
diff --git a/apps/web/src/components/NotificationSettingsPanel.tsx b/apps/web/src/components/NotificationSettingsPanel.tsx index 16bf3f8..0683223 100644 --- a/apps/web/src/components/NotificationSettingsPanel.tsx +++ b/apps/web/src/components/NotificationSettingsPanel.tsx @@ -4,7 +4,13 @@ import { Input, Switch } from "@rakazo/ui-web"; import type { ReactNode } from "react"; import { useEffect, useId, useRef, useState } from "react"; import { rpc } from "../lib/rpc"; -import { disableWebPush, enableWebPush, webPushEnabled, webPushSupported } from "../lib/web-push"; +import { + disableWebPush, + enableWebPush, + isIosBrowserNeedsHomeScreen, + webPushEnabled, + webPushSupported, +} from "../lib/web-push"; import { SettingsSection } from "./SettingsSection"; const DEFAULT_QUIET_HOURS = { start: "22:00", end: "07:00" }; @@ -51,6 +57,7 @@ function SettingRow({ export function NotificationSettingsPanel() { const { t } = useLingui(); const pushSupported = webPushSupported(); + const iosNeedsHomeScreen = isIosBrowserNeedsHomeScreen(); const [settings, setSettings] = useState(null); const [push, setPush] = useState("loading"); const [pushBusy, setPushBusy] = useState(false); @@ -153,6 +160,15 @@ export function NotificationSettingsPanel() {

) : null} + ) : iosNeedsHomeScreen ? ( +
  • +
    + Push on this device +
    +

    + Add evOo to your Home Screen via Share to enable push on iOS. +

    +
  • ) : null} Quiet hours} diff --git a/apps/web/src/components/OoAtelier.tsx b/apps/web/src/components/OoAtelier.tsx index 2aa2c8c..2fe801f 100644 --- a/apps/web/src/components/OoAtelier.tsx +++ b/apps/web/src/components/OoAtelier.tsx @@ -254,10 +254,10 @@ export function OoAtelierDialog({ - + Atelier @@ -274,19 +274,19 @@ export function OoAtelierDialog({ -
    +
    - - + + Body @@ -303,7 +303,11 @@ export function OoAtelierDialog({ ) : null} - + - + - + {allowUpload ? ( - + + {onOpenComputer && !unavailable && !computerScreenError ? ( + + ) : null}
    ["t"]): string { +function sourceLabel(id: DiscoverySourceId): ReactNode { switch (id) { case "profile-basics": - return t`Your profile`; + return Your profile; case "connected-tools": - return t`Connected tools`; + return Connected tools; case "specialist-crew": - return t`Specialist crew`; + return Specialist crew; default: { const never: never = id; return never; @@ -83,7 +83,7 @@ export function OnboardingDiscoveryShell({ > {active ? ( <> - {sourceLabel(active.id, t)} + {sourceLabel(active.id)} {active.status === "found" && active.detail === DISCOVERY_MAIN_BOT_ROLE ? ( <> {" · "} diff --git a/apps/web/src/components/onboarding/OnboardingOoDesigner.tsx b/apps/web/src/components/onboarding/OnboardingOoDesigner.tsx index a4003bb..3c40baa 100644 --- a/apps/web/src/components/onboarding/OnboardingOoDesigner.tsx +++ b/apps/web/src/components/onboarding/OnboardingOoDesigner.tsx @@ -120,7 +120,7 @@ export function OnboardingOoDesigner({ config, onChange, onContinue }: Onboardin What feels like you?

    diff --git a/apps/web/src/components/voice/VoiceLiveText.tsx b/apps/web/src/components/voice/VoiceLiveText.tsx index 1c0f262..bf69a03 100644 --- a/apps/web/src/components/voice/VoiceLiveText.tsx +++ b/apps/web/src/components/voice/VoiceLiveText.tsx @@ -17,7 +17,7 @@ export function VoiceLiveText({ text, reducedMotion }: { text: string; reducedMo return (

    {captionWords(text).map((word, index) => ( diff --git a/apps/web/src/components/voice/VoiceStage.tsx b/apps/web/src/components/voice/VoiceStage.tsx index 31250c9..f099fb0 100644 --- a/apps/web/src/components/voice/VoiceStage.tsx +++ b/apps/web/src/components/voice/VoiceStage.tsx @@ -90,21 +90,52 @@ export function VoiceStage({ }, []); return ( -

    {botName}

    +
    +
    + {transcriptLines.length ? ( +
      + {transcriptLines.map((line, index) => ( +
    • + + {line.role === "user" ? t`You` : (line.speakerName ?? botName)} + + + {line.text} + +
    • + ))} +
    + ) : ( +

    + Your conversation will appear here. +

    + )} +
    +
    + {awarenessLabel && awarenessEvent ? ( -
    +
    -
    -
    +
    +
    - - {error ? ( -

    - {error} -

    - ) : null} +
    + + {error ? ( +

    + {error} +

    + ) : null} +
    -
    +
    {phaseLabel} @@ -187,39 +220,6 @@ export function VoiceStage({
    - -
    -
    - {transcriptLines.length ? ( -
      - {transcriptLines.map((line, index) => ( -
    • - - {line.role === "user" ? t`You` : (line.speakerName ?? botName)} - - - {line.text} - -
    • - ))} -
    - ) : ( -

    - Your conversation will appear here. -

    - )} -
    -
    -
    + ); } diff --git a/apps/web/src/lib/chat-markdown.test.tsx b/apps/web/src/lib/chat-markdown.test.tsx index db3c0b2..27b9d76 100644 --- a/apps/web/src/lib/chat-markdown.test.tsx +++ b/apps/web/src/lib/chat-markdown.test.tsx @@ -42,4 +42,20 @@ describe("ChatMarkdown", () => { expect(html).toContain('aria-label="Copy code"'); expect(html).toContain("rk-chat-markdown-copy"); }); + + it("marks outbound links with target=_blank, external indicator, and renders inline hex color swatches", () => { + const html = renderToStaticMarkup( + + {"See [Preview](https://example.com/review) and palette `#6B4226` or `#E8722A`."} + , + ); + + 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"'); + }); }); diff --git a/apps/web/src/lib/dictation.ts b/apps/web/src/lib/dictation.ts index f66150a..38c92a6 100644 --- a/apps/web/src/lib/dictation.ts +++ b/apps/web/src/lib/dictation.ts @@ -9,6 +9,37 @@ import { StreamingVad, } from "@rakazo/core"; import { selectedSpaceId, withSpaceHeaders } from "./rpc.js"; +import { readUiLocale } from "./ui-locale.js"; + +const SPEECH_LOCALE_TAGS: Record = { + de: "de-DE", + en: "en-US", + es: "es-ES", + fr: "fr-FR", + hi: "hi-IN", + it: "it-IT", + ja: "ja-JP", + ko: "ko-KR", + "pt-BR": "pt-BR", + ru: "ru-RU", + tr: "tr-TR", + "zh-CN": "zh-CN", +}; + +export function speechRecognitionLang(): string { + const uiLocale = readUiLocale(); + const mapped = uiLocale ? SPEECH_LOCALE_TAGS[uiLocale] : undefined; + if (uiLocale && mapped) { + // Prefer browser's regional variant when it matches the UI language (e.g. de-AT / de-CH) + const navLang = typeof navigator !== "undefined" ? navigator.language : ""; + const primary = uiLocale.split("-")[0]?.toLowerCase() ?? ""; + if (navLang?.toLowerCase().startsWith(`${primary}-`)) { + return navLang; + } + return mapped; + } + return (typeof navigator !== "undefined" && navigator.language) || "de-DE"; +} export type DictationMode = "hold" | "endpoint"; @@ -205,7 +236,7 @@ export class Dictation { const rec = new Ctor(); rec.continuous = true; rec.interimResults = true; - rec.lang = navigator.language || "en-US"; + rec.lang = speechRecognitionLang(); rec.onresult = (event) => { if (this.token !== mine) return; let transcript = ""; diff --git a/apps/web/src/lib/heute-brief.test.ts b/apps/web/src/lib/heute-brief.test.ts index 1373105..14475ab 100644 --- a/apps/web/src/lib/heute-brief.test.ts +++ b/apps/web/src/lib/heute-brief.test.ts @@ -98,13 +98,25 @@ describe("isToday", () => { }); describe("buildHeuteBrief", () => { - it("collects needs-you, doing, and done buckets", () => { + it("collects needs-you, doing, and done buckets, ignoring passive unread chats that do not need the user", () => { const brief = buildHeuteBrief({ bots: [ - bot({ id: "bot-1", unread: true }), - bot({ id: "bot-2", unread: true, updatedAt: "2026-09-22T08:00:00.000Z", name: "Steve" }), + bot({ id: "bot-1", unread: true, status: "waiting_input" }), + bot({ + id: "bot-2", + unread: true, + status: "waiting_takeover", + updatedAt: "2026-09-22T08:00:00.000Z", + name: "Steve", + }), + bot({ + id: "bot-passive", + unread: true, + status: "idle", + name: "PassiveBot", + }), ], - groups: [group({ updatedAt: "2026-09-22T09:30:00.000Z" })], + groups: [group({ updatedAt: "2026-09-22T09:30:00.000Z", unread: true })], activeRuns: [ run({ runId: "run-1", botId: "bot-1" }), run({ runId: "run-3", botId: "bot-4", botName: "Worker", status: "running" }), @@ -157,7 +169,7 @@ describe("buildHeuteBrief", () => { it("caps needs-you at three items", () => { const bots = Array.from({ length: 5 }, (_, index) => - bot({ id: `bot-${index}`, unread: true, name: `Bot ${index}` }), + bot({ id: `bot-${index}`, unread: true, status: "waiting_input", name: `Bot ${index}` }), ); const brief = buildHeuteBrief({ bots, @@ -169,6 +181,57 @@ describe("buildHeuteBrief", () => { expect(brief.needsYou).toHaveLength(HEUTE_NEEDS_YOU_CAP); expect(brief.actionCount).toBe(5); }); + + it("merges and deduplicates actionable unread bots and groups across spaces", () => { + const brief = buildHeuteBrief({ + bots: [bot({ id: "bot-1", spaceId: "space-2", unread: false, status: "idle" })], + groups: [], + spaces: [ + { + id: "space-1", + bots: [ + bot({ + id: "bot-home-1", + spaceId: "space-1", + unread: true, + status: "waiting_input", + name: "Code Architect", + }), + bot({ + id: "bot-home-2", + spaceId: "space-1", + unread: true, + status: "waiting_takeover", + name: "UI Designer", + }), + ], + groups: [ + group({ + id: "group-home-1", + spaceId: "space-1", + unread: true, + leadBotId: "bot-home-1", + }), + ], + }, + { + id: "space-2", + // Current space's optimistic `bots` entry (unread: false) takes precedence over stale space snapshot + bots: [bot({ id: "bot-1", spaceId: "space-2", unread: true, status: "waiting_input" })], + groups: [], + }, + ], + activeRuns: [], + recentRuns: [], + now, + }); + expect(brief.actionCount).toBe(3); + expect(brief.needsYou.map((item) => (item.kind === "unread" ? item.chat.id : ""))).toEqual([ + "bot-home-1", + "bot-home-2", + "group-home-1", + ]); + }); }); function habit(overrides: Partial = {}): HabitToday { diff --git a/apps/web/src/lib/heute-brief.ts b/apps/web/src/lib/heute-brief.ts index cd9c1df..3a24c7e 100644 --- a/apps/web/src/lib/heute-brief.ts +++ b/apps/web/src/lib/heute-brief.ts @@ -1,5 +1,18 @@ -import type { Bot, CoachToday, Group, RunActivityRow } from "@rakazo/contracts"; -import { isActionableRun, isDoingStatus, isFailedRoutine } from "./agent-activity"; +import type { + Bot, + CoachToday, + Group, + RunActivityRow, + Space, + SpaceBot, + SpaceGroup, +} from "@rakazo/contracts"; +import { + isActionableRun, + isDoingStatus, + isFailedRoutine, + isNeedsYouStatus, +} from "./agent-activity"; export type JetztRunItem = { kind: "run"; @@ -9,7 +22,7 @@ export type JetztRunItem = { export type JetztUnreadItem = { kind: "unread"; chatKind: "bot" | "group"; - chat: Bot | Group; + chat: SpaceBot | (SpaceGroup & { leadBotId?: string | null }); }; export type JetztItem = JetztRunItem | JetztUnreadItem; @@ -76,16 +89,33 @@ function chatTargetKey(chatKind: "bot" | "group", chatId: string): string { } export function buildHeuteBrief(params: { - bots: Bot[]; - groups: Group[]; + bots: readonly SpaceBot[]; + groups: readonly (SpaceGroup & { leadBotId?: string | null })[]; + spaces?: readonly Pick[]; activeRuns: RunActivityRow[]; recentRuns: RunActivityRow[]; now?: Date; }): HeuteBriefData { - const { bots, groups, activeRuns, recentRuns, now = new Date() } = params; + const { bots, groups, spaces = [], activeRuns, recentRuns, now = new Date() } = params; const needsYouAll: JetztItem[] = []; const coveredTargets = new Set(); + const seenBotIds = new Set(); + 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(); + const allGroups: (SpaceGroup & { leadBotId?: string | null })[] = []; + for (const group of [...groups, ...spaces.flatMap((space) => space.groups)]) { + if (seenGroupIds.has(group.id)) continue; + seenGroupIds.add(group.id); + allGroups.push(group); + } + const actionableRuns = [ ...activeRuns.filter(isActionableRun), ...recentRuns.filter(isFailedRoutine), @@ -98,12 +128,20 @@ export function buildHeuteBrief(params: { coveredTargets.add(runTargetKey(run)); } + const botStatusById = new Map(allBots.map((bot) => [bot.id, bot.status])); const unreadChats: JetztUnreadItem[] = [ - ...bots - .filter((bot) => bot.unread) + ...allBots + .filter((bot) => bot.unread && isNeedsYouStatus(bot.status)) .map((bot) => ({ kind: "unread" as const, chatKind: "bot" as const, chat: bot })), - ...groups - .filter((group) => group.unread) + ...allGroups + .filter((group) => { + if (!group.unread) return false; + const memberBotIds = [ + ...(group.leadBotId ? [group.leadBotId] : []), + ...(group.members?.map((m) => m.botId) ?? []), + ]; + return memberBotIds.some((id) => isNeedsYouStatus(botStatusById.get(id))); + }) .map((group) => ({ kind: "unread" as const, chatKind: "group" as const, diff --git a/apps/web/src/lib/oo-audio-bus.ts b/apps/web/src/lib/oo-audio-bus.ts index a8967f2..003dade 100644 --- a/apps/web/src/lib/oo-audio-bus.ts +++ b/apps/web/src/lib/oo-audio-bus.ts @@ -5,8 +5,8 @@ type AudioContextCtor = new () => AudioContext; -/** Master output ~−15 LUFS for short procedural peaks. */ -const SFX_MASTER_GAIN = 0.58; +/** Master output ~−12 LUFS for clear, warm procedural peaks. */ +const SFX_MASTER_GAIN = 0.85; /** Duck depth when voice lane active (TTS or mic wash). */ const SFX_DUCK_RATIO = 0.28; const DUCK_RAMP_S = 0.065; @@ -22,8 +22,10 @@ class OoAudioBus { private ctx: AudioContext | null = null; private sfxGain: GainNode | null = null; private voiceHold = 0; + private unlockBound = false; ensureContext(): AudioContext | null { + this.bindGestureUnlock(); if (this.ctx && this.ctx.state !== "closed") return this.ctx; const Ctor = audioContextCtor(); if (!Ctor) return null; @@ -43,6 +45,34 @@ class OoAudioBus { } } + /** Unlocks Web Audio on the user's first click, key, or tap so async SSE events can play audio. */ + bindGestureUnlock(): void { + if ( + this.unlockBound || + typeof window === "undefined" || + typeof window.addEventListener !== "function" + ) { + return; + } + this.unlockBound = true; + const unlock = () => { + const ctx = this.ensureContext(); + if (ctx && ctx.state === "suspended") { + void ctx.resume().catch(() => undefined); + } + if (typeof window !== "undefined" && "speechSynthesis" in window) { + try { + window.speechSynthesis.getVoices?.(); + } catch { + // Ignore voice preload errors. + } + } + }; + window.addEventListener("pointerdown", unlock, { capture: true, passive: true }); + window.addEventListener("keydown", unlock, { capture: true, passive: true }); + window.addEventListener("touchstart", unlock, { capture: true, passive: true }); + } + /** Route for oo-sound / oo-vocals / oo-voice-mark oscillators. */ sfxDestination(ctx: AudioContext): AudioNode { if (ctx !== this.ctx) { diff --git a/apps/web/src/lib/oo-sound.ts b/apps/web/src/lib/oo-sound.ts index f798e9f..d39aec6 100644 --- a/apps/web/src/lib/oo-sound.ts +++ b/apps/web/src/lib/oo-sound.ts @@ -68,61 +68,62 @@ function playCueOnContext(ctx: AudioContext, cue: OoSoundCue) { switch (cue) { case "app-start": - sweep(ctx, master, 523.25, 659.25, t0, 0.14, { gain: 0.038, type: "sine" }); - sweep(ctx, master, 659.25, 493.88, t0 + 0.1, 0.18, { gain: 0.032, type: "triangle" }); + sweep(ctx, master, 523.25, 659.25, t0, 0.16, { gain: 0.11, type: "sine" }); + sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.2, { gain: 0.095, type: "triangle" }); break; case "interact": - tone(ctx, master, 1046.5, t0, 0.06, { - gain: 0.018, - attack: 0.003, - release: 0.04, + tone(ctx, master, 880, t0, 0.075, { + gain: 0.065, + attack: 0.004, + release: 0.045, type: "triangle", }); break; case "success-resolve": case "work-complete": // mint → gold resolve (~270 ms) - sweep(ctx, master, 523.25, 659.25, t0, 0.12, { gain: 0.042, type: "sine" }); - sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.22, { gain: 0.036, type: "triangle" }); - tone(ctx, master, 880, t0 + 0.2, 0.28, { gain: 0.03, type: "sine" }); + sweep(ctx, master, 523.25, 659.25, t0, 0.13, { gain: 0.12, type: "sine" }); + sweep(ctx, master, 659.25, 783.99, t0 + 0.1, 0.22, { gain: 0.105, type: "triangle" }); + tone(ctx, master, 880, t0 + 0.2, 0.28, { gain: 0.09, type: "sine" }); break; case "need-you": - sweep(ctx, master, 220, 330, t0, 0.1, { gain: 0.042, type: "triangle" }); - tone(ctx, master, 277.18, t0 + 0.09, 0.12, { gain: 0.035, release: 0.08 }); + sweep(ctx, master, 261.63, 392, t0, 0.12, { gain: 0.125, type: "triangle" }); + tone(ctx, master, 523.25, t0 + 0.1, 0.18, { gain: 0.11, release: 0.1 }); break; case "listening-start": case "voice_listen": - tone(ctx, master, 220, t0, 0.22, { gain: 0.045, type: "triangle" }); - tone(ctx, master, 330, t0 + 0.04, 0.28, { gain: 0.035 }); + tone(ctx, master, 293.66, t0, 0.2, { gain: 0.11, type: "triangle" }); + tone(ctx, master, 440, t0 + 0.05, 0.24, { gain: 0.095 }); break; case "listening-stop": - tone(ctx, master, 330, t0, 0.18, { gain: 0.03, release: 0.12 }); + tone(ctx, master, 392, t0, 0.16, { gain: 0.085, release: 0.1 }); + tone(ctx, master, 523.25, t0 + 0.06, 0.18, { gain: 0.075, release: 0.11 }); break; case "speaking-start": - tone(ctx, master, 440, t0, 0.2, { gain: 0.04, type: "sine" }); + sweep(ctx, master, 392, 523.25, t0, 0.14, { gain: 0.095, type: "sine" }); break; case "speaking-done": - tone(ctx, master, 523.25, t0, 0.24, { gain: 0.035 }); - tone(ctx, master, 392, t0 + 0.08, 0.3, { gain: 0.028, release: 0.14 }); + tone(ctx, master, 523.25, t0, 0.2, { gain: 0.085 }); + tone(ctx, master, 392, t0 + 0.08, 0.26, { gain: 0.075, release: 0.14 }); break; case "voice_bargein": - sweep(ctx, master, 523.25, 261.63, t0, 0.05, { gain: 0.032, type: "sine" }); + sweep(ctx, master, 523.25, 261.63, t0, 0.07, { gain: 0.09, type: "sine" }); break; case "attention": - tone(ctx, master, 277.18, t0, 0.16, { gain: 0.05, type: "triangle" }); - tone(ctx, master, 233.08, t0 + 0.12, 0.22, { gain: 0.045 }); + tone(ctx, master, 329.63, t0, 0.16, { gain: 0.13, type: "triangle" }); + tone(ctx, master, 261.63, t0 + 0.12, 0.22, { gain: 0.115 }); break; case "message-sent": - tone(ctx, master, 880, t0, 0.1, { gain: 0.022, attack: 0.004, release: 0.06 }); + sweep(ctx, master, 587.33, 880, t0, 0.11, { gain: 0.08, type: "sine" }); break; case "handoff": - tone(ctx, master, 349.23, t0, 0.2, { gain: 0.04 }); - tone(ctx, master, 466.16, t0 + 0.1, 0.24, { gain: 0.035, type: "triangle" }); + tone(ctx, master, 392, t0, 0.18, { gain: 0.11 }); + tone(ctx, master, 523.25, t0 + 0.09, 0.22, { gain: 0.1, type: "triangle" }); break; case "group-spark": - tone(ctx, master, 587.33, t0, 0.11, { gain: 0.032, attack: 0.004, type: "triangle" }); - tone(ctx, master, 739.99, t0 + 0.06, 0.14, { gain: 0.028, type: "sine" }); - tone(ctx, master, 880, t0 + 0.12, 0.1, { gain: 0.022, release: 0.05 }); + tone(ctx, master, 587.33, t0, 0.11, { gain: 0.095, attack: 0.004, type: "triangle" }); + tone(ctx, master, 739.99, t0 + 0.06, 0.14, { gain: 0.085, type: "sine" }); + tone(ctx, master, 880, t0 + 0.12, 0.12, { gain: 0.075, release: 0.06 }); break; case "routine_start": tone(ctx, master, 392, t0, 0.08, { gain: 0.025, attack: 0.005, release: 0.03, type: "sine" }); diff --git a/apps/web/src/lib/oo-vocals.ts b/apps/web/src/lib/oo-vocals.ts index 0856a8e..5c8fda2 100644 --- a/apps/web/src/lib/oo-vocals.ts +++ b/apps/web/src/lib/oo-vocals.ts @@ -3,8 +3,8 @@ import { readOoSoundEnabled } from "./oo-sound-pref"; export type OoVocalReaction = "ouh" | "mhm" | "aaah" | "mm" | "nod-down" | "nod-up" | "hm"; -const COOLDOWN_MIN_MS = 8_000; -const COOLDOWN_MAX_MS = 12_000; +const COOLDOWN_MIN_MS = 1_600; +const COOLDOWN_MAX_MS = 2_800; let lastPlayedAt = 0; let nextCooldownMs = COOLDOWN_MIN_MS; @@ -28,17 +28,23 @@ function vocalFormant( ) { const osc = ctx.createOscillator(); const gain = ctx.createGain(); - const filter = ctx.createBiquadFilter(); - filter.type = "bandpass"; - filter.frequency.value = baseHz * 2.4; - filter.Q.value = 4.5; - osc.type = "triangle"; + const boostedPeak = Math.min(0.22, gainPeak * 3.2); + osc.type = "sine"; osc.frequency.setValueAtTime(baseHz, startAt); + osc.frequency.exponentialRampToValueAtTime(baseHz * 1.12, startAt + duration * 0.75); gain.gain.setValueAtTime(0.0001, startAt); - gain.gain.exponentialRampToValueAtTime(gainPeak, startAt + 0.025); + gain.gain.exponentialRampToValueAtTime(boostedPeak, startAt + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration); - osc.connect(filter); - filter.connect(gain); + if (typeof ctx.createBiquadFilter === "function") { + const filter = ctx.createBiquadFilter(); + filter.type = "bandpass"; + filter.frequency.value = baseHz * 1.6; + filter.Q.value = 1.8; + osc.connect(filter); + filter.connect(gain); + } else { + osc.connect(gain); + } gain.connect(destination); osc.start(startAt); osc.stop(startAt + duration + 0.02); diff --git a/apps/web/src/lib/queued-follow-ups.test.ts b/apps/web/src/lib/queued-follow-ups.test.ts index 3da1ab0..7707c3d 100644 --- a/apps/web/src/lib/queued-follow-ups.test.ts +++ b/apps/web/src/lib/queued-follow-ups.test.ts @@ -37,4 +37,10 @@ describe("queuedFollowUps", () => { const synthetic = userMessage("progress:1", "2026-01-01T10:00:05.000Z"); expect(queuedFollowUps([synthetic], RUN_START, new Map())).toEqual([]); }); + + it("clears queued state once sendNow starts a newer run after the message", () => { + const followUp = userMessage("queued-1", "2026-01-01T10:00:05.000Z"); + expect(queuedFollowUps([followUp], RUN_START, new Map())).toEqual([followUp]); + expect(queuedFollowUps([followUp], "2026-01-01T10:00:05.001Z", new Map())).toEqual([]); + }); }); diff --git a/apps/web/src/lib/queued-follow-ups.ts b/apps/web/src/lib/queued-follow-ups.ts index 8350266..7c0b977 100644 --- a/apps/web/src/lib/queued-follow-ups.ts +++ b/apps/web/src/lib/queued-follow-ups.ts @@ -1,25 +1 @@ -import type { ThreadMessage } from "@rakazo/contracts"; - -/** The bot marks a steering message with this reaction once it has picked it up. */ -const SEEN_REACTION = "👀"; - -/** - * User messages sent after the active run started that the bot has not picked - * up yet. The run's own trigger message predates the run, so it never counts. - */ -export function queuedFollowUps( - messages: readonly ThreadMessage[], - runCreatedAt: string | undefined, - reactions: ReadonlyMap>, -): ThreadMessage[] { - if (!runCreatedAt) return []; - const runStart = Date.parse(runCreatedAt); - if (Number.isNaN(runStart)) return []; - return messages.filter( - (message) => - message.role === "user" && - !message.id.includes(":") && - Date.parse(message.createdAt) > runStart && - !reactions.get(message.id)?.has(SEEN_REACTION), - ); -} +export { queuedFollowUps } from "@rakazo/core"; diff --git a/apps/web/src/lib/tts.test.ts b/apps/web/src/lib/tts.test.ts index 23a2b23..8743ab0 100644 --- a/apps/web/src/lib/tts.test.ts +++ b/apps/web/src/lib/tts.test.ts @@ -215,6 +215,53 @@ describe("Speaker", () => { expect(speaker.state.status).toBe("idle"); expect(speaker.state.error).toBeUndefined(); }); + + it("filters out novelty voices and selects a natural German voice for German text", async () => { + stubSelectedSpace("space-voice"); + const speak = vi.fn(); + const cancel = vi.fn(); + const eddy = { name: "Eddy (German (Germany))", lang: "de-DE", localService: true }; + const grandma = { name: "Grandma (German (Germany))", lang: "de-DE", localService: true }; + const anna = { name: "Anna", lang: "de-DE", localService: true }; + vi.stubGlobal( + "SpeechSynthesisUtterance", + class { + onend: (() => void) | null = null; + onerror: (() => void) | null = null; + voice: unknown = null; + lang = ""; + rate = 1; + pitch = 1; + constructor(public text: string) {} + }, + ); + vi.stubGlobal("window", { + speechSynthesis: { + cancel, + getVoices: () => [eddy, grandma, anna], + speak, + }, + localStorage: globalThis.localStorage, + }); + + const speaker = new Speaker(); + vi.spyOn( + speaker as unknown as { prepare: () => Promise }, + "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", () => { diff --git a/apps/web/src/lib/tts.ts b/apps/web/src/lib/tts.ts index 6ece0f1..dd8170b 100644 --- a/apps/web/src/lib/tts.ts +++ b/apps/web/src/lib/tts.ts @@ -1,5 +1,7 @@ import { ClauseSegmenter, readBoundedResponseBytes, speakable } from "@rakazo/core"; +import { ooAudioBus } from "./oo-audio-bus.js"; import { rpc, selectedSpaceId, withSpaceHeaders } from "./rpc.js"; +import { readUiLocale } from "./ui-locale.js"; export type SpeechStatus = "idle" | "preparing" | "speaking"; @@ -76,7 +78,19 @@ export class Speaker { if (this.snapshot.status !== "idle" || this.snapshot.error) this.set(IDLE); } + private bufferSource: AudioBufferSourceNode | null = null; + private teardownAudio() { + if (this.bufferSource) { + try { + this.bufferSource.onended = null; + this.bufferSource.stop(); + this.bufferSource.disconnect(); + } catch { + // Ignore already-stopped buffer source errors. + } + this.bufferSource = null; + } if (this.audio) { this.audio.pause(); this.audio.src = ""; @@ -225,16 +239,51 @@ export class Speaker { audio.onended = null; audio.onerror = null; if (this.settlePlayback === done) this.settlePlayback = null; - if (this.audio === audio) this.teardownAudio(); + if (this.audio === audio || this.bufferSource) this.teardownAudio(); resolve(ok); }; this.settlePlayback = done; audio.onended = () => done(true); - audio.onerror = () => done(false); - audio.play().catch(() => done(false)); + audio.onerror = () => { + void this.playViaWebAudio(blob, live).then(done); + }; + audio.play().catch(() => { + void this.playViaWebAudio(blob, live).then(done); + }); }); } + private async playViaWebAudio(blob: Blob, live: () => boolean): Promise { + 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((resolve) => { + const source = ctx.createBufferSource(); + source.buffer = audioBuffer; + source.connect(ctx.destination); + this.bufferSource = source; + source.onended = () => { + if (this.bufferSource === source) this.bufferSource = null; + resolve(true); + }; + source.start(0); + }); + } catch { + return false; + } + } + async tryBrowserFallback( text: string, opts: SpeakOptions, @@ -255,17 +304,65 @@ export class Speaker { return new Promise((resolve) => { try { window.speechSynthesis.cancel(); + window.speechSynthesis.resume?.(); const utterance = new SpeechSynthesisUtterance(text); const voices = window.speechSynthesis.getVoices?.() ?? []; - const isGerman = /[äöüß]|(?:und|der|die|das|ist|nicht|für|mit|den|dem|des)/i.test(text); - const langPrefix = isGerman ? "de" : "en"; + const uiLocale = readUiLocale(); + const voiceId = opts.voiceId ?? ""; + const isGermanVoiceId = /^d[fm]_/i.test(voiceId); + const isGermanText = + /[äöüß]|(?:\b(?:und|der|die|das|ist|nicht|für|mit|den|dem|des|ein|eine|ich|wir|bitte)\b)/i.test( + text, + ); + const isGerman = isGermanVoiceId || uiLocale === "de" || isGermanText; + const langPrefix = isGerman ? "de" : uiLocale.split("-")[0] || "en"; + utterance.lang = isGerman ? "de-DE" : langPrefix === "en" ? "en-US" : uiLocale; + + const allLangVoices = voices.filter((v) => + (v.lang ?? "").toLowerCase().replace(/_/g, "-").startsWith(langPrefix), + ); + const noveltyHint = + /\b(?:eddy|flo|grandma|grandpa|reed|rocko|sandy|shelley|albert|bad news|bahh|bells|boing|bubbles|cellos|deranged|good news|hysterical|junior|organ|superstar|trinoids|whisper|zarvox|fred|kathy|ralph)\b/i; + const naturalLangVoices = allLangVoices.filter((v) => !noveltyHint.test(v.name ?? "")); + const langVoices = naturalLangVoices.length > 0 ? naturalLangVoices : allLangVoices; + const wantsMale = /^dm_/i.test(voiceId); + const wantsFemale = /^df_/i.test(voiceId) || (!wantsMale && isGerman); + const maleHint = + /yannick|martin|markus|stefan|viktor|hans|daniel|thomas|florian|conrad|male/i; + const femaleHint = /anna|petra|helena|marlene|vicki|katja|seraphina|amala|siri|female/i; + const matchedVoice = - voices.find((v) => v.lang.startsWith(langPrefix) && !v.localService) || - voices.find((v) => v.lang.startsWith(langPrefix)) || + (wantsMale && + (langVoices.find((v) => !v.localService && maleHint.test(v.name ?? "")) || + langVoices.find((v) => maleHint.test(v.name ?? "")))) || + (wantsFemale && + (langVoices.find((v) => !v.localService && femaleHint.test(v.name ?? "")) || + langVoices.find((v) => femaleHint.test(v.name ?? "")))) || + langVoices.find((v) => !v.localService) || + langVoices[0] || voices[0]; if (matchedVoice) utterance.voice = matchedVoice; - utterance.rate = 1.05; - utterance.pitch = 1.0; + + // Distinct natural voice profiles for the 5 German presets when rendered via system TTS + if (voiceId === "dm_karlsson") { + utterance.rate = 0.98; + utterance.pitch = 0.85; + } else if (voiceId === "dm_thorsten") { + utterance.rate = 1.03; + utterance.pitch = 0.95; + } else if (voiceId === "df_ramona") { + utterance.rate = 0.97; + utterance.pitch = 1.0; + } else if (voiceId === "df_eva") { + utterance.rate = 1.02; + utterance.pitch = 1.04; + } else if (voiceId === "df_kerstin") { + utterance.rate = 1.05; + utterance.pitch = 1.08; + } else { + utterance.rate = 1.04; + utterance.pitch = 1.0; + } this.set({ status: "speaking", diff --git a/apps/web/src/lib/web-push.ts b/apps/web/src/lib/web-push.ts index 2dd999d..12d3329 100644 --- a/apps/web/src/lib/web-push.ts +++ b/apps/web/src/lib/web-push.ts @@ -100,3 +100,37 @@ export async function disableWebPush(): Promise { await rpc.notifications.webPush.unsubscribe({ endpoint: subscription.endpoint }); await subscription.unsubscribe().catch(() => undefined); } + +/** + * True on iOS/iPadOS when opened in a browser tab rather than as an installed Home-Screen PWA + * (where Apple WebKit only exposes Web Push in standalone display mode). + */ +export function isIosBrowserNeedsHomeScreen(): boolean { + if (typeof window === "undefined" || typeof navigator === "undefined" || desktopBridge()) { + return false; + } + const ua = navigator.userAgent || ""; + const isIos = + /iPad|iPhone|iPod/i.test(ua) || + (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); + if (!isIos) return false; + const standalone = + (typeof window.matchMedia === "function" && + window.matchMedia("(display-mode: standalone)").matches) || + Boolean((navigator as Navigator & { standalone?: boolean }).standalone); + return !standalone && !webPushSupported(); +} + +/** Syncs the installed PWA / OS app icon badge with the actionable "Needs You" count. */ +export function syncAppBadge(count: number): void { + if (typeof navigator === "undefined") return; + const nav = navigator as Navigator & { + setAppBadge?: (contents?: number) => Promise; + clearAppBadge?: () => Promise; + }; + 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); + } +} diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index c315583..2a93096 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -161,11 +161,6 @@ msgstr "Browser von {0}" msgid "{0}'s browser appears here when the computer is running." msgstr "{0}s Browser erscheint hier, wenn der Computer läuft." -#. placeholder {0}: active.name -#: src/pages/shell/computer-panel-preview.tsx -msgid "{0}'s screen" -msgstr "{0}s Bildschirm" - #: src/pages/HeuteBrief.tsx msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgstr "{actionCount, plural, one {# Sache braucht dich} other {# Sachen brauchen dich}}" @@ -182,10 +177,38 @@ msgstr "{botName} prüft die Architektur…" msgid "{botName} clears the path…" msgstr "{botName} räumt den Weg frei…" +#: src/pages/shell/transcript.tsx +msgid "{botName} is building" +msgstr "{botName} baut & tüftelt" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is checking a problem" +msgstr "{botName} prüft ein Problem" + #: src/lib/voice-persona-copy.ts msgid "{botName} is on it…" msgstr "{botName} ist dran…" +#: src/pages/shell/transcript.tsx +msgid "{botName} is replying" +msgstr "{botName} antwortet" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is researching" +msgstr "{botName} liest & recherchiert" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is thinking" +msgstr "{botName} denkt nach" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is waiting for your answer" +msgstr "{botName} wartet auf deine Antwort" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is working" +msgstr "{botName} arbeitet" + #: src/lib/voice-persona-copy.ts #: src/pages/CallView.tsx msgid "{botName} is working…" @@ -364,6 +387,7 @@ msgstr "Noch {remaining} · Bot beobachtet nur" msgid "{size} B" msgstr "{size} B" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "{stepCount, plural, one {# step} other {# steps}}" msgstr "{stepCount, plural, one {# Schritt} other {# Schritte}}" @@ -386,10 +410,6 @@ msgstr "{title}, {label}" msgid "{toolCount, plural, one {# tool} other {# tools}}" msgstr "{toolCount, plural, one {# Tool} other {# Tools}}" -#: src/pages/shell/transcript.tsx -msgid "{workingBotName} is working" -msgstr "{workingBotName} arbeitet" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "/Users/you/projects/acme" @@ -433,7 +453,7 @@ msgstr "Über Gruppen" #: src/pages/shell/bot-picker.tsx msgid "About spaces" -msgstr "Über Spaces" +msgstr "Über Projekte" #: src/components/UserProfileSettings.tsx msgid "About you" @@ -544,11 +564,16 @@ msgstr "Weiteres hinzufügen" msgid "Add as projects" msgstr "Als Projekte hinzufügen" +#: src/components/NotificationSettingsPanel.tsx +msgid "Add evOo to your Home Screen via Share to enable push on iOS." +msgstr "Füge evOo über „Teilen“ zum Home-Bildschirm hinzu, um Push-Mitteilungen auf iOS zu aktivieren." + #: src/pages/PluginsOverlay.tsx msgid "Add Executor" msgstr "Executor hinzufügen" #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Add goal" msgstr "Ziel hinzufügen" @@ -737,6 +762,14 @@ msgstr "Immer erlaubt" msgid "Analysiert die Aufgabe und führt Tools aus…" msgstr "Analysiert die Aufgabe und führt Tools aus…" +#: src/pages/Shell.tsx +msgid "Analyze the project and give me a brief overview." +msgstr "Analysiere das Projekt und gib mir einen kurzen Überblick." + +#: src/pages/shell/project-overview.tsx +msgid "Analyzing project…" +msgstr "Projekt wird analysiert…" + #: src/components/AskCard.tsx msgid "Answer" msgstr "Antwort" @@ -996,7 +1029,7 @@ msgstr "Körper" msgid "Booting live desktop…" msgstr "Live-Desktop wird gestartet…" -#. placeholder {0}: active?.name +#. placeholder {0}: (active ?? coordinatorBot)?.name #: src/pages/Shell.tsx msgid "Booting up {0}’s computer" msgstr "Computer von {0} wird gestartet" @@ -1019,10 +1052,6 @@ msgstr "Bot" msgid "Bot screen" msgstr "Bot-Bildschirm" -#: src/pages/shell/computer-panel-preview.tsx -msgid "Bot screen preview" -msgstr "Bot-Bildschirmvorschau" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Bot to link" msgstr "Zu verknüpfender Bot" @@ -1031,10 +1060,6 @@ msgstr "Zu verknüpfender Bot" msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first." msgstr "Bots handeln standardmäßig ohne Nachfrage. Füge nur dann eine Ausnahme hinzu, wenn du eine Aktionsart zuerst prüfen möchtest." -#: src/pages/shell/transcript.tsx -msgid "Bots are working" -msgstr "Bots arbeiten" - #: src/components/OoAtelier.tsx msgid "Bow" msgstr "Schleife" @@ -1064,7 +1089,6 @@ msgstr "Durchsuchen" msgid "Browse MCP servers" msgstr "MCP-Server durchsuchen" -#: src/pages/Shell.tsx #: src/pages/shell/side-panel-header.tsx msgid "Browser" msgstr "Browser" @@ -1493,6 +1517,10 @@ msgstr "Computer" msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "Computer sind aus. Setze SANDBOX_PROVIDER=docker mit SANDBOX_SUPERVISOR_TOKEN, oder setze SANDBOX_PROVIDER auf e2b, daytona oder box mit dem passenden API-Key. Stack nach der Änderung von .env neu erstellen." +#: src/pages/PluginsOverlay.tsx +msgid "Configure a plugin catalog on the server to connect apps." +msgstr "Richte auf dem Server einen Plugin-Katalog ein, um Apps zu verbinden." + #: src/pages/ModelSettingsOverlay.tsx msgid "Configured by deployment" msgstr "Durch die Bereitstellung konfiguriert" @@ -1584,6 +1612,10 @@ msgstr "Verbunden, {0} wird verwendet." msgid "Connected app" msgstr "Verbundene App" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Connected tools" +msgstr "Verbundene Tools" + #: src/pages/shell/message-cards.tsx msgid "Connected. Its tools are available from your next message." msgstr "Verbunden. Die Tools sind ab deiner nächsten Nachricht verfügbar." @@ -2077,7 +2109,7 @@ msgstr "Neue Gruppe erstellen" #: src/pages/shell/bot-picker.tsx msgid "Create new Space" -msgstr "Neuen Space erstellen" +msgstr "Neues Projekt erstellen" #: src/pages/shell/dialogs.tsx msgid "Create project" @@ -2086,7 +2118,7 @@ msgstr "Projekt erstellen" #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" -msgstr "Space erstellen" +msgstr "Projekt erstellen" #: src/components/integrations/GithubConnect.tsx msgid "Create token on GitHub" @@ -2241,7 +2273,7 @@ msgstr "Erinnerungen ebenfalls löschen" #: src/pages/Shell.tsx msgid "Delete space" -msgstr "Space löschen" +msgstr "Projekt löschen" #: src/pages/PrivacySettingsOverlay.tsx msgid "Delete your account?" @@ -2322,6 +2354,7 @@ msgstr "Verbindung wird getrennt…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Ausblenden" @@ -2381,7 +2414,9 @@ msgstr "Kein Stress — wir sind jetzt da." #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/message-cards.tsx #: src/pages/shell/project-overview.tsx +#: src/pages/shell/transcript.tsx msgid "Done" msgstr "Fertig" @@ -2394,8 +2429,11 @@ msgstr "Erledigt" msgid "Dot" msgstr "Punkt" +#. placeholder {0}: activeItem.name +#. placeholder {0}: item.name #. placeholder {0}: props.name #: src/components/ArtifactFileCard.tsx +#: src/pages/shell/message-cards.tsx msgid "Download {0}" msgstr "{0} herunterladen" @@ -2403,7 +2441,6 @@ msgstr "{0} herunterladen" #: src/components/ArtifactPane.tsx #: src/components/media/MediaCard.tsx #: src/components/media/MediaPlayer.tsx -#: src/pages/shell/message-cards.tsx msgid "Download {name}" msgstr "{name} herunterladen" @@ -2589,6 +2626,8 @@ msgid "Expand" msgstr "Erweitern" #. placeholder {0}: group.title +#. placeholder {0}: item.name +#: src/pages/shell/message-cards.tsx #: src/pages/shell/sidebar-group-header.tsx msgid "Expand {0}" msgstr "{0} ausklappen" @@ -2660,6 +2699,10 @@ msgstr "Workspace-Dateien konnten nicht geladen werden" msgid "Failed to open file" msgstr "Datei konnte nicht geöffnet werden" +#: src/pages/Shell.tsx +msgid "Failed to remove message" +msgstr "Nachricht konnte nicht entfernt werden" + #: src/pages/Shell.tsx msgid "Failed to send message" msgstr "Nachricht konnte nicht gesendet werden" @@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…" msgstr "Das sehe ich in den Logs…" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Hi, this is how I'll sound when I read replies out loud." -msgstr "Hallo, so klinge ich, wenn ich Antworten vorlese." +msgid "Hi, this is how I'll sound when we talk." +msgstr "Hallo, so klinge ich, wenn wir miteinander sprechen." #: src/pages/Shell.tsx +#: src/pages/shell/sidebar-header.tsx msgid "Hide bots" msgstr "Bots ausblenden" +#: src/pages/shell/transcript.tsx +msgid "Hide details" +msgstr "Details ausblenden" + #: src/pages/Auth.tsx msgid "Hide password" msgstr "Passwort verbergen" @@ -3080,6 +3128,7 @@ msgstr "Bot importieren" msgid "Import OpenAPI JSON" msgstr "OpenAPI-JSON importieren" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "In Bearbeitung" msgstr "In Bearbeitung" @@ -3166,6 +3215,10 @@ msgstr "Isoliert" msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgstr "Seine Unterhaltung, Dateien und Routinen werden dauerhaft gelöscht. Von ihm erstellte Bots bleiben in deiner Liste." +#: src/pages/shell/transcript.tsx +msgid "joined" +msgstr "dazugeschaltet" + #: src/pages/shell/transcript.tsx msgid "Jump to latest" msgstr "Zur neuesten Nachricht springen" @@ -3174,6 +3227,10 @@ msgstr "Zur neuesten Nachricht springen" msgid "Jump to replied message" msgstr "Zur beantworteten Nachricht springen" +#: src/pages/shell/project-overview.tsx +msgid "Just ask in the chat whenever something should happen regularly." +msgstr "Sag einfach im Chat Bescheid, wenn etwas regelmäßig erledigt werden soll." + #: src/pages/ActivityList.tsx #: src/pages/ProjectSessionTimeline.tsx msgid "just now" @@ -3771,9 +3828,10 @@ msgstr "Neuer Skill" #: src/pages/shell/dialogs.tsx msgid "New space" -msgstr "Neuer Space" +msgstr "Neues Projekt" #: src/components/CookingMode.tsx +#: src/pages/shell/message-cards.tsx msgid "Next" msgstr "Weiter" @@ -3885,6 +3943,10 @@ msgstr "Noch keine Projekte." msgid "No providers found." msgstr "Keine Anbieter gefunden." +#: src/pages/ActivityList.tsx +msgid "No recent activity" +msgstr "Keine letzte Aktivität" + #: src/components/integrations/IntegrationSetup.tsx msgid "No remote MCP servers found" msgstr "Keine Remote-MCP-Server gefunden" @@ -3899,6 +3961,7 @@ msgid "No results" msgstr "Keine Ergebnisse" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "No runs yet" msgstr "Noch keine Ausführungen" @@ -3957,6 +4020,7 @@ msgid "Notifications" msgstr "Benachrichtigungen" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Now" msgstr "Jetzt" @@ -4027,7 +4091,6 @@ msgid "Oo name" msgstr "Name des Oo" #: src/pages/ScratchpadSection.tsx -#: src/pages/shell/computer-panel-preview.tsx msgid "Open" msgstr "Öffnen" @@ -4056,7 +4119,7 @@ msgstr "Unterhaltung öffnen" msgid "Open in browser" msgstr "Im Browser öffnen" -#: src/pages/shell/computer-panel-preview.tsx +#: src/components/browser/OoComputerBrowser.tsx msgid "Open in full window" msgstr "In vollständigem Fenster öffnen" @@ -4288,10 +4351,6 @@ msgstr "Privater Arbeitsbereich mit eigenen Bots und Gruppen." msgid "Project" msgstr "Projekt" -#: src/pages/Shell.tsx -msgid "Project · sessions & folders" -msgstr "Projekt · Sessions & Ordner" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "Project folder" @@ -4399,6 +4458,10 @@ msgstr "Lese den Stacktrace…" msgid "Ready" msgstr "Bereit" +#: src/pages/shell/project-overview.tsx +msgid "Ready" +msgstr "Bereit" + #: src/pages/ModelSettingsOverlay.tsx msgid "Reasoning effort" msgstr "Denkaufwand" @@ -4412,6 +4475,7 @@ msgid "Recall only" msgstr "Nur abrufen" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Recent" msgstr "Kürzlich" @@ -4528,6 +4592,10 @@ msgstr "{0} entfernen" msgid "Remove folder" msgstr "Ordner entfernen" +#: src/pages/shell/transcript.tsx +msgid "Remove from queue" +msgstr "Aus der Warteschlange entfernen" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "Git-Ereignis entfernen" @@ -4653,6 +4721,11 @@ msgstr "Stellt den zuletzt gespeicherten Arbeitsbereich wieder her. Nicht gespei msgid "Restoring your workspace" msgstr "Dein Arbeitsbereich wird wiederhergestellt" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "Fortsetzen" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "Wiederholen" @@ -4700,11 +4773,13 @@ msgstr "Rund" #: src/pages/RoutineEditor.tsx #: src/pages/Shell.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/routine-event-line.tsx msgid "Routine" msgstr "Routine" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Routines" msgstr "Routinen" @@ -4718,6 +4793,7 @@ msgid "Run at" msgstr "Ausführen um" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Run history" msgstr "Ausführungsverlauf" @@ -4756,10 +4832,6 @@ msgstr "Wird ausgeführt…" msgid "Runs when this bot receives a verified message from this provider." msgstr "Läuft, wenn dieser Bot eine verifizierte Nachricht von diesem Anbieter erhält." -#: src/pages/shell/side-panel-header.tsx -msgid "Sandbox" -msgstr "Sandbox" - #: src/components/AskCard.tsx #: src/components/MediaSettings.tsx #: src/components/teach/SkillDraftCard.tsx @@ -4909,6 +4981,10 @@ msgstr "Antwort senden" msgid "Send it" msgstr "Senden" +#: src/pages/shell/transcript.tsx +msgid "Send now" +msgstr "Jetzt gleich senden" + #: src/pages/Auth.tsx msgid "Send reset link" msgstr "Link zum Zurücksetzen senden" @@ -5021,6 +5097,10 @@ msgstr "Bots anzeigen" msgid "Show computer" msgstr "Computer anzeigen" +#: src/pages/shell/transcript.tsx +msgid "Show details" +msgstr "Details anzeigen" + #: src/pages/Shell.tsx msgid "Show less" msgstr "Weniger anzeigen" @@ -5037,6 +5117,7 @@ msgstr "Mehr anzeigen" msgid "Show password" msgstr "Passwort anzeigen" +#: src/pages/shell/project-overview.tsx #: src/pages/shell/side-panel-header.tsx msgid "Show settings" msgstr "Einstellungen anzeigen" @@ -5136,7 +5217,7 @@ msgstr "Space-Standard" #: src/pages/shell/dialogs.tsx msgid "Spaces" -msgstr "Spaces" +msgstr "Projekte" #: src/components/OoAtelier.tsx msgid "Sparkle" @@ -5172,6 +5253,7 @@ msgstr "Vorlesen an oder aus" msgid "Speaking with intent…" msgstr "Spreche mit Bedacht…" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx #: src/lib/onboarding-copy.ts msgid "Specialist crew" msgstr "Spezialisten-Team" @@ -5286,6 +5368,10 @@ msgstr "Anlernen beenden" msgid "Stop timer" msgstr "Timer stoppen" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "Gestoppt" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." @@ -5398,6 +5484,7 @@ msgstr "Aufgabe wird angelernt. Beende das Anlernen, bevor du eine neue Nachrich #: src/pages/ProjectSessionTimeline.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-overview.tsx msgid "Team" msgstr "Team" @@ -5512,10 +5599,6 @@ msgstr "Diese Abonnementanmeldung ist in Rakazo noch nicht verfügbar. Verwende msgid "This will permanently delete this project and all its conversations, bots, and memory." msgstr "Dadurch werden dieses Projekt und alle zugehörigen Unterhaltungen, Bots und Erinnerungen dauerhaft gelöscht." -#: src/pages/Shell.tsx -msgid "Threads by topic · folders" -msgstr "Unterhaltungen nach Thema · Ordner" - #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx msgid "Tiers" @@ -5701,6 +5784,7 @@ msgstr "Bis wann?" #: src/components/DesktopUpdates.tsx #: src/components/SoftwareUpdateSection.tsx +#: src/pages/AccountSettingsOverlay.tsx msgid "Up to date" msgstr "Aktuell" @@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead." msgstr "Arbeitskanal offen — leg los." #: src/components/stage/OoStage.tsx +#: src/pages/shell/project-overview.tsx msgid "Working" msgstr "Arbeitet" @@ -6037,6 +6122,7 @@ msgstr "Gestern" #: src/components/UserProfileSettings.tsx #: src/components/voice/VoiceStage.tsx #: src/pages/Shell.tsx +#: src/pages/shell/transcript.tsx msgid "You" msgstr "Du" @@ -6077,13 +6163,25 @@ msgid "Your name" msgstr "Dein Name" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Your Oo reads replies aloud when this is on." -msgstr "Wenn das an ist, liest dein Oo Antworten vor." +msgid "Your Oo talks with you naturally when this is on." +msgstr "Wenn das an ist, spricht dein Oo ganz natürlich mit dir." #: src/pages/Onboarding.tsx msgid "Your Oo will communicate in this language. You can change it later." msgstr "Dein Oo kommuniziert in dieser Sprache. Du kannst sie später ändern." +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Your profile" +msgstr "Dein Profil" + #: src/pages/Welcome.tsx msgid "Your specialists. One calm place to talk." msgstr "Deine Spezialisten. Ein ruhiger Ort zum Reden." + +#: src/pages/shell/message-cards.tsx +msgid "Zoom in" +msgstr "Vergrößern" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom out" +msgstr "Verkleinern" diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index 60c0cdd..6c28cad 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -161,11 +161,6 @@ msgstr "{0}'s browser" msgid "{0}'s browser appears here when the computer is running." msgstr "{0}'s browser appears here when the computer is running." -#. placeholder {0}: active.name -#: src/pages/shell/computer-panel-preview.tsx -msgid "{0}'s screen" -msgstr "{0}'s screen" - #: src/pages/HeuteBrief.tsx msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgstr "{actionCount, plural, one {# thing needs you} other {# things need you}}" @@ -182,10 +177,38 @@ msgstr "{botName} checks the architecture…" msgid "{botName} clears the path…" msgstr "{botName} clears the path…" +#: src/pages/shell/transcript.tsx +msgid "{botName} is building" +msgstr "{botName} is building" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is checking a problem" +msgstr "{botName} is checking a problem" + #: src/lib/voice-persona-copy.ts msgid "{botName} is on it…" msgstr "{botName} is on it…" +#: src/pages/shell/transcript.tsx +msgid "{botName} is replying" +msgstr "{botName} is replying" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is researching" +msgstr "{botName} is researching" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is thinking" +msgstr "{botName} is thinking" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is waiting for your answer" +msgstr "{botName} is waiting for your answer" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is working" +msgstr "{botName} is working" + #: src/lib/voice-persona-copy.ts #: src/pages/CallView.tsx msgid "{botName} is working…" @@ -364,6 +387,7 @@ msgstr "{remaining} left · bot is watching, not acting" msgid "{size} B" msgstr "{size} B" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "{stepCount, plural, one {# step} other {# steps}}" msgstr "{stepCount, plural, one {# step} other {# steps}}" @@ -386,10 +410,6 @@ msgstr "{title}, {label}" msgid "{toolCount, plural, one {# tool} other {# tools}}" msgstr "{toolCount, plural, one {# tool} other {# tools}}" -#: src/pages/shell/transcript.tsx -msgid "{workingBotName} is working" -msgstr "{workingBotName} is working" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "/Users/you/projects/acme" @@ -544,11 +564,16 @@ msgstr "Add another" msgid "Add as projects" msgstr "Add as projects" +#: src/components/NotificationSettingsPanel.tsx +msgid "Add evOo to your Home Screen via Share to enable push on iOS." +msgstr "Add evOo to your Home Screen via Share to enable push on iOS." + #: src/pages/PluginsOverlay.tsx msgid "Add Executor" msgstr "Add Executor" #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Add goal" msgstr "Add goal" @@ -737,6 +762,14 @@ msgstr "Always allowed" msgid "Analysiert die Aufgabe und führt Tools aus…" msgstr "Analysiert die Aufgabe und führt Tools aus…" +#: src/pages/Shell.tsx +msgid "Analyze the project and give me a brief overview." +msgstr "Analyze the project and give me a brief overview." + +#: src/pages/shell/project-overview.tsx +msgid "Analyzing project…" +msgstr "Analyzing project…" + #: src/components/AskCard.tsx msgid "Answer" msgstr "Answer" @@ -996,7 +1029,7 @@ msgstr "Body" msgid "Booting live desktop…" msgstr "Booting live desktop…" -#. placeholder {0}: active?.name +#. placeholder {0}: (active ?? coordinatorBot)?.name #: src/pages/Shell.tsx msgid "Booting up {0}’s computer" msgstr "Booting up {0}’s computer" @@ -1019,10 +1052,6 @@ msgstr "Bot" msgid "Bot screen" msgstr "Bot screen" -#: src/pages/shell/computer-panel-preview.tsx -msgid "Bot screen preview" -msgstr "Bot screen preview" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Bot to link" msgstr "Bot to link" @@ -1031,10 +1060,6 @@ msgstr "Bot to link" msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first." msgstr "Bots act without asking by default. Add an exception only when you want to review a type of action first." -#: src/pages/shell/transcript.tsx -msgid "Bots are working" -msgstr "Bots are working" - #: src/components/OoAtelier.tsx msgid "Bow" msgstr "Bow" @@ -1064,7 +1089,6 @@ msgstr "Browse" msgid "Browse MCP servers" msgstr "Browse MCP servers" -#: src/pages/Shell.tsx #: src/pages/shell/side-panel-header.tsx msgid "Browser" msgstr "Browser" @@ -1493,6 +1517,10 @@ msgstr "Computers" msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." +#: src/pages/PluginsOverlay.tsx +msgid "Configure a plugin catalog on the server to connect apps." +msgstr "Configure a plugin catalog on the server to connect apps." + #: src/pages/ModelSettingsOverlay.tsx msgid "Configured by deployment" msgstr "Configured by deployment" @@ -1584,6 +1612,10 @@ msgstr "Connected and using {0}." msgid "Connected app" msgstr "Connected app" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Connected tools" +msgstr "Connected tools" + #: src/pages/shell/message-cards.tsx msgid "Connected. Its tools are available from your next message." msgstr "Connected. Its tools are available from your next message." @@ -2322,6 +2354,7 @@ msgstr "Disconnecting…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Dismiss" @@ -2381,7 +2414,9 @@ msgstr "Don't stress — we're here now." #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/message-cards.tsx #: src/pages/shell/project-overview.tsx +#: src/pages/shell/transcript.tsx msgid "Done" msgstr "Done" @@ -2394,8 +2429,11 @@ msgstr "Done" msgid "Dot" msgstr "Dot" +#. placeholder {0}: activeItem.name +#. placeholder {0}: item.name #. placeholder {0}: props.name #: src/components/ArtifactFileCard.tsx +#: src/pages/shell/message-cards.tsx msgid "Download {0}" msgstr "Download {0}" @@ -2403,7 +2441,6 @@ msgstr "Download {0}" #: src/components/ArtifactPane.tsx #: src/components/media/MediaCard.tsx #: src/components/media/MediaPlayer.tsx -#: src/pages/shell/message-cards.tsx msgid "Download {name}" msgstr "Download {name}" @@ -2589,6 +2626,8 @@ msgid "Expand" msgstr "Expand" #. placeholder {0}: group.title +#. placeholder {0}: item.name +#: src/pages/shell/message-cards.tsx #: src/pages/shell/sidebar-group-header.tsx msgid "Expand {0}" msgstr "Expand {0}" @@ -2660,6 +2699,10 @@ msgstr "Failed to load workspace files" msgid "Failed to open file" msgstr "Failed to open file" +#: src/pages/Shell.tsx +msgid "Failed to remove message" +msgstr "Failed to remove message" + #: src/pages/Shell.tsx msgid "Failed to send message" msgstr "Failed to send message" @@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…" msgstr "Here's what I see in the logs…" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Hi, this is how I'll sound when I read replies out loud." -msgstr "Hi, this is how I'll sound when I read replies out loud." +msgid "Hi, this is how I'll sound when we talk." +msgstr "Hi, this is how I'll sound when we talk." #: src/pages/Shell.tsx +#: src/pages/shell/sidebar-header.tsx msgid "Hide bots" msgstr "Hide bots" +#: src/pages/shell/transcript.tsx +msgid "Hide details" +msgstr "Hide details" + #: src/pages/Auth.tsx msgid "Hide password" msgstr "Hide password" @@ -3080,6 +3128,7 @@ msgstr "Import bot" msgid "Import OpenAPI JSON" msgstr "Import OpenAPI JSON" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "In Bearbeitung" msgstr "In Bearbeitung" @@ -3166,6 +3215,10 @@ msgstr "Isolated" msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgstr "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." +#: src/pages/shell/transcript.tsx +msgid "joined" +msgstr "joined" + #: src/pages/shell/transcript.tsx msgid "Jump to latest" msgstr "Jump to latest" @@ -3174,6 +3227,10 @@ msgstr "Jump to latest" msgid "Jump to replied message" msgstr "Jump to replied message" +#: src/pages/shell/project-overview.tsx +msgid "Just ask in the chat whenever something should happen regularly." +msgstr "Just ask in the chat whenever something should happen regularly." + #: src/pages/ActivityList.tsx #: src/pages/ProjectSessionTimeline.tsx msgid "just now" @@ -3774,6 +3831,7 @@ msgid "New space" msgstr "New space" #: src/components/CookingMode.tsx +#: src/pages/shell/message-cards.tsx msgid "Next" msgstr "Next" @@ -3885,6 +3943,10 @@ msgstr "No projects yet." msgid "No providers found." msgstr "No providers found." +#: src/pages/ActivityList.tsx +msgid "No recent activity" +msgstr "No recent activity" + #: src/components/integrations/IntegrationSetup.tsx msgid "No remote MCP servers found" msgstr "No remote MCP servers found" @@ -3899,6 +3961,7 @@ msgid "No results" msgstr "No results" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "No runs yet" msgstr "No runs yet" @@ -3957,6 +4020,7 @@ msgid "Notifications" msgstr "Notifications" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Now" msgstr "Now" @@ -4027,7 +4091,6 @@ msgid "Oo name" msgstr "Oo name" #: src/pages/ScratchpadSection.tsx -#: src/pages/shell/computer-panel-preview.tsx msgid "Open" msgstr "Open" @@ -4056,7 +4119,7 @@ msgstr "Open conversation" msgid "Open in browser" msgstr "Open in browser" -#: src/pages/shell/computer-panel-preview.tsx +#: src/components/browser/OoComputerBrowser.tsx msgid "Open in full window" msgstr "Open in full window" @@ -4288,10 +4351,6 @@ msgstr "Private workspace with its own bots and groups." msgid "Project" msgstr "Project" -#: src/pages/Shell.tsx -msgid "Project · sessions & folders" -msgstr "Project · sessions & folders" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "Project folder" @@ -4399,6 +4458,10 @@ msgstr "Reading the stack trace…" msgid "Ready" msgstr "Ready" +#: src/pages/shell/project-overview.tsx +msgid "Ready" +msgstr "Ready" + #: src/pages/ModelSettingsOverlay.tsx msgid "Reasoning effort" msgstr "Reasoning effort" @@ -4412,6 +4475,7 @@ msgid "Recall only" msgstr "Recall only" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Recent" msgstr "Recent" @@ -4528,6 +4592,10 @@ msgstr "Remove {0}" msgid "Remove folder" msgstr "Remove folder" +#: src/pages/shell/transcript.tsx +msgid "Remove from queue" +msgstr "Remove from queue" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "Remove Git event" @@ -4653,6 +4721,11 @@ msgstr "Restore the last saved workspace. Unsaved work on the computer is lost." msgid "Restoring your workspace" msgstr "Restoring your workspace" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "Resume" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "Retry" @@ -4700,11 +4773,13 @@ msgstr "Round" #: src/pages/RoutineEditor.tsx #: src/pages/Shell.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/routine-event-line.tsx msgid "Routine" msgstr "Routine" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Routines" msgstr "Routines" @@ -4718,6 +4793,7 @@ msgid "Run at" msgstr "Run at" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Run history" msgstr "Run history" @@ -4756,10 +4832,6 @@ msgstr "Running…" msgid "Runs when this bot receives a verified message from this provider." msgstr "Runs when this bot receives a verified message from this provider." -#: src/pages/shell/side-panel-header.tsx -msgid "Sandbox" -msgstr "Sandbox" - #: src/components/AskCard.tsx #: src/components/MediaSettings.tsx #: src/components/teach/SkillDraftCard.tsx @@ -4909,6 +4981,10 @@ msgstr "Send answer" msgid "Send it" msgstr "Send it" +#: src/pages/shell/transcript.tsx +msgid "Send now" +msgstr "Send now" + #: src/pages/Auth.tsx msgid "Send reset link" msgstr "Send reset link" @@ -5021,6 +5097,10 @@ msgstr "Show bots" msgid "Show computer" msgstr "Show computer" +#: src/pages/shell/transcript.tsx +msgid "Show details" +msgstr "Show details" + #: src/pages/Shell.tsx msgid "Show less" msgstr "Show less" @@ -5037,6 +5117,7 @@ msgstr "Show more" msgid "Show password" msgstr "Show password" +#: src/pages/shell/project-overview.tsx #: src/pages/shell/side-panel-header.tsx msgid "Show settings" msgstr "Show settings" @@ -5172,6 +5253,7 @@ msgstr "Speaking on or off" msgid "Speaking with intent…" msgstr "Speaking with intent…" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx #: src/lib/onboarding-copy.ts msgid "Specialist crew" msgstr "Specialist crew" @@ -5286,6 +5368,10 @@ msgstr "Stop teaching" msgid "Stop timer" msgstr "Stop timer" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "Stopped" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." @@ -5398,6 +5484,7 @@ msgstr "Teaching in progress. Stop teaching before sending a new message." #: src/pages/ProjectSessionTimeline.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-overview.tsx msgid "Team" msgstr "Team" @@ -5512,10 +5599,6 @@ msgstr "This subscription sign-in is not available in Rakazo yet. Use a deployme msgid "This will permanently delete this project and all its conversations, bots, and memory." msgstr "This will permanently delete this project and all its conversations, bots, and memory." -#: src/pages/Shell.tsx -msgid "Threads by topic · folders" -msgstr "Threads by topic · folders" - #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx msgid "Tiers" @@ -5701,6 +5784,7 @@ msgstr "Until when?" #: src/components/DesktopUpdates.tsx #: src/components/SoftwareUpdateSection.tsx +#: src/pages/AccountSettingsOverlay.tsx msgid "Up to date" msgstr "Up to date" @@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead." msgstr "Work channel open — go ahead." #: src/components/stage/OoStage.tsx +#: src/pages/shell/project-overview.tsx msgid "Working" msgstr "Working" @@ -6037,6 +6122,7 @@ msgstr "Yesterday" #: src/components/UserProfileSettings.tsx #: src/components/voice/VoiceStage.tsx #: src/pages/Shell.tsx +#: src/pages/shell/transcript.tsx msgid "You" msgstr "You" @@ -6077,13 +6163,25 @@ msgid "Your name" msgstr "Your name" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Your Oo reads replies aloud when this is on." -msgstr "Your Oo reads replies aloud when this is on." +msgid "Your Oo talks with you naturally when this is on." +msgstr "Your Oo talks with you naturally when this is on." #: src/pages/Onboarding.tsx msgid "Your Oo will communicate in this language. You can change it later." msgstr "Your Oo will communicate in this language. You can change it later." +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Your profile" +msgstr "Your profile" + #: src/pages/Welcome.tsx msgid "Your specialists. One calm place to talk." msgstr "Your specialists. One calm place to talk." + +#: src/pages/shell/message-cards.tsx +msgid "Zoom in" +msgstr "Zoom in" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom out" +msgstr "Zoom out" diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index 8c0fe44..502caa2 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -161,11 +161,6 @@ msgstr "" msgid "{0}'s browser appears here when the computer is running." msgstr "" -#. placeholder {0}: active.name -#: src/pages/shell/computer-panel-preview.tsx -msgid "{0}'s screen" -msgstr "Pantalla de {0}" - #: src/pages/HeuteBrief.tsx msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgstr "" @@ -182,10 +177,38 @@ msgstr "" msgid "{botName} clears the path…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is building" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is checking a problem" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "{botName} is on it…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is replying" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is researching" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is thinking" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is waiting for your answer" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is working" +msgstr "" + #: src/lib/voice-persona-copy.ts #: src/pages/CallView.tsx msgid "{botName} is working…" @@ -364,6 +387,7 @@ msgstr "Quedan {remaining} · el bot observa, no actúa" msgid "{size} B" msgstr "{size} B" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "{stepCount, plural, one {# step} other {# steps}}" msgstr "" @@ -386,10 +410,6 @@ msgstr "{title}, {label}" msgid "{toolCount, plural, one {# tool} other {# tools}}" msgstr "" -#: src/pages/shell/transcript.tsx -msgid "{workingBotName} is working" -msgstr "{workingBotName} está trabajando" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "/Users/you/projects/acme" @@ -544,11 +564,16 @@ msgstr "" msgid "Add as projects" msgstr "" +#: src/components/NotificationSettingsPanel.tsx +msgid "Add evOo to your Home Screen via Share to enable push on iOS." +msgstr "" + #: src/pages/PluginsOverlay.tsx msgid "Add Executor" msgstr "Agregar Executor" #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Add goal" msgstr "" @@ -737,6 +762,14 @@ msgstr "Siempre permitido" msgid "Analysiert die Aufgabe und führt Tools aus…" msgstr "" +#: src/pages/Shell.tsx +msgid "Analyze the project and give me a brief overview." +msgstr "" + +#: src/pages/shell/project-overview.tsx +msgid "Analyzing project…" +msgstr "" + #: src/components/AskCard.tsx msgid "Answer" msgstr "Responder" @@ -996,7 +1029,7 @@ msgstr "" msgid "Booting live desktop…" msgstr "Iniciando escritorio en vivo…" -#. placeholder {0}: active?.name +#. placeholder {0}: (active ?? coordinatorBot)?.name #: src/pages/Shell.tsx msgid "Booting up {0}’s computer" msgstr "Iniciando la computadora de {0}" @@ -1019,10 +1052,6 @@ msgstr "Bot" msgid "Bot screen" msgstr "Pantalla del bot" -#: src/pages/shell/computer-panel-preview.tsx -msgid "Bot screen preview" -msgstr "Vista previa de la pantalla del bot" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Bot to link" msgstr "Bot a vincular" @@ -1031,10 +1060,6 @@ msgstr "Bot a vincular" msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first." msgstr "Los bots actúan sin preguntar de forma predeterminada. Agrega una excepción solo cuando quieras revisar primero un tipo de acción." -#: src/pages/shell/transcript.tsx -msgid "Bots are working" -msgstr "Los bots están trabajando" - #: src/components/OoAtelier.tsx msgid "Bow" msgstr "" @@ -1064,7 +1089,6 @@ msgstr "" msgid "Browse MCP servers" msgstr "" -#: src/pages/Shell.tsx #: src/pages/shell/side-panel-header.tsx msgid "Browser" msgstr "" @@ -1493,6 +1517,10 @@ msgstr "Computadoras" msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "Las computadoras están apagadas. Configura SANDBOX_PROVIDER=docker con SANDBOX_SUPERVISOR_TOKEN, o establece SANDBOX_PROVIDER en e2b, daytona o box con su clave de API. Recrea el stack después de cambiar .env." +#: src/pages/PluginsOverlay.tsx +msgid "Configure a plugin catalog on the server to connect apps." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Configured by deployment" msgstr "Configurado por el despliegue" @@ -1584,6 +1612,10 @@ msgstr "Conectado y usando {0}." msgid "Connected app" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Connected tools" +msgstr "" + #: src/pages/shell/message-cards.tsx msgid "Connected. Its tools are available from your next message." msgstr "Conectado: sus herramientas estarán disponibles desde tu próximo mensaje." @@ -2322,6 +2354,7 @@ msgstr "Desconectando…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Descartar" @@ -2381,7 +2414,9 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/message-cards.tsx #: src/pages/shell/project-overview.tsx +#: src/pages/shell/transcript.tsx msgid "Done" msgstr "Listo" @@ -2394,8 +2429,11 @@ msgstr "" msgid "Dot" msgstr "" +#. placeholder {0}: activeItem.name +#. placeholder {0}: item.name #. placeholder {0}: props.name #: src/components/ArtifactFileCard.tsx +#: src/pages/shell/message-cards.tsx msgid "Download {0}" msgstr "Descargar {0}" @@ -2403,7 +2441,6 @@ msgstr "Descargar {0}" #: src/components/ArtifactPane.tsx #: src/components/media/MediaCard.tsx #: src/components/media/MediaPlayer.tsx -#: src/pages/shell/message-cards.tsx msgid "Download {name}" msgstr "Descargar {name}" @@ -2589,6 +2626,8 @@ msgid "Expand" msgstr "Expandir" #. placeholder {0}: group.title +#. placeholder {0}: item.name +#: src/pages/shell/message-cards.tsx #: src/pages/shell/sidebar-group-header.tsx msgid "Expand {0}" msgstr "Expandir {0}" @@ -2660,6 +2699,10 @@ msgstr "" msgid "Failed to open file" msgstr "" +#: src/pages/Shell.tsx +msgid "Failed to remove message" +msgstr "" + #: src/pages/Shell.tsx msgid "Failed to send message" msgstr "No se pudo enviar el mensaje" @@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…" msgstr "" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Hi, this is how I'll sound when I read replies out loud." -msgstr "Hola, así sonaré cuando lea las respuestas en voz alta." +msgid "Hi, this is how I'll sound when we talk." +msgstr "" #: src/pages/Shell.tsx +#: src/pages/shell/sidebar-header.tsx msgid "Hide bots" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Hide details" +msgstr "" + #: src/pages/Auth.tsx msgid "Hide password" msgstr "Ocultar contraseña" @@ -3080,6 +3128,7 @@ msgstr "" msgid "Import OpenAPI JSON" msgstr "Importar JSON de OpenAPI" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "In Bearbeitung" msgstr "" @@ -3166,6 +3215,10 @@ msgstr "Aislado" msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgstr "Su conversación, archivos y rutinas se eliminarán de forma permanente. Los bots que creó permanecerán en tu lista." +#: src/pages/shell/transcript.tsx +msgid "joined" +msgstr "" + #: src/pages/shell/transcript.tsx msgid "Jump to latest" msgstr "Ir al más reciente" @@ -3174,6 +3227,10 @@ msgstr "Ir al más reciente" msgid "Jump to replied message" msgstr "Ir al mensaje respondido" +#: src/pages/shell/project-overview.tsx +msgid "Just ask in the chat whenever something should happen regularly." +msgstr "" + #: src/pages/ActivityList.tsx #: src/pages/ProjectSessionTimeline.tsx msgid "just now" @@ -3774,6 +3831,7 @@ msgid "New space" msgstr "Nuevo space" #: src/components/CookingMode.tsx +#: src/pages/shell/message-cards.tsx msgid "Next" msgstr "" @@ -3885,6 +3943,10 @@ msgstr "" msgid "No providers found." msgstr "No se encontraron proveedores." +#: src/pages/ActivityList.tsx +msgid "No recent activity" +msgstr "" + #: src/components/integrations/IntegrationSetup.tsx msgid "No remote MCP servers found" msgstr "" @@ -3899,6 +3961,7 @@ msgid "No results" msgstr "Sin resultados" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "No runs yet" msgstr "Aún no hay ejecuciones" @@ -3957,6 +4020,7 @@ msgid "Notifications" msgstr "Notificaciones" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Now" msgstr "Ahora" @@ -4027,7 +4091,6 @@ msgid "Oo name" msgstr "" #: src/pages/ScratchpadSection.tsx -#: src/pages/shell/computer-panel-preview.tsx msgid "Open" msgstr "Abrir" @@ -4056,7 +4119,7 @@ msgstr "" msgid "Open in browser" msgstr "" -#: src/pages/shell/computer-panel-preview.tsx +#: src/components/browser/OoComputerBrowser.tsx msgid "Open in full window" msgstr "Abrir en ventana completa" @@ -4288,10 +4351,6 @@ msgstr "" msgid "Project" msgstr "" -#: src/pages/Shell.tsx -msgid "Project · sessions & folders" -msgstr "" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "Project folder" @@ -4399,6 +4458,10 @@ msgstr "" msgid "Ready" msgstr "" +#: src/pages/shell/project-overview.tsx +msgid "Ready" +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Reasoning effort" msgstr "Esfuerzo de razonamiento" @@ -4412,6 +4475,7 @@ msgid "Recall only" msgstr "" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Recent" msgstr "Recientes" @@ -4528,6 +4592,10 @@ msgstr "Quitar {0}" msgid "Remove folder" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Remove from queue" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" @@ -4653,6 +4721,11 @@ msgstr "Restaura el último espacio de trabajo guardado. Se pierde el trabajo si msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -4700,11 +4773,13 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/Shell.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/routine-event-line.tsx msgid "Routine" msgstr "Rutina" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Routines" msgstr "Rutinas" @@ -4718,6 +4793,7 @@ msgid "Run at" msgstr "Ejecutar a las" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Run history" msgstr "Historial de ejecuciones" @@ -4756,10 +4832,6 @@ msgstr "Ejecutando…" msgid "Runs when this bot receives a verified message from this provider." msgstr "" -#: src/pages/shell/side-panel-header.tsx -msgid "Sandbox" -msgstr "" - #: src/components/AskCard.tsx #: src/components/MediaSettings.tsx #: src/components/teach/SkillDraftCard.tsx @@ -4909,6 +4981,10 @@ msgstr "Enviar respuesta" msgid "Send it" msgstr "Enviarlo" +#: src/pages/shell/transcript.tsx +msgid "Send now" +msgstr "Enviar ahora" + #: src/pages/Auth.tsx msgid "Send reset link" msgstr "Enviar enlace de restablecimiento" @@ -5021,6 +5097,10 @@ msgstr "" msgid "Show computer" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Show details" +msgstr "" + #: src/pages/Shell.tsx msgid "Show less" msgstr "" @@ -5037,6 +5117,7 @@ msgstr "Mostrar más" msgid "Show password" msgstr "Mostrar contraseña" +#: src/pages/shell/project-overview.tsx #: src/pages/shell/side-panel-header.tsx msgid "Show settings" msgstr "Mostrar configuración" @@ -5172,6 +5253,7 @@ msgstr "" msgid "Speaking with intent…" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx #: src/lib/onboarding-copy.ts msgid "Specialist crew" msgstr "" @@ -5286,6 +5368,10 @@ msgstr "Detener enseñanza" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." @@ -5398,6 +5484,7 @@ msgstr "Enseñanza en curso: detén la enseñanza antes de enviar un nuevo mensa #: src/pages/ProjectSessionTimeline.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-overview.tsx msgid "Team" msgstr "Equipo" @@ -5512,10 +5599,6 @@ msgstr "Este inicio de sesión por suscripción aún no está disponible en Raka msgid "This will permanently delete this project and all its conversations, bots, and memory." msgstr "" -#: src/pages/Shell.tsx -msgid "Threads by topic · folders" -msgstr "" - #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx msgid "Tiers" @@ -5701,6 +5784,7 @@ msgstr "" #: src/components/DesktopUpdates.tsx #: src/components/SoftwareUpdateSection.tsx +#: src/pages/AccountSettingsOverlay.tsx msgid "Up to date" msgstr "Actualizado" @@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead." msgstr "" #: src/components/stage/OoStage.tsx +#: src/pages/shell/project-overview.tsx msgid "Working" msgstr "" @@ -6037,6 +6122,7 @@ msgstr "Ayer" #: src/components/UserProfileSettings.tsx #: src/components/voice/VoiceStage.tsx #: src/pages/Shell.tsx +#: src/pages/shell/transcript.tsx msgid "You" msgstr "Tú" @@ -6077,13 +6163,25 @@ msgid "Your name" msgstr "Tu nombre" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Your Oo reads replies aloud when this is on." +msgid "Your Oo talks with you naturally when this is on." msgstr "" #: src/pages/Onboarding.tsx msgid "Your Oo will communicate in this language. You can change it later." msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Your profile" +msgstr "" + #: src/pages/Welcome.tsx msgid "Your specialists. One calm place to talk." msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom in" +msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom out" +msgstr "" diff --git a/apps/web/src/locales/hi/messages.po b/apps/web/src/locales/hi/messages.po index e195083..effdf3c 100644 --- a/apps/web/src/locales/hi/messages.po +++ b/apps/web/src/locales/hi/messages.po @@ -161,11 +161,6 @@ msgstr "" msgid "{0}'s browser appears here when the computer is running." msgstr "" -#. placeholder {0}: active.name -#: src/pages/shell/computer-panel-preview.tsx -msgid "{0}'s screen" -msgstr "" - #: src/pages/HeuteBrief.tsx msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgstr "" @@ -182,10 +177,38 @@ msgstr "" msgid "{botName} clears the path…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is building" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is checking a problem" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "{botName} is on it…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is replying" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is researching" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is thinking" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is waiting for your answer" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is working" +msgstr "" + #: src/lib/voice-persona-copy.ts #: src/pages/CallView.tsx msgid "{botName} is working…" @@ -364,6 +387,7 @@ msgstr "{remaining} शेष · बॉट देख रहा है, कार msgid "{size} B" msgstr "{size} B" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "{stepCount, plural, one {# step} other {# steps}}" msgstr "" @@ -386,10 +410,6 @@ msgstr "{title}, {label}" msgid "{toolCount, plural, one {# tool} other {# tools}}" msgstr "" -#: src/pages/shell/transcript.tsx -msgid "{workingBotName} is working" -msgstr "{workingBotName} काम कर रहा है" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "/Users/you/projects/acme" @@ -544,11 +564,16 @@ msgstr "" msgid "Add as projects" msgstr "" +#: src/components/NotificationSettingsPanel.tsx +msgid "Add evOo to your Home Screen via Share to enable push on iOS." +msgstr "" + #: src/pages/PluginsOverlay.tsx msgid "Add Executor" msgstr "Executor जोड़ें" #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Add goal" msgstr "" @@ -737,6 +762,14 @@ msgstr "हमेशा अनुमत" msgid "Analysiert die Aufgabe und führt Tools aus…" msgstr "" +#: src/pages/Shell.tsx +msgid "Analyze the project and give me a brief overview." +msgstr "" + +#: src/pages/shell/project-overview.tsx +msgid "Analyzing project…" +msgstr "" + #: src/components/AskCard.tsx msgid "Answer" msgstr "उत्तर" @@ -996,7 +1029,7 @@ msgstr "" msgid "Booting live desktop…" msgstr "लाइव डेस्कटॉप शुरू हो रहा है…" -#. placeholder {0}: active?.name +#. placeholder {0}: (active ?? coordinatorBot)?.name #: src/pages/Shell.tsx msgid "Booting up {0}’s computer" msgstr "{0} का कंप्यूटर शुरू हो रहा है" @@ -1019,10 +1052,6 @@ msgstr "बॉट" msgid "Bot screen" msgstr "बॉट स्क्रीन" -#: src/pages/shell/computer-panel-preview.tsx -msgid "Bot screen preview" -msgstr "बॉट स्क्रीन प्रीव्यू" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Bot to link" msgstr "" @@ -1031,10 +1060,6 @@ msgstr "" msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first." msgstr "बॉट डिफ़ॉल्ट रूप से बिना पूछे कार्रवाई करते हैं। अपवाद तभी जोड़ें जब आप किसी प्रकार की कार्रवाई की पहले समीक्षा करना चाहते हों।" -#: src/pages/shell/transcript.tsx -msgid "Bots are working" -msgstr "बॉट काम कर रहे हैं" - #: src/components/OoAtelier.tsx msgid "Bow" msgstr "" @@ -1064,7 +1089,6 @@ msgstr "" msgid "Browse MCP servers" msgstr "" -#: src/pages/Shell.tsx #: src/pages/shell/side-panel-header.tsx msgid "Browser" msgstr "" @@ -1493,6 +1517,10 @@ msgstr "कंप्यूटर" msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "कंप्यूटर बंद हैं। SANDBOX_PROVIDER=docker और SANDBOX_SUPERVISOR_TOKEN सेट करें, या SANDBOX_PROVIDER को e2b, daytona, या box पर सेट कर उसका API key दें। .env बदलने के बाद स्टैक फिर बनाएँ।" +#: src/pages/PluginsOverlay.tsx +msgid "Configure a plugin catalog on the server to connect apps." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Configured by deployment" msgstr "डिप्लॉयमेंट द्वारा कॉन्फ़िगर" @@ -1584,6 +1612,10 @@ msgstr "{0} कनेक्ट किया गया और उपयोग म msgid "Connected app" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Connected tools" +msgstr "" + #: src/pages/shell/message-cards.tsx msgid "Connected. Its tools are available from your next message." msgstr "कनेक्टेड। इसके टूल आपके अगले संदेश से उपलब्ध हैं।" @@ -2322,6 +2354,7 @@ msgstr "डिस्कनेक्ट हो रहा है…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -2381,7 +2414,9 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/message-cards.tsx #: src/pages/shell/project-overview.tsx +#: src/pages/shell/transcript.tsx msgid "Done" msgstr "पूर्ण" @@ -2394,8 +2429,11 @@ msgstr "" msgid "Dot" msgstr "" +#. placeholder {0}: activeItem.name +#. placeholder {0}: item.name #. placeholder {0}: props.name #: src/components/ArtifactFileCard.tsx +#: src/pages/shell/message-cards.tsx msgid "Download {0}" msgstr "{0} डाउनलोड करें" @@ -2403,7 +2441,6 @@ msgstr "{0} डाउनलोड करें" #: src/components/ArtifactPane.tsx #: src/components/media/MediaCard.tsx #: src/components/media/MediaPlayer.tsx -#: src/pages/shell/message-cards.tsx msgid "Download {name}" msgstr "{name} डाउनलोड करें" @@ -2589,6 +2626,8 @@ msgid "Expand" msgstr "विस्तृत करें" #. placeholder {0}: group.title +#. placeholder {0}: item.name +#: src/pages/shell/message-cards.tsx #: src/pages/shell/sidebar-group-header.tsx msgid "Expand {0}" msgstr "" @@ -2660,6 +2699,10 @@ msgstr "" msgid "Failed to open file" msgstr "" +#: src/pages/Shell.tsx +msgid "Failed to remove message" +msgstr "" + #: src/pages/Shell.tsx msgid "Failed to send message" msgstr "संदेश भेजने में विफल" @@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…" msgstr "" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Hi, this is how I'll sound when I read replies out loud." -msgstr "नमस्ते, उत्तर पढ़कर सुनाते समय मेरी आवाज़ ऐसी होगी।" +msgid "Hi, this is how I'll sound when we talk." +msgstr "" #: src/pages/Shell.tsx +#: src/pages/shell/sidebar-header.tsx msgid "Hide bots" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Hide details" +msgstr "" + #: src/pages/Auth.tsx msgid "Hide password" msgstr "" @@ -3080,6 +3128,7 @@ msgstr "" msgid "Import OpenAPI JSON" msgstr "OpenAPI JSON इम्पोर्ट करें" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "In Bearbeitung" msgstr "" @@ -3166,6 +3215,10 @@ msgstr "पृथक" msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgstr "इसकी बातचीत, फ़ाइलें और रूटीन स्थायी रूप से डिलीट कर दिए जाएंगे। इसके बनाए बॉट आपकी सूची में बने रहेंगे।" +#: src/pages/shell/transcript.tsx +msgid "joined" +msgstr "" + #: src/pages/shell/transcript.tsx msgid "Jump to latest" msgstr "नवीनतम संदेश पर जाएँ" @@ -3174,6 +3227,10 @@ msgstr "नवीनतम संदेश पर जाएँ" msgid "Jump to replied message" msgstr "जिस संदेश का उत्तर दिया गया उस पर जाएं" +#: src/pages/shell/project-overview.tsx +msgid "Just ask in the chat whenever something should happen regularly." +msgstr "" + #: src/pages/ActivityList.tsx #: src/pages/ProjectSessionTimeline.tsx msgid "just now" @@ -3774,6 +3831,7 @@ msgid "New space" msgstr "" #: src/components/CookingMode.tsx +#: src/pages/shell/message-cards.tsx msgid "Next" msgstr "" @@ -3885,6 +3943,10 @@ msgstr "" msgid "No providers found." msgstr "कोई प्रोवाइडर नहीं मिला।" +#: src/pages/ActivityList.tsx +msgid "No recent activity" +msgstr "" + #: src/components/integrations/IntegrationSetup.tsx msgid "No remote MCP servers found" msgstr "" @@ -3899,6 +3961,7 @@ msgid "No results" msgstr "कोई परिणाम नहीं" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "No runs yet" msgstr "" @@ -3957,6 +4020,7 @@ msgid "Notifications" msgstr "सूचनाएं" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Now" msgstr "अभी" @@ -4027,7 +4091,6 @@ msgid "Oo name" msgstr "" #: src/pages/ScratchpadSection.tsx -#: src/pages/shell/computer-panel-preview.tsx msgid "Open" msgstr "खोलें" @@ -4056,7 +4119,7 @@ msgstr "" msgid "Open in browser" msgstr "" -#: src/pages/shell/computer-panel-preview.tsx +#: src/components/browser/OoComputerBrowser.tsx msgid "Open in full window" msgstr "पूरी विंडो में खोलें" @@ -4288,10 +4351,6 @@ msgstr "" msgid "Project" msgstr "" -#: src/pages/Shell.tsx -msgid "Project · sessions & folders" -msgstr "" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "Project folder" @@ -4399,6 +4458,10 @@ msgstr "" msgid "Ready" msgstr "" +#: src/pages/shell/project-overview.tsx +msgid "Ready" +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Reasoning effort" msgstr "रीज़निंग प्रयास" @@ -4412,6 +4475,7 @@ msgid "Recall only" msgstr "" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Recent" msgstr "हाल के" @@ -4528,6 +4592,10 @@ msgstr "{0} हटाएं" msgid "Remove folder" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Remove from queue" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" @@ -4653,6 +4721,11 @@ msgstr "अंतिम सहेजे गए वर्कस्पेस क msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -4700,11 +4773,13 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/Shell.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/routine-event-line.tsx msgid "Routine" msgstr "रूटीन" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Routines" msgstr "रूटीन्स" @@ -4718,6 +4793,7 @@ msgid "Run at" msgstr "" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Run history" msgstr "" @@ -4756,10 +4832,6 @@ msgstr "चल रहा है…" msgid "Runs when this bot receives a verified message from this provider." msgstr "" -#: src/pages/shell/side-panel-header.tsx -msgid "Sandbox" -msgstr "" - #: src/components/AskCard.tsx #: src/components/MediaSettings.tsx #: src/components/teach/SkillDraftCard.tsx @@ -4909,6 +4981,10 @@ msgstr "उत्तर भेजें" msgid "Send it" msgstr "इसे भेजें" +#: src/pages/shell/transcript.tsx +msgid "Send now" +msgstr "अभी भेजें" + #: src/pages/Auth.tsx msgid "Send reset link" msgstr "रीसेट लिंक भेजें" @@ -5021,6 +5097,10 @@ msgstr "" msgid "Show computer" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Show details" +msgstr "" + #: src/pages/Shell.tsx msgid "Show less" msgstr "" @@ -5037,6 +5117,7 @@ msgstr "और दिखाएँ" msgid "Show password" msgstr "" +#: src/pages/shell/project-overview.tsx #: src/pages/shell/side-panel-header.tsx msgid "Show settings" msgstr "सेटिंग्स दिखाएं" @@ -5172,6 +5253,7 @@ msgstr "" msgid "Speaking with intent…" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx #: src/lib/onboarding-copy.ts msgid "Specialist crew" msgstr "" @@ -5286,6 +5368,10 @@ msgstr "सिखाना रोकें" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." @@ -5398,6 +5484,7 @@ msgstr "सिखाना जारी है। नया संदेश भ #: src/pages/ProjectSessionTimeline.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-overview.tsx msgid "Team" msgstr "टीम" @@ -5512,10 +5599,6 @@ msgstr "यह सब्सक्रिप्शन साइन-इन अभ msgid "This will permanently delete this project and all its conversations, bots, and memory." msgstr "" -#: src/pages/Shell.tsx -msgid "Threads by topic · folders" -msgstr "" - #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx msgid "Tiers" @@ -5701,6 +5784,7 @@ msgstr "" #: src/components/DesktopUpdates.tsx #: src/components/SoftwareUpdateSection.tsx +#: src/pages/AccountSettingsOverlay.tsx msgid "Up to date" msgstr "" @@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead." msgstr "" #: src/components/stage/OoStage.tsx +#: src/pages/shell/project-overview.tsx msgid "Working" msgstr "" @@ -6037,6 +6122,7 @@ msgstr "कल" #: src/components/UserProfileSettings.tsx #: src/components/voice/VoiceStage.tsx #: src/pages/Shell.tsx +#: src/pages/shell/transcript.tsx msgid "You" msgstr "आप" @@ -6077,13 +6163,25 @@ msgid "Your name" msgstr "आपका नाम" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Your Oo reads replies aloud when this is on." +msgid "Your Oo talks with you naturally when this is on." msgstr "" #: src/pages/Onboarding.tsx msgid "Your Oo will communicate in this language. You can change it later." msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Your profile" +msgstr "" + #: src/pages/Welcome.tsx msgid "Your specialists. One calm place to talk." msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom in" +msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom out" +msgstr "" diff --git a/apps/web/src/locales/ko/messages.po b/apps/web/src/locales/ko/messages.po index 512f3d4..1b29905 100644 --- a/apps/web/src/locales/ko/messages.po +++ b/apps/web/src/locales/ko/messages.po @@ -161,11 +161,6 @@ msgstr "" msgid "{0}'s browser appears here when the computer is running." msgstr "" -#. placeholder {0}: active.name -#: src/pages/shell/computer-panel-preview.tsx -msgid "{0}'s screen" -msgstr "{0}의 화면" - #: src/pages/HeuteBrief.tsx msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgstr "" @@ -182,10 +177,38 @@ msgstr "" msgid "{botName} clears the path…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is building" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is checking a problem" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "{botName} is on it…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is replying" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is researching" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is thinking" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is waiting for your answer" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is working" +msgstr "" + #: src/lib/voice-persona-copy.ts #: src/pages/CallView.tsx msgid "{botName} is working…" @@ -364,6 +387,7 @@ msgstr "남은 시간 {remaining} · Bot은 화면만 보고 직접 조작하지 msgid "{size} B" msgstr "{size} B" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "{stepCount, plural, one {# step} other {# steps}}" msgstr "" @@ -386,10 +410,6 @@ msgstr "{title}, {label}" msgid "{toolCount, plural, one {# tool} other {# tools}}" msgstr "" -#: src/pages/shell/transcript.tsx -msgid "{workingBotName} is working" -msgstr "{workingBotName} 작업 중" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "/Users/you/projects/acme" @@ -544,11 +564,16 @@ msgstr "" msgid "Add as projects" msgstr "" +#: src/components/NotificationSettingsPanel.tsx +msgid "Add evOo to your Home Screen via Share to enable push on iOS." +msgstr "" + #: src/pages/PluginsOverlay.tsx msgid "Add Executor" msgstr "Executor 추가" #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Add goal" msgstr "" @@ -737,6 +762,14 @@ msgstr "항상 허용됨" msgid "Analysiert die Aufgabe und führt Tools aus…" msgstr "" +#: src/pages/Shell.tsx +msgid "Analyze the project and give me a brief overview." +msgstr "" + +#: src/pages/shell/project-overview.tsx +msgid "Analyzing project…" +msgstr "" + #: src/components/AskCard.tsx msgid "Answer" msgstr "답변" @@ -996,7 +1029,7 @@ msgstr "" msgid "Booting live desktop…" msgstr "라이브 데스크톱 시작 중…" -#. placeholder {0}: active?.name +#. placeholder {0}: (active ?? coordinatorBot)?.name #: src/pages/Shell.tsx msgid "Booting up {0}’s computer" msgstr "{0}의 컴퓨터를 켜는 중" @@ -1019,10 +1052,6 @@ msgstr "봇" msgid "Bot screen" msgstr "Bot 화면" -#: src/pages/shell/computer-panel-preview.tsx -msgid "Bot screen preview" -msgstr "Bot 화면 미리보기" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Bot to link" msgstr "연결할 Bot" @@ -1031,10 +1060,6 @@ msgstr "연결할 Bot" msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first." msgstr "Bot은 기본적으로 묻지 않고 작업합니다. 먼저 확인하고 싶은 작업만 예외로 추가하세요." -#: src/pages/shell/transcript.tsx -msgid "Bots are working" -msgstr "봇 작업 중" - #: src/components/OoAtelier.tsx msgid "Bow" msgstr "" @@ -1064,7 +1089,6 @@ msgstr "" msgid "Browse MCP servers" msgstr "" -#: src/pages/Shell.tsx #: src/pages/shell/side-panel-header.tsx msgid "Browser" msgstr "" @@ -1493,6 +1517,10 @@ msgstr "컴퓨터" msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "컴퓨터가 꺼져 있습니다. SANDBOX_PROVIDER=docker와 SANDBOX_SUPERVISOR_TOKEN을 설정하거나, SANDBOX_PROVIDER를 e2b, daytona, box 중 하나로 두고 해당 API 키를 넣으세요. .env 변경 후 스택을 다시 만드세요." +#: src/pages/PluginsOverlay.tsx +msgid "Configure a plugin catalog on the server to connect apps." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Configured by deployment" msgstr "서버에서 설정됨" @@ -1584,6 +1612,10 @@ msgstr "{0}에 연결하고 기본 모델로 설정했습니다." msgid "Connected app" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Connected tools" +msgstr "" + #: src/pages/shell/message-cards.tsx msgid "Connected. Its tools are available from your next message." msgstr "연결되었습니다. 다음 메시지부터 이 도구를 사용할 수 있습니다." @@ -2322,6 +2354,7 @@ msgstr "연결 해제 중…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "닫기" @@ -2381,7 +2414,9 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/message-cards.tsx #: src/pages/shell/project-overview.tsx +#: src/pages/shell/transcript.tsx msgid "Done" msgstr "완료" @@ -2394,8 +2429,11 @@ msgstr "" msgid "Dot" msgstr "" +#. placeholder {0}: activeItem.name +#. placeholder {0}: item.name #. placeholder {0}: props.name #: src/components/ArtifactFileCard.tsx +#: src/pages/shell/message-cards.tsx msgid "Download {0}" msgstr "{0} 다운로드" @@ -2403,7 +2441,6 @@ msgstr "{0} 다운로드" #: src/components/ArtifactPane.tsx #: src/components/media/MediaCard.tsx #: src/components/media/MediaPlayer.tsx -#: src/pages/shell/message-cards.tsx msgid "Download {name}" msgstr "{name} 다운로드" @@ -2589,6 +2626,8 @@ msgid "Expand" msgstr "펼치기" #. placeholder {0}: group.title +#. placeholder {0}: item.name +#: src/pages/shell/message-cards.tsx #: src/pages/shell/sidebar-group-header.tsx msgid "Expand {0}" msgstr "{0} 펼치기" @@ -2660,6 +2699,10 @@ msgstr "" msgid "Failed to open file" msgstr "" +#: src/pages/Shell.tsx +msgid "Failed to remove message" +msgstr "" + #: src/pages/Shell.tsx msgid "Failed to send message" msgstr "메시지를 보내지 못했습니다." @@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…" msgstr "" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Hi, this is how I'll sound when I read replies out loud." -msgstr "안녕하세요. 답변을 소리 내어 읽을 때 이런 목소리로 들립니다." +msgid "Hi, this is how I'll sound when we talk." +msgstr "" #: src/pages/Shell.tsx +#: src/pages/shell/sidebar-header.tsx msgid "Hide bots" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Hide details" +msgstr "" + #: src/pages/Auth.tsx msgid "Hide password" msgstr "비밀번호 숨기기" @@ -3080,6 +3128,7 @@ msgstr "" msgid "Import OpenAPI JSON" msgstr "OpenAPI JSON 가져오기" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "In Bearbeitung" msgstr "" @@ -3166,6 +3215,10 @@ msgstr "이 Bot만 사용" msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgstr "대화, 파일, 자동 실행이 영구 삭제됩니다. 이 Bot이 만든 다른 Bot은 목록에 남습니다." +#: src/pages/shell/transcript.tsx +msgid "joined" +msgstr "" + #: src/pages/shell/transcript.tsx msgid "Jump to latest" msgstr "최신 메시지로 이동" @@ -3174,6 +3227,10 @@ msgstr "최신 메시지로 이동" msgid "Jump to replied message" msgstr "답장한 메시지로 이동" +#: src/pages/shell/project-overview.tsx +msgid "Just ask in the chat whenever something should happen regularly." +msgstr "" + #: src/pages/ActivityList.tsx #: src/pages/ProjectSessionTimeline.tsx msgid "just now" @@ -3774,6 +3831,7 @@ msgid "New space" msgstr "새 스페이스" #: src/components/CookingMode.tsx +#: src/pages/shell/message-cards.tsx msgid "Next" msgstr "" @@ -3885,6 +3943,10 @@ msgstr "" msgid "No providers found." msgstr "검색된 AI 서비스가 없습니다." +#: src/pages/ActivityList.tsx +msgid "No recent activity" +msgstr "" + #: src/components/integrations/IntegrationSetup.tsx msgid "No remote MCP servers found" msgstr "" @@ -3899,6 +3961,7 @@ msgid "No results" msgstr "검색 결과가 없습니다" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "No runs yet" msgstr "아직 실행 기록이 없습니다" @@ -3957,6 +4020,7 @@ msgid "Notifications" msgstr "알림" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Now" msgstr "지금" @@ -4027,7 +4091,6 @@ msgid "Oo name" msgstr "" #: src/pages/ScratchpadSection.tsx -#: src/pages/shell/computer-panel-preview.tsx msgid "Open" msgstr "열기" @@ -4056,7 +4119,7 @@ msgstr "" msgid "Open in browser" msgstr "" -#: src/pages/shell/computer-panel-preview.tsx +#: src/components/browser/OoComputerBrowser.tsx msgid "Open in full window" msgstr "전체 창으로 열기" @@ -4288,10 +4351,6 @@ msgstr "" msgid "Project" msgstr "" -#: src/pages/Shell.tsx -msgid "Project · sessions & folders" -msgstr "" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "Project folder" @@ -4399,6 +4458,10 @@ msgstr "" msgid "Ready" msgstr "" +#: src/pages/shell/project-overview.tsx +msgid "Ready" +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Reasoning effort" msgstr "추론 강도" @@ -4412,6 +4475,7 @@ msgid "Recall only" msgstr "" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Recent" msgstr "최근" @@ -4528,6 +4592,10 @@ msgstr "{0} 삭제" msgid "Remove folder" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Remove from queue" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" @@ -4653,6 +4721,11 @@ msgstr "마지막으로 저장된 작업 공간을 복원합니다. 컴퓨터에 msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -4700,11 +4773,13 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/Shell.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/routine-event-line.tsx msgid "Routine" msgstr "자동 실행" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Routines" msgstr "자동 실행" @@ -4718,6 +4793,7 @@ msgid "Run at" msgstr "실행 시간" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Run history" msgstr "실행 기록" @@ -4756,10 +4832,6 @@ msgstr "실행 중…" msgid "Runs when this bot receives a verified message from this provider." msgstr "" -#: src/pages/shell/side-panel-header.tsx -msgid "Sandbox" -msgstr "" - #: src/components/AskCard.tsx #: src/components/MediaSettings.tsx #: src/components/teach/SkillDraftCard.tsx @@ -4909,6 +4981,10 @@ msgstr "답변 보내기" msgid "Send it" msgstr "보내기" +#: src/pages/shell/transcript.tsx +msgid "Send now" +msgstr "지금 보내기" + #: src/pages/Auth.tsx msgid "Send reset link" msgstr "재설정 링크 보내기" @@ -5021,6 +5097,10 @@ msgstr "" msgid "Show computer" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Show details" +msgstr "" + #: src/pages/Shell.tsx msgid "Show less" msgstr "" @@ -5037,6 +5117,7 @@ msgstr "더 보기" msgid "Show password" msgstr "비밀번호 표시" +#: src/pages/shell/project-overview.tsx #: src/pages/shell/side-panel-header.tsx msgid "Show settings" msgstr "설정 보기" @@ -5172,6 +5253,7 @@ msgstr "" msgid "Speaking with intent…" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx #: src/lib/onboarding-copy.ts msgid "Specialist crew" msgstr "" @@ -5286,6 +5368,10 @@ msgstr "가르치기 종료" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." @@ -5398,6 +5484,7 @@ msgstr "작업을 가르치는 중입니다. 새 메시지를 보내려면 먼 #: src/pages/ProjectSessionTimeline.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-overview.tsx msgid "Team" msgstr "팀 공용" @@ -5512,10 +5599,6 @@ msgstr "이 구독 로그인 방식은 아직 Rakazo에서 지원되지 않습 msgid "This will permanently delete this project and all its conversations, bots, and memory." msgstr "" -#: src/pages/Shell.tsx -msgid "Threads by topic · folders" -msgstr "" - #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx msgid "Tiers" @@ -5701,6 +5784,7 @@ msgstr "" #: src/components/DesktopUpdates.tsx #: src/components/SoftwareUpdateSection.tsx +#: src/pages/AccountSettingsOverlay.tsx msgid "Up to date" msgstr "최신 상태" @@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead." msgstr "" #: src/components/stage/OoStage.tsx +#: src/pages/shell/project-overview.tsx msgid "Working" msgstr "" @@ -6037,6 +6122,7 @@ msgstr "어제" #: src/components/UserProfileSettings.tsx #: src/components/voice/VoiceStage.tsx #: src/pages/Shell.tsx +#: src/pages/shell/transcript.tsx msgid "You" msgstr "나" @@ -6077,13 +6163,25 @@ msgid "Your name" msgstr "이름" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Your Oo reads replies aloud when this is on." +msgid "Your Oo talks with you naturally when this is on." msgstr "" #: src/pages/Onboarding.tsx msgid "Your Oo will communicate in this language. You can change it later." msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Your profile" +msgstr "" + #: src/pages/Welcome.tsx msgid "Your specialists. One calm place to talk." msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom in" +msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom out" +msgstr "" diff --git a/apps/web/src/locales/pt-BR/messages.po b/apps/web/src/locales/pt-BR/messages.po index c828b0a..1227b2c 100644 --- a/apps/web/src/locales/pt-BR/messages.po +++ b/apps/web/src/locales/pt-BR/messages.po @@ -161,11 +161,6 @@ msgstr "" msgid "{0}'s browser appears here when the computer is running." msgstr "" -#. placeholder {0}: active.name -#: src/pages/shell/computer-panel-preview.tsx -msgid "{0}'s screen" -msgstr "" - #: src/pages/HeuteBrief.tsx msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgstr "" @@ -182,10 +177,38 @@ msgstr "" msgid "{botName} clears the path…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is building" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is checking a problem" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "{botName} is on it…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is replying" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is researching" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is thinking" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is waiting for your answer" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is working" +msgstr "" + #: src/lib/voice-persona-copy.ts #: src/pages/CallView.tsx msgid "{botName} is working…" @@ -364,6 +387,7 @@ msgstr "{remaining} restantes · o bot está observando, não agindo" msgid "{size} B" msgstr "{size} B" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "{stepCount, plural, one {# step} other {# steps}}" msgstr "" @@ -386,10 +410,6 @@ msgstr "{title}, {label}" msgid "{toolCount, plural, one {# tool} other {# tools}}" msgstr "" -#: src/pages/shell/transcript.tsx -msgid "{workingBotName} is working" -msgstr "{workingBotName} está trabalhando" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "/Users/you/projects/acme" @@ -544,11 +564,16 @@ msgstr "" msgid "Add as projects" msgstr "" +#: src/components/NotificationSettingsPanel.tsx +msgid "Add evOo to your Home Screen via Share to enable push on iOS." +msgstr "" + #: src/pages/PluginsOverlay.tsx msgid "Add Executor" msgstr "Adicionar Executor" #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Add goal" msgstr "" @@ -737,6 +762,14 @@ msgstr "Sempre permitido" msgid "Analysiert die Aufgabe und führt Tools aus…" msgstr "" +#: src/pages/Shell.tsx +msgid "Analyze the project and give me a brief overview." +msgstr "" + +#: src/pages/shell/project-overview.tsx +msgid "Analyzing project…" +msgstr "" + #: src/components/AskCard.tsx msgid "Answer" msgstr "Resposta" @@ -996,7 +1029,7 @@ msgstr "" msgid "Booting live desktop…" msgstr "Iniciando a área de trabalho ao vivo..." -#. placeholder {0}: active?.name +#. placeholder {0}: (active ?? coordinatorBot)?.name #: src/pages/Shell.tsx msgid "Booting up {0}’s computer" msgstr "Inicializando o computador do {0}" @@ -1019,10 +1052,6 @@ msgstr "Bot" msgid "Bot screen" msgstr "Tela do bot" -#: src/pages/shell/computer-panel-preview.tsx -msgid "Bot screen preview" -msgstr "Pré-visualização da tela" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Bot to link" msgstr "" @@ -1031,10 +1060,6 @@ msgstr "" msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first." msgstr "Os bots agem sem perguntar por padrão. Adicione uma exceção apenas quando quiser analisar um tipo de ação primeiro." -#: src/pages/shell/transcript.tsx -msgid "Bots are working" -msgstr "Bots estão trabalhando" - #: src/components/OoAtelier.tsx msgid "Bow" msgstr "" @@ -1064,7 +1089,6 @@ msgstr "" msgid "Browse MCP servers" msgstr "" -#: src/pages/Shell.tsx #: src/pages/shell/side-panel-header.tsx msgid "Browser" msgstr "" @@ -1493,6 +1517,10 @@ msgstr "Computadores" msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "Computadores estão desligados. Defina SANDBOX_PROVIDER=docker com SANDBOX_SUPERVISOR_TOKEN, ou SANDBOX_PROVIDER como e2b, daytona ou box com a chave de API. Recrie o stack após alterar o .env." +#: src/pages/PluginsOverlay.tsx +msgid "Configure a plugin catalog on the server to connect apps." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Configured by deployment" msgstr "Configurado por implantação" @@ -1584,6 +1612,10 @@ msgstr "Conectado e usando {0}." msgid "Connected app" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Connected tools" +msgstr "" + #: src/pages/shell/message-cards.tsx msgid "Connected. Its tools are available from your next message." msgstr "Conectado. Suas ferramentas estão disponíveis na sua próxima mensagem." @@ -2322,6 +2354,7 @@ msgstr "Desconectando…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -2381,7 +2414,9 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/message-cards.tsx #: src/pages/shell/project-overview.tsx +#: src/pages/shell/transcript.tsx msgid "Done" msgstr "Concluído" @@ -2394,8 +2429,11 @@ msgstr "" msgid "Dot" msgstr "" +#. placeholder {0}: activeItem.name +#. placeholder {0}: item.name #. placeholder {0}: props.name #: src/components/ArtifactFileCard.tsx +#: src/pages/shell/message-cards.tsx msgid "Download {0}" msgstr "Baixar {0}" @@ -2403,7 +2441,6 @@ msgstr "Baixar {0}" #: src/components/ArtifactPane.tsx #: src/components/media/MediaCard.tsx #: src/components/media/MediaPlayer.tsx -#: src/pages/shell/message-cards.tsx msgid "Download {name}" msgstr "Baixar {name}" @@ -2589,6 +2626,8 @@ msgid "Expand" msgstr "Expandir" #. placeholder {0}: group.title +#. placeholder {0}: item.name +#: src/pages/shell/message-cards.tsx #: src/pages/shell/sidebar-group-header.tsx msgid "Expand {0}" msgstr "" @@ -2660,6 +2699,10 @@ msgstr "" msgid "Failed to open file" msgstr "" +#: src/pages/Shell.tsx +msgid "Failed to remove message" +msgstr "" + #: src/pages/Shell.tsx msgid "Failed to send message" msgstr "Ocorreu um erro ao enviar a mensagem" @@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…" msgstr "" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Hi, this is how I'll sound when I read replies out loud." -msgstr "Olá, é assim que vou soar quando ler as respostas em voz alta." +msgid "Hi, this is how I'll sound when we talk." +msgstr "" #: src/pages/Shell.tsx +#: src/pages/shell/sidebar-header.tsx msgid "Hide bots" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Hide details" +msgstr "" + #: src/pages/Auth.tsx msgid "Hide password" msgstr "" @@ -3080,6 +3128,7 @@ msgstr "" msgid "Import OpenAPI JSON" msgstr "Importar OpenAPI JSON" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "In Bearbeitung" msgstr "" @@ -3166,6 +3215,10 @@ msgstr "Isolado" msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgstr "Suas conversas, arquivos e rotinas serão excluídos permanentemente. Os bots criados ficam na sua lista." +#: src/pages/shell/transcript.tsx +msgid "joined" +msgstr "" + #: src/pages/shell/transcript.tsx msgid "Jump to latest" msgstr "Ir para a mensagem mais recente" @@ -3174,6 +3227,10 @@ msgstr "Ir para a mensagem mais recente" msgid "Jump to replied message" msgstr "Ir para a mensagem respondida" +#: src/pages/shell/project-overview.tsx +msgid "Just ask in the chat whenever something should happen regularly." +msgstr "" + #: src/pages/ActivityList.tsx #: src/pages/ProjectSessionTimeline.tsx msgid "just now" @@ -3774,6 +3831,7 @@ msgid "New space" msgstr "" #: src/components/CookingMode.tsx +#: src/pages/shell/message-cards.tsx msgid "Next" msgstr "" @@ -3885,6 +3943,10 @@ msgstr "" msgid "No providers found." msgstr "Nenhum provedor encontrado" +#: src/pages/ActivityList.tsx +msgid "No recent activity" +msgstr "" + #: src/components/integrations/IntegrationSetup.tsx msgid "No remote MCP servers found" msgstr "" @@ -3899,6 +3961,7 @@ msgid "No results" msgstr "Nenhum resultado" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "No runs yet" msgstr "" @@ -3957,6 +4020,7 @@ msgid "Notifications" msgstr "Notificações" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Now" msgstr "Agora" @@ -4027,7 +4091,6 @@ msgid "Oo name" msgstr "" #: src/pages/ScratchpadSection.tsx -#: src/pages/shell/computer-panel-preview.tsx msgid "Open" msgstr "Abrir" @@ -4056,7 +4119,7 @@ msgstr "" msgid "Open in browser" msgstr "" -#: src/pages/shell/computer-panel-preview.tsx +#: src/components/browser/OoComputerBrowser.tsx msgid "Open in full window" msgstr "Abrir em janela cheia" @@ -4288,10 +4351,6 @@ msgstr "" msgid "Project" msgstr "" -#: src/pages/Shell.tsx -msgid "Project · sessions & folders" -msgstr "" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "Project folder" @@ -4399,6 +4458,10 @@ msgstr "" msgid "Ready" msgstr "" +#: src/pages/shell/project-overview.tsx +msgid "Ready" +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Reasoning effort" msgstr "Esforço de raciocínio" @@ -4412,6 +4475,7 @@ msgid "Recall only" msgstr "" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Recent" msgstr "Recente" @@ -4528,6 +4592,10 @@ msgstr "Remover {0}" msgid "Remove folder" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Remove from queue" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" @@ -4653,6 +4721,11 @@ msgstr "Restaure o último espaço de trabalho salvo. O trabalho não salvo no c msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -4700,11 +4773,13 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/Shell.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/routine-event-line.tsx msgid "Routine" msgstr "Rotina" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Routines" msgstr "Rotinas" @@ -4718,6 +4793,7 @@ msgid "Run at" msgstr "" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Run history" msgstr "" @@ -4756,10 +4832,6 @@ msgstr "Executando..." msgid "Runs when this bot receives a verified message from this provider." msgstr "" -#: src/pages/shell/side-panel-header.tsx -msgid "Sandbox" -msgstr "" - #: src/components/AskCard.tsx #: src/components/MediaSettings.tsx #: src/components/teach/SkillDraftCard.tsx @@ -4909,6 +4981,10 @@ msgstr "Enviar resposta" msgid "Send it" msgstr "Enviar" +#: src/pages/shell/transcript.tsx +msgid "Send now" +msgstr "Enviar agora" + #: src/pages/Auth.tsx msgid "Send reset link" msgstr "Enviar link de redefinição" @@ -5021,6 +5097,10 @@ msgstr "" msgid "Show computer" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Show details" +msgstr "" + #: src/pages/Shell.tsx msgid "Show less" msgstr "" @@ -5037,6 +5117,7 @@ msgstr "Mostrar mais" msgid "Show password" msgstr "" +#: src/pages/shell/project-overview.tsx #: src/pages/shell/side-panel-header.tsx msgid "Show settings" msgstr "Mostrar configurações" @@ -5172,6 +5253,7 @@ msgstr "" msgid "Speaking with intent…" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx #: src/lib/onboarding-copy.ts msgid "Specialist crew" msgstr "" @@ -5286,6 +5368,10 @@ msgstr "Parar de ensinar" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." @@ -5398,6 +5484,7 @@ msgstr "Ensino em andamento. Pare de ensinar antes de enviar uma nova mensagem." #: src/pages/ProjectSessionTimeline.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-overview.tsx msgid "Team" msgstr "Equipe" @@ -5512,10 +5599,6 @@ msgstr "Este login de assinatura ainda não está disponível no Rakazo. Use uma msgid "This will permanently delete this project and all its conversations, bots, and memory." msgstr "" -#: src/pages/Shell.tsx -msgid "Threads by topic · folders" -msgstr "" - #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx msgid "Tiers" @@ -5701,6 +5784,7 @@ msgstr "" #: src/components/DesktopUpdates.tsx #: src/components/SoftwareUpdateSection.tsx +#: src/pages/AccountSettingsOverlay.tsx msgid "Up to date" msgstr "" @@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead." msgstr "" #: src/components/stage/OoStage.tsx +#: src/pages/shell/project-overview.tsx msgid "Working" msgstr "" @@ -6037,6 +6122,7 @@ msgstr "Ontem" #: src/components/UserProfileSettings.tsx #: src/components/voice/VoiceStage.tsx #: src/pages/Shell.tsx +#: src/pages/shell/transcript.tsx msgid "You" msgstr "Você" @@ -6077,13 +6163,25 @@ msgid "Your name" msgstr "O seu nome" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Your Oo reads replies aloud when this is on." +msgid "Your Oo talks with you naturally when this is on." msgstr "" #: src/pages/Onboarding.tsx msgid "Your Oo will communicate in this language. You can change it later." msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Your profile" +msgstr "" + #: src/pages/Welcome.tsx msgid "Your specialists. One calm place to talk." msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom in" +msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom out" +msgstr "" diff --git a/apps/web/src/locales/ru/messages.po b/apps/web/src/locales/ru/messages.po index 60ca017..ec38a25 100644 --- a/apps/web/src/locales/ru/messages.po +++ b/apps/web/src/locales/ru/messages.po @@ -161,11 +161,6 @@ msgstr "" msgid "{0}'s browser appears here when the computer is running." msgstr "" -#. placeholder {0}: active.name -#: src/pages/shell/computer-panel-preview.tsx -msgid "{0}'s screen" -msgstr "Экран {0}" - #: src/pages/HeuteBrief.tsx msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgstr "" @@ -182,10 +177,38 @@ msgstr "" msgid "{botName} clears the path…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is building" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is checking a problem" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "{botName} is on it…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is replying" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is researching" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is thinking" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is waiting for your answer" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is working" +msgstr "" + #: src/lib/voice-persona-copy.ts #: src/pages/CallView.tsx msgid "{botName} is working…" @@ -364,6 +387,7 @@ msgstr "Осталось: {remaining} · бот наблюдает, но не д msgid "{size} B" msgstr "{size} Б" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "{stepCount, plural, one {# step} other {# steps}}" msgstr "" @@ -386,10 +410,6 @@ msgstr "{title}, {label}" msgid "{toolCount, plural, one {# tool} other {# tools}}" msgstr "{toolCount, plural, one {# инструмент} few {# инструмента} many {# инструментов} other {# инструмента}}" -#: src/pages/shell/transcript.tsx -msgid "{workingBotName} is working" -msgstr "{workingBotName} работает" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "/Users/you/projects/acme" @@ -544,11 +564,16 @@ msgstr "Добавить еще" msgid "Add as projects" msgstr "" +#: src/components/NotificationSettingsPanel.tsx +msgid "Add evOo to your Home Screen via Share to enable push on iOS." +msgstr "" + #: src/pages/PluginsOverlay.tsx msgid "Add Executor" msgstr "Добавить Executor" #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Add goal" msgstr "" @@ -737,6 +762,14 @@ msgstr "Всегда разрешено" msgid "Analysiert die Aufgabe und führt Tools aus…" msgstr "" +#: src/pages/Shell.tsx +msgid "Analyze the project and give me a brief overview." +msgstr "" + +#: src/pages/shell/project-overview.tsx +msgid "Analyzing project…" +msgstr "" + #: src/components/AskCard.tsx msgid "Answer" msgstr "Ответ" @@ -996,7 +1029,7 @@ msgstr "" msgid "Booting live desktop…" msgstr "Загрузка рабочего стола…" -#. placeholder {0}: active?.name +#. placeholder {0}: (active ?? coordinatorBot)?.name #: src/pages/Shell.tsx msgid "Booting up {0}’s computer" msgstr "Загрузка компьютера {0}" @@ -1019,10 +1052,6 @@ msgstr "Бот" msgid "Bot screen" msgstr "Экран бота" -#: src/pages/shell/computer-panel-preview.tsx -msgid "Bot screen preview" -msgstr "Предварительный просмотр экрана бота" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Bot to link" msgstr "Бот для ссылки" @@ -1031,10 +1060,6 @@ msgstr "Бот для ссылки" msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first." msgstr "По умолчанию боты действуют, не спрашивая. Добавляйте исключение только в том случае, если вы хотите сначала просмотреть тип действия." -#: src/pages/shell/transcript.tsx -msgid "Bots are working" -msgstr "Боты работают" - #: src/components/OoAtelier.tsx msgid "Bow" msgstr "" @@ -1064,7 +1089,6 @@ msgstr "" msgid "Browse MCP servers" msgstr "Просмотреть MCP-серверы" -#: src/pages/Shell.tsx #: src/pages/shell/side-panel-header.tsx msgid "Browser" msgstr "" @@ -1493,6 +1517,10 @@ msgstr "Компьютеры" msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "Компьютеры выключены. Установите SANDBOX_PROVIDER=docker с помощью SANDBOX_SUPERVISOR_TOKEN или установите для SANDBOX_PROVIDER значение e2b, Daytona или Box с его ключом API. Воссоздайте стек после изменения .env." +#: src/pages/PluginsOverlay.tsx +msgid "Configure a plugin catalog on the server to connect apps." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Configured by deployment" msgstr "Настраивается путем развертывания" @@ -1584,6 +1612,10 @@ msgstr "Подключено и используется {0}." msgid "Connected app" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Connected tools" +msgstr "" + #: src/pages/shell/message-cards.tsx msgid "Connected. Its tools are available from your next message." msgstr "Подключено. Его инструменты доступны из вашего следующего сообщения." @@ -2322,6 +2354,7 @@ msgstr "Отключение…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Закрыть" @@ -2381,7 +2414,9 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/message-cards.tsx #: src/pages/shell/project-overview.tsx +#: src/pages/shell/transcript.tsx msgid "Done" msgstr "Готово" @@ -2394,8 +2429,11 @@ msgstr "" msgid "Dot" msgstr "" +#. placeholder {0}: activeItem.name +#. placeholder {0}: item.name #. placeholder {0}: props.name #: src/components/ArtifactFileCard.tsx +#: src/pages/shell/message-cards.tsx msgid "Download {0}" msgstr "Скачать {0}" @@ -2403,7 +2441,6 @@ msgstr "Скачать {0}" #: src/components/ArtifactPane.tsx #: src/components/media/MediaCard.tsx #: src/components/media/MediaPlayer.tsx -#: src/pages/shell/message-cards.tsx msgid "Download {name}" msgstr "Скачать {name}" @@ -2589,6 +2626,8 @@ msgid "Expand" msgstr "Развернуть" #. placeholder {0}: group.title +#. placeholder {0}: item.name +#: src/pages/shell/message-cards.tsx #: src/pages/shell/sidebar-group-header.tsx msgid "Expand {0}" msgstr "Развернуть {0}" @@ -2660,6 +2699,10 @@ msgstr "" msgid "Failed to open file" msgstr "" +#: src/pages/Shell.tsx +msgid "Failed to remove message" +msgstr "" + #: src/pages/Shell.tsx msgid "Failed to send message" msgstr "Не удалось отправить сообщение" @@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…" msgstr "" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Hi, this is how I'll sound when I read replies out loud." -msgstr "Привет, вот как я буду звучать, когда буду читать ответы вслух." +msgid "Hi, this is how I'll sound when we talk." +msgstr "" #: src/pages/Shell.tsx +#: src/pages/shell/sidebar-header.tsx msgid "Hide bots" msgstr "Скрыть ботов" +#: src/pages/shell/transcript.tsx +msgid "Hide details" +msgstr "" + #: src/pages/Auth.tsx msgid "Hide password" msgstr "Скрыть пароль" @@ -3080,6 +3128,7 @@ msgstr "" msgid "Import OpenAPI JSON" msgstr "Импортировать OpenAPI JSON" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "In Bearbeitung" msgstr "" @@ -3166,6 +3215,10 @@ msgstr "изолированный" msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgstr "Его диалог, файлы и задачи будут безвозвратно удалены. Созданные им боты останутся в вашем списке." +#: src/pages/shell/transcript.tsx +msgid "joined" +msgstr "" + #: src/pages/shell/transcript.tsx msgid "Jump to latest" msgstr "Перейти к последнему" @@ -3174,6 +3227,10 @@ msgstr "Перейти к последнему" msgid "Jump to replied message" msgstr "Перейти к ответному сообщению" +#: src/pages/shell/project-overview.tsx +msgid "Just ask in the chat whenever something should happen regularly." +msgstr "" + #: src/pages/ActivityList.tsx #: src/pages/ProjectSessionTimeline.tsx msgid "just now" @@ -3774,6 +3831,7 @@ msgid "New space" msgstr "Новое пространство" #: src/components/CookingMode.tsx +#: src/pages/shell/message-cards.tsx msgid "Next" msgstr "" @@ -3885,6 +3943,10 @@ msgstr "" msgid "No providers found." msgstr "Поставщики не найдены." +#: src/pages/ActivityList.tsx +msgid "No recent activity" +msgstr "" + #: src/components/integrations/IntegrationSetup.tsx msgid "No remote MCP servers found" msgstr "Удаленные MCP-серверы не найдены" @@ -3899,6 +3961,7 @@ msgid "No results" msgstr "Нет результатов" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "No runs yet" msgstr "Запусков пока нет" @@ -3957,6 +4020,7 @@ msgid "Notifications" msgstr "Уведомления" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Now" msgstr "Сейчас" @@ -4027,7 +4091,6 @@ msgid "Oo name" msgstr "" #: src/pages/ScratchpadSection.tsx -#: src/pages/shell/computer-panel-preview.tsx msgid "Open" msgstr "Открыть" @@ -4056,7 +4119,7 @@ msgstr "" msgid "Open in browser" msgstr "" -#: src/pages/shell/computer-panel-preview.tsx +#: src/components/browser/OoComputerBrowser.tsx msgid "Open in full window" msgstr "Открыть в полном окне" @@ -4288,10 +4351,6 @@ msgstr "Приватное пространство со своими ботам msgid "Project" msgstr "" -#: src/pages/Shell.tsx -msgid "Project · sessions & folders" -msgstr "" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "Project folder" @@ -4399,6 +4458,10 @@ msgstr "" msgid "Ready" msgstr "" +#: src/pages/shell/project-overview.tsx +msgid "Ready" +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Reasoning effort" msgstr "" @@ -4412,6 +4475,7 @@ msgid "Recall only" msgstr "" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Recent" msgstr "Недавние" @@ -4528,6 +4592,10 @@ msgstr "Удалить {0}" msgid "Remove folder" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Remove from queue" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "Удалить событие Git" @@ -4653,6 +4721,11 @@ msgstr "Восстановите последнее сохраненное ра msgid "Restoring your workspace" msgstr "Восстановление вашего рабочего пространства" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -4700,11 +4773,13 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/Shell.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/routine-event-line.tsx msgid "Routine" msgstr "Задача" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Routines" msgstr "Задачи" @@ -4718,6 +4793,7 @@ msgid "Run at" msgstr "Запустить в" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Run history" msgstr "История запуска" @@ -4756,10 +4832,6 @@ msgstr "Выполняется…" msgid "Runs when this bot receives a verified message from this provider." msgstr "Запускается, когда этот бот получает проверенное сообщение от этого провайдера." -#: src/pages/shell/side-panel-header.tsx -msgid "Sandbox" -msgstr "" - #: src/components/AskCard.tsx #: src/components/MediaSettings.tsx #: src/components/teach/SkillDraftCard.tsx @@ -4909,6 +4981,10 @@ msgstr "Отправить ответ" msgid "Send it" msgstr "Отправить" +#: src/pages/shell/transcript.tsx +msgid "Send now" +msgstr "Отправить сейчас" + #: src/pages/Auth.tsx msgid "Send reset link" msgstr "Отправить ссылку для сброса" @@ -5021,6 +5097,10 @@ msgstr "Показать ботов" msgid "Show computer" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Show details" +msgstr "" + #: src/pages/Shell.tsx msgid "Show less" msgstr "" @@ -5037,6 +5117,7 @@ msgstr "Показать ещё" msgid "Show password" msgstr "Показать пароль" +#: src/pages/shell/project-overview.tsx #: src/pages/shell/side-panel-header.tsx msgid "Show settings" msgstr "Показать настройки" @@ -5172,6 +5253,7 @@ msgstr "" msgid "Speaking with intent…" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx #: src/lib/onboarding-copy.ts msgid "Specialist crew" msgstr "" @@ -5286,6 +5368,10 @@ msgstr "Остановить обучение" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." @@ -5398,6 +5484,7 @@ msgstr "Обучение в процессе. Прекратите обучен #: src/pages/ProjectSessionTimeline.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-overview.tsx msgid "Team" msgstr "Команда" @@ -5512,10 +5599,6 @@ msgstr "Вход по этой подписке пока недоступен в msgid "This will permanently delete this project and all its conversations, bots, and memory." msgstr "" -#: src/pages/Shell.tsx -msgid "Threads by topic · folders" -msgstr "" - #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx msgid "Tiers" @@ -5701,6 +5784,7 @@ msgstr "" #: src/components/DesktopUpdates.tsx #: src/components/SoftwareUpdateSection.tsx +#: src/pages/AccountSettingsOverlay.tsx msgid "Up to date" msgstr "Актуально" @@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead." msgstr "" #: src/components/stage/OoStage.tsx +#: src/pages/shell/project-overview.tsx msgid "Working" msgstr "" @@ -6037,6 +6122,7 @@ msgstr "Вчера" #: src/components/UserProfileSettings.tsx #: src/components/voice/VoiceStage.tsx #: src/pages/Shell.tsx +#: src/pages/shell/transcript.tsx msgid "You" msgstr "Вы" @@ -6077,13 +6163,25 @@ msgid "Your name" msgstr "Ваше имя" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Your Oo reads replies aloud when this is on." +msgid "Your Oo talks with you naturally when this is on." msgstr "" #: src/pages/Onboarding.tsx msgid "Your Oo will communicate in this language. You can change it later." msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Your profile" +msgstr "" + #: src/pages/Welcome.tsx msgid "Your specialists. One calm place to talk." msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom in" +msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom out" +msgstr "" diff --git a/apps/web/src/locales/tr/messages.po b/apps/web/src/locales/tr/messages.po index e3ccf9d..0f7a212 100644 --- a/apps/web/src/locales/tr/messages.po +++ b/apps/web/src/locales/tr/messages.po @@ -161,11 +161,6 @@ msgstr "" msgid "{0}'s browser appears here when the computer is running." msgstr "" -#. placeholder {0}: active.name -#: src/pages/shell/computer-panel-preview.tsx -msgid "{0}'s screen" -msgstr "" - #: src/pages/HeuteBrief.tsx msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgstr "" @@ -182,10 +177,38 @@ msgstr "" msgid "{botName} clears the path…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is building" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is checking a problem" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "{botName} is on it…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is replying" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is researching" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is thinking" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is waiting for your answer" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is working" +msgstr "" + #: src/lib/voice-persona-copy.ts #: src/pages/CallView.tsx msgid "{botName} is working…" @@ -364,6 +387,7 @@ msgstr "{remaining} kaldı · bot yalnızca izliyor" msgid "{size} B" msgstr "{size} B" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "{stepCount, plural, one {# step} other {# steps}}" msgstr "" @@ -386,10 +410,6 @@ msgstr "{title}, {label}" msgid "{toolCount, plural, one {# tool} other {# tools}}" msgstr "" -#: src/pages/shell/transcript.tsx -msgid "{workingBotName} is working" -msgstr "{workingBotName} çalışıyor" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "/Users/you/projects/acme" @@ -544,11 +564,16 @@ msgstr "" msgid "Add as projects" msgstr "" +#: src/components/NotificationSettingsPanel.tsx +msgid "Add evOo to your Home Screen via Share to enable push on iOS." +msgstr "" + #: src/pages/PluginsOverlay.tsx msgid "Add Executor" msgstr "Executor ekle" #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Add goal" msgstr "" @@ -737,6 +762,14 @@ msgstr "Her zaman izinli" msgid "Analysiert die Aufgabe und führt Tools aus…" msgstr "" +#: src/pages/Shell.tsx +msgid "Analyze the project and give me a brief overview." +msgstr "" + +#: src/pages/shell/project-overview.tsx +msgid "Analyzing project…" +msgstr "" + #: src/components/AskCard.tsx msgid "Answer" msgstr "Yanıtla" @@ -996,7 +1029,7 @@ msgstr "" msgid "Booting live desktop…" msgstr "Canlı masaüstü başlatılıyor…" -#. placeholder {0}: active?.name +#. placeholder {0}: (active ?? coordinatorBot)?.name #: src/pages/Shell.tsx msgid "Booting up {0}’s computer" msgstr "{0} adlı botun bilgisayarı başlatılıyor" @@ -1019,10 +1052,6 @@ msgstr "Bot" msgid "Bot screen" msgstr "Bot ekranı" -#: src/pages/shell/computer-panel-preview.tsx -msgid "Bot screen preview" -msgstr "Bot ekranı önizlemesi" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Bot to link" msgstr "" @@ -1031,10 +1060,6 @@ msgstr "" msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first." msgstr "Botlar varsayılan olarak sormadan hareket eder. Yalnızca belirli bir işlem türünü önce incelemek istediğinde istisna ekle." -#: src/pages/shell/transcript.tsx -msgid "Bots are working" -msgstr "Botlar çalışıyor" - #: src/components/OoAtelier.tsx msgid "Bow" msgstr "" @@ -1064,7 +1089,6 @@ msgstr "" msgid "Browse MCP servers" msgstr "" -#: src/pages/Shell.tsx #: src/pages/shell/side-panel-header.tsx msgid "Browser" msgstr "" @@ -1493,6 +1517,10 @@ msgstr "Bilgisayarlar" msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "Bilgisayarlar kapalı. SANDBOX_PROVIDER=docker ve SANDBOX_SUPERVISOR_TOKEN ayarlayın; ya da SANDBOX_PROVIDER değerini e2b, daytona veya box yapıp API anahtarını ekleyin. .env değişince stacki yeniden oluşturun." +#: src/pages/PluginsOverlay.tsx +msgid "Configure a plugin catalog on the server to connect apps." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Configured by deployment" msgstr "Dağıtım tarafından yapılandırıldı" @@ -1584,6 +1612,10 @@ msgstr "Bağlandı, {0} kullanılıyor." msgid "Connected app" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Connected tools" +msgstr "" + #: src/pages/shell/message-cards.tsx msgid "Connected. Its tools are available from your next message." msgstr "Bağlandı. Araçları bir sonraki mesajından itibaren kullanılabilir." @@ -2322,6 +2354,7 @@ msgstr "Bağlantı kesiliyor…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -2381,7 +2414,9 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/message-cards.tsx #: src/pages/shell/project-overview.tsx +#: src/pages/shell/transcript.tsx msgid "Done" msgstr "Bitti" @@ -2394,8 +2429,11 @@ msgstr "" msgid "Dot" msgstr "" +#. placeholder {0}: activeItem.name +#. placeholder {0}: item.name #. placeholder {0}: props.name #: src/components/ArtifactFileCard.tsx +#: src/pages/shell/message-cards.tsx msgid "Download {0}" msgstr "{0} indir" @@ -2403,7 +2441,6 @@ msgstr "{0} indir" #: src/components/ArtifactPane.tsx #: src/components/media/MediaCard.tsx #: src/components/media/MediaPlayer.tsx -#: src/pages/shell/message-cards.tsx msgid "Download {name}" msgstr "{name} indir" @@ -2589,6 +2626,8 @@ msgid "Expand" msgstr "Genişlet" #. placeholder {0}: group.title +#. placeholder {0}: item.name +#: src/pages/shell/message-cards.tsx #: src/pages/shell/sidebar-group-header.tsx msgid "Expand {0}" msgstr "" @@ -2660,6 +2699,10 @@ msgstr "" msgid "Failed to open file" msgstr "" +#: src/pages/Shell.tsx +msgid "Failed to remove message" +msgstr "" + #: src/pages/Shell.tsx msgid "Failed to send message" msgstr "Mesaj gönderilemedi" @@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…" msgstr "" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Hi, this is how I'll sound when I read replies out loud." -msgstr "Merhaba, yanıtları sesli okuduğumda böyle duyulacağım." +msgid "Hi, this is how I'll sound when we talk." +msgstr "" #: src/pages/Shell.tsx +#: src/pages/shell/sidebar-header.tsx msgid "Hide bots" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Hide details" +msgstr "" + #: src/pages/Auth.tsx msgid "Hide password" msgstr "" @@ -3080,6 +3128,7 @@ msgstr "" msgid "Import OpenAPI JSON" msgstr "OpenAPI JSON içe aktar" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "In Bearbeitung" msgstr "" @@ -3166,6 +3215,10 @@ msgstr "Yalıtılmış" msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgstr "Sohbeti, dosyaları ve rutinleri kalıcı olarak silinecek. Oluşturduğu botlar listende kalır." +#: src/pages/shell/transcript.tsx +msgid "joined" +msgstr "" + #: src/pages/shell/transcript.tsx msgid "Jump to latest" msgstr "En son mesaja git" @@ -3174,6 +3227,10 @@ msgstr "En son mesaja git" msgid "Jump to replied message" msgstr "Yanıtlanan mesaja git" +#: src/pages/shell/project-overview.tsx +msgid "Just ask in the chat whenever something should happen regularly." +msgstr "" + #: src/pages/ActivityList.tsx #: src/pages/ProjectSessionTimeline.tsx msgid "just now" @@ -3774,6 +3831,7 @@ msgid "New space" msgstr "" #: src/components/CookingMode.tsx +#: src/pages/shell/message-cards.tsx msgid "Next" msgstr "" @@ -3885,6 +3943,10 @@ msgstr "" msgid "No providers found." msgstr "Sağlayıcı bulunamadı." +#: src/pages/ActivityList.tsx +msgid "No recent activity" +msgstr "" + #: src/components/integrations/IntegrationSetup.tsx msgid "No remote MCP servers found" msgstr "" @@ -3899,6 +3961,7 @@ msgid "No results" msgstr "Sonuç yok" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "No runs yet" msgstr "" @@ -3957,6 +4020,7 @@ msgid "Notifications" msgstr "Bildirimler" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Now" msgstr "Şimdi" @@ -4027,7 +4091,6 @@ msgid "Oo name" msgstr "" #: src/pages/ScratchpadSection.tsx -#: src/pages/shell/computer-panel-preview.tsx msgid "Open" msgstr "Aç" @@ -4056,7 +4119,7 @@ msgstr "" msgid "Open in browser" msgstr "" -#: src/pages/shell/computer-panel-preview.tsx +#: src/components/browser/OoComputerBrowser.tsx msgid "Open in full window" msgstr "Tam pencerede aç" @@ -4288,10 +4351,6 @@ msgstr "" msgid "Project" msgstr "" -#: src/pages/Shell.tsx -msgid "Project · sessions & folders" -msgstr "" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "Project folder" @@ -4399,6 +4458,10 @@ msgstr "" msgid "Ready" msgstr "" +#: src/pages/shell/project-overview.tsx +msgid "Ready" +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Reasoning effort" msgstr "Akıl yürütme düzeyi" @@ -4412,6 +4475,7 @@ msgid "Recall only" msgstr "" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Recent" msgstr "Son" @@ -4528,6 +4592,10 @@ msgstr "{0} kaldır" msgid "Remove folder" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Remove from queue" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" @@ -4653,6 +4721,11 @@ msgstr "" msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -4700,11 +4773,13 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/Shell.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/routine-event-line.tsx msgid "Routine" msgstr "Rutin" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Routines" msgstr "Rutinler" @@ -4718,6 +4793,7 @@ msgid "Run at" msgstr "" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Run history" msgstr "" @@ -4756,10 +4832,6 @@ msgstr "Çalışıyor…" msgid "Runs when this bot receives a verified message from this provider." msgstr "" -#: src/pages/shell/side-panel-header.tsx -msgid "Sandbox" -msgstr "" - #: src/components/AskCard.tsx #: src/components/MediaSettings.tsx #: src/components/teach/SkillDraftCard.tsx @@ -4909,6 +4981,10 @@ msgstr "Yanıtı gönder" msgid "Send it" msgstr "Gönder" +#: src/pages/shell/transcript.tsx +msgid "Send now" +msgstr "Şimdi gönder" + #: src/pages/Auth.tsx msgid "Send reset link" msgstr "Sıfırlama bağlantısı gönder" @@ -5021,6 +5097,10 @@ msgstr "" msgid "Show computer" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Show details" +msgstr "" + #: src/pages/Shell.tsx msgid "Show less" msgstr "" @@ -5037,6 +5117,7 @@ msgstr "Daha fazla göster" msgid "Show password" msgstr "" +#: src/pages/shell/project-overview.tsx #: src/pages/shell/side-panel-header.tsx msgid "Show settings" msgstr "Ayarları göster" @@ -5172,6 +5253,7 @@ msgstr "" msgid "Speaking with intent…" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx #: src/lib/onboarding-copy.ts msgid "Specialist crew" msgstr "" @@ -5286,6 +5368,10 @@ msgstr "Öğretmeyi durdur" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." @@ -5398,6 +5484,7 @@ msgstr "Öğretme sürüyor. Yeni mesaj göndermeden önce öğretmeyi durdur." #: src/pages/ProjectSessionTimeline.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-overview.tsx msgid "Team" msgstr "Takım" @@ -5512,10 +5599,6 @@ msgstr "Bu abonelik girişi Rakazo'da henüz kullanılamıyor. Bir dağıtım ki msgid "This will permanently delete this project and all its conversations, bots, and memory." msgstr "" -#: src/pages/Shell.tsx -msgid "Threads by topic · folders" -msgstr "" - #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx msgid "Tiers" @@ -5701,6 +5784,7 @@ msgstr "" #: src/components/DesktopUpdates.tsx #: src/components/SoftwareUpdateSection.tsx +#: src/pages/AccountSettingsOverlay.tsx msgid "Up to date" msgstr "" @@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead." msgstr "" #: src/components/stage/OoStage.tsx +#: src/pages/shell/project-overview.tsx msgid "Working" msgstr "" @@ -6037,6 +6122,7 @@ msgstr "Dün" #: src/components/UserProfileSettings.tsx #: src/components/voice/VoiceStage.tsx #: src/pages/Shell.tsx +#: src/pages/shell/transcript.tsx msgid "You" msgstr "Sen" @@ -6077,13 +6163,25 @@ msgid "Your name" msgstr "Adın" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Your Oo reads replies aloud when this is on." +msgid "Your Oo talks with you naturally when this is on." msgstr "" #: src/pages/Onboarding.tsx msgid "Your Oo will communicate in this language. You can change it later." msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Your profile" +msgstr "" + #: src/pages/Welcome.tsx msgid "Your specialists. One calm place to talk." msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom in" +msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom out" +msgstr "" diff --git a/apps/web/src/locales/zh-CN/messages.po b/apps/web/src/locales/zh-CN/messages.po index 42d5df8..8294f7f 100644 --- a/apps/web/src/locales/zh-CN/messages.po +++ b/apps/web/src/locales/zh-CN/messages.po @@ -161,11 +161,6 @@ msgstr "" msgid "{0}'s browser appears here when the computer is running." msgstr "" -#. placeholder {0}: active.name -#: src/pages/shell/computer-panel-preview.tsx -msgid "{0}'s screen" -msgstr "" - #: src/pages/HeuteBrief.tsx msgid "{actionCount, plural, one {# thing needs you} other {# things need you}}" msgstr "" @@ -182,10 +177,38 @@ msgstr "" msgid "{botName} clears the path…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is building" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is checking a problem" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "{botName} is on it…" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "{botName} is replying" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is researching" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is thinking" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is waiting for your answer" +msgstr "" + +#: src/pages/shell/transcript.tsx +msgid "{botName} is working" +msgstr "" + #: src/lib/voice-persona-copy.ts #: src/pages/CallView.tsx msgid "{botName} is working…" @@ -364,6 +387,7 @@ msgstr "剩余 {remaining} · Bot 只看不操作" msgid "{size} B" msgstr "{size} B" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "{stepCount, plural, one {# step} other {# steps}}" msgstr "" @@ -386,10 +410,6 @@ msgstr "{title},{label}" msgid "{toolCount, plural, one {# tool} other {# tools}}" msgstr "" -#: src/pages/shell/transcript.tsx -msgid "{workingBotName} is working" -msgstr "{workingBotName} 正在工作" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "/Users/you/projects/acme" @@ -544,11 +564,16 @@ msgstr "" msgid "Add as projects" msgstr "" +#: src/components/NotificationSettingsPanel.tsx +msgid "Add evOo to your Home Screen via Share to enable push on iOS." +msgstr "" + #: src/pages/PluginsOverlay.tsx msgid "Add Executor" msgstr "添加 Executor" #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Add goal" msgstr "" @@ -737,6 +762,14 @@ msgstr "始终允许" msgid "Analysiert die Aufgabe und führt Tools aus…" msgstr "" +#: src/pages/Shell.tsx +msgid "Analyze the project and give me a brief overview." +msgstr "" + +#: src/pages/shell/project-overview.tsx +msgid "Analyzing project…" +msgstr "" + #: src/components/AskCard.tsx msgid "Answer" msgstr "回答" @@ -996,7 +1029,7 @@ msgstr "" msgid "Booting live desktop…" msgstr "正在启动实时桌面…" -#. placeholder {0}: active?.name +#. placeholder {0}: (active ?? coordinatorBot)?.name #: src/pages/Shell.tsx msgid "Booting up {0}’s computer" msgstr "正在启动 {0} 的电脑" @@ -1019,10 +1052,6 @@ msgstr "Bot" msgid "Bot screen" msgstr "Bot 屏幕" -#: src/pages/shell/computer-panel-preview.tsx -msgid "Bot screen preview" -msgstr "Bot 屏幕预览" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Bot to link" msgstr "要关联的 Bot" @@ -1031,10 +1060,6 @@ msgstr "要关联的 Bot" msgid "Bots act without asking by default. Add an exception only when you want to review a type of action first." msgstr "默认情况下 Bot 会直接执行。只有当你想先审核某类操作时,才添加例外。" -#: src/pages/shell/transcript.tsx -msgid "Bots are working" -msgstr "Bot 正在工作" - #: src/components/OoAtelier.tsx msgid "Bow" msgstr "" @@ -1064,7 +1089,6 @@ msgstr "" msgid "Browse MCP servers" msgstr "" -#: src/pages/Shell.tsx #: src/pages/shell/side-panel-header.tsx msgid "Browser" msgstr "" @@ -1493,6 +1517,10 @@ msgstr "电脑" msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "电脑未开启。请设置 SANDBOX_PROVIDER=docker 并提供 SANDBOX_SUPERVISOR_TOKEN,或将 SANDBOX_PROVIDER 设为 e2b、daytona 或 box 并提供对应 API 密钥。更改 .env 后请重建环境。" +#: src/pages/PluginsOverlay.tsx +msgid "Configure a plugin catalog on the server to connect apps." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Configured by deployment" msgstr "由部署配置" @@ -1584,6 +1612,10 @@ msgstr "已连接并使用 {0}。" msgid "Connected app" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Connected tools" +msgstr "" + #: src/pages/shell/message-cards.tsx msgid "Connected. Its tools are available from your next message." msgstr "已连接,下一条消息起即可使用其工具。" @@ -2322,6 +2354,7 @@ msgstr "正在断开…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -2381,7 +2414,9 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/message-cards.tsx #: src/pages/shell/project-overview.tsx +#: src/pages/shell/transcript.tsx msgid "Done" msgstr "完成" @@ -2394,8 +2429,11 @@ msgstr "" msgid "Dot" msgstr "" +#. placeholder {0}: activeItem.name +#. placeholder {0}: item.name #. placeholder {0}: props.name #: src/components/ArtifactFileCard.tsx +#: src/pages/shell/message-cards.tsx msgid "Download {0}" msgstr "下载 {0}" @@ -2403,7 +2441,6 @@ msgstr "下载 {0}" #: src/components/ArtifactPane.tsx #: src/components/media/MediaCard.tsx #: src/components/media/MediaPlayer.tsx -#: src/pages/shell/message-cards.tsx msgid "Download {name}" msgstr "下载 {name}" @@ -2589,6 +2626,8 @@ msgid "Expand" msgstr "展开" #. placeholder {0}: group.title +#. placeholder {0}: item.name +#: src/pages/shell/message-cards.tsx #: src/pages/shell/sidebar-group-header.tsx msgid "Expand {0}" msgstr "展开 {0}" @@ -2660,6 +2699,10 @@ msgstr "" msgid "Failed to open file" msgstr "" +#: src/pages/Shell.tsx +msgid "Failed to remove message" +msgstr "" + #: src/pages/Shell.tsx msgid "Failed to send message" msgstr "发送消息失败" @@ -2983,13 +3026,18 @@ msgid "Here's what I see in the logs…" msgstr "" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Hi, this is how I'll sound when I read replies out loud." -msgstr "你好,这是我朗读回复时的声音。" +msgid "Hi, this is how I'll sound when we talk." +msgstr "" #: src/pages/Shell.tsx +#: src/pages/shell/sidebar-header.tsx msgid "Hide bots" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Hide details" +msgstr "" + #: src/pages/Auth.tsx msgid "Hide password" msgstr "隐藏密码" @@ -3080,6 +3128,7 @@ msgstr "" msgid "Import OpenAPI JSON" msgstr "导入 OpenAPI JSON" +#: src/pages/shell/message-cards.tsx #: src/pages/shell/run-steps.tsx msgid "In Bearbeitung" msgstr "" @@ -3166,6 +3215,10 @@ msgstr "隔离" msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list." msgstr "它的对话、文件和例行任务将被永久删除。它创建的 Bot 仍留在列表中。" +#: src/pages/shell/transcript.tsx +msgid "joined" +msgstr "" + #: src/pages/shell/transcript.tsx msgid "Jump to latest" msgstr "跳到最新" @@ -3174,6 +3227,10 @@ msgstr "跳到最新" msgid "Jump to replied message" msgstr "跳到被回复的消息" +#: src/pages/shell/project-overview.tsx +msgid "Just ask in the chat whenever something should happen regularly." +msgstr "" + #: src/pages/ActivityList.tsx #: src/pages/ProjectSessionTimeline.tsx msgid "just now" @@ -3774,6 +3831,7 @@ msgid "New space" msgstr "新建工作区" #: src/components/CookingMode.tsx +#: src/pages/shell/message-cards.tsx msgid "Next" msgstr "" @@ -3885,6 +3943,10 @@ msgstr "" msgid "No providers found." msgstr "未找到提供方。" +#: src/pages/ActivityList.tsx +msgid "No recent activity" +msgstr "" + #: src/components/integrations/IntegrationSetup.tsx msgid "No remote MCP servers found" msgstr "" @@ -3899,6 +3961,7 @@ msgid "No results" msgstr "无结果" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "No runs yet" msgstr "还没有运行记录" @@ -3957,6 +4020,7 @@ msgid "Notifications" msgstr "通知" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Now" msgstr "现在" @@ -4027,7 +4091,6 @@ msgid "Oo name" msgstr "" #: src/pages/ScratchpadSection.tsx -#: src/pages/shell/computer-panel-preview.tsx msgid "Open" msgstr "打开" @@ -4056,7 +4119,7 @@ msgstr "" msgid "Open in browser" msgstr "" -#: src/pages/shell/computer-panel-preview.tsx +#: src/components/browser/OoComputerBrowser.tsx msgid "Open in full window" msgstr "在完整窗口中打开" @@ -4288,10 +4351,6 @@ msgstr "" msgid "Project" msgstr "" -#: src/pages/Shell.tsx -msgid "Project · sessions & folders" -msgstr "" - #: src/pages/shell/dialogs.tsx #: src/pages/shell/project-look-dialog.tsx msgid "Project folder" @@ -4399,6 +4458,10 @@ msgstr "" msgid "Ready" msgstr "" +#: src/pages/shell/project-overview.tsx +msgid "Ready" +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Reasoning effort" msgstr "推理强度" @@ -4412,6 +4475,7 @@ msgid "Recall only" msgstr "" #: src/pages/ActivityList.tsx +#: src/pages/shell/project-overview.tsx msgid "Recent" msgstr "最近" @@ -4528,6 +4592,10 @@ msgstr "移除 {0}" msgid "Remove folder" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Remove from queue" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" @@ -4653,6 +4721,11 @@ msgstr "还原上次保存的工作区。电脑上未保存的工作会丢失。 msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -4700,11 +4773,13 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/Shell.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/routine-event-line.tsx msgid "Routine" msgstr "例行任务" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Routines" msgstr "例行任务" @@ -4718,6 +4793,7 @@ msgid "Run at" msgstr "运行时间" #: src/pages/RoutineEditor.tsx +#: src/pages/shell/project-overview.tsx msgid "Run history" msgstr "运行记录" @@ -4756,10 +4832,6 @@ msgstr "正在运行…" msgid "Runs when this bot receives a verified message from this provider." msgstr "" -#: src/pages/shell/side-panel-header.tsx -msgid "Sandbox" -msgstr "" - #: src/components/AskCard.tsx #: src/components/MediaSettings.tsx #: src/components/teach/SkillDraftCard.tsx @@ -4909,6 +4981,10 @@ msgstr "发送回答" msgid "Send it" msgstr "发送" +#: src/pages/shell/transcript.tsx +msgid "Send now" +msgstr "立即发送" + #: src/pages/Auth.tsx msgid "Send reset link" msgstr "发送重置链接" @@ -5021,6 +5097,10 @@ msgstr "" msgid "Show computer" msgstr "" +#: src/pages/shell/transcript.tsx +msgid "Show details" +msgstr "" + #: src/pages/Shell.tsx msgid "Show less" msgstr "" @@ -5037,6 +5117,7 @@ msgstr "显示更多" msgid "Show password" msgstr "显示密码" +#: src/pages/shell/project-overview.tsx #: src/pages/shell/side-panel-header.tsx msgid "Show settings" msgstr "显示设置" @@ -5172,6 +5253,7 @@ msgstr "" msgid "Speaking with intent…" msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx #: src/lib/onboarding-copy.ts msgid "Specialist crew" msgstr "" @@ -5286,6 +5368,10 @@ msgstr "停止示范" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." @@ -5398,6 +5484,7 @@ msgstr "正在示范,发送新消息前请先停止示范。" #: src/pages/ProjectSessionTimeline.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-overview.tsx msgid "Team" msgstr "团队" @@ -5512,10 +5599,6 @@ msgstr "此订阅登录在 Rakazo 中尚不可用。请使用部署凭据或选 msgid "This will permanently delete this project and all its conversations, bots, and memory." msgstr "" -#: src/pages/Shell.tsx -msgid "Threads by topic · folders" -msgstr "" - #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx msgid "Tiers" @@ -5701,6 +5784,7 @@ msgstr "" #: src/components/DesktopUpdates.tsx #: src/components/SoftwareUpdateSection.tsx +#: src/pages/AccountSettingsOverlay.tsx msgid "Up to date" msgstr "已是最新" @@ -5995,6 +6079,7 @@ msgid "Work channel open — go ahead." msgstr "" #: src/components/stage/OoStage.tsx +#: src/pages/shell/project-overview.tsx msgid "Working" msgstr "" @@ -6037,6 +6122,7 @@ msgstr "昨天" #: src/components/UserProfileSettings.tsx #: src/components/voice/VoiceStage.tsx #: src/pages/Shell.tsx +#: src/pages/shell/transcript.tsx msgid "You" msgstr "你" @@ -6077,13 +6163,25 @@ msgid "Your name" msgstr "你的名字" #: src/pages/VoiceSettingsOverlay.tsx -msgid "Your Oo reads replies aloud when this is on." +msgid "Your Oo talks with you naturally when this is on." msgstr "" #: src/pages/Onboarding.tsx msgid "Your Oo will communicate in this language. You can change it later." msgstr "" +#: src/components/onboarding/OnboardingDiscoveryShell.tsx +msgid "Your profile" +msgstr "" + #: src/pages/Welcome.tsx msgid "Your specialists. One calm place to talk." msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom in" +msgstr "" + +#: src/pages/shell/message-cards.tsx +msgid "Zoom out" +msgstr "" diff --git a/apps/web/src/pages/AccountSettingsOverlay.tsx b/apps/web/src/pages/AccountSettingsOverlay.tsx index 9aa7448..74d972f 100644 --- a/apps/web/src/pages/AccountSettingsOverlay.tsx +++ b/apps/web/src/pages/AccountSettingsOverlay.tsx @@ -188,6 +188,11 @@ export function UpdatesSettingsPanel({
    +
    +

    + Up to date +

    +
    ); } diff --git a/apps/web/src/pages/ActivityList.tsx b/apps/web/src/pages/ActivityList.tsx index e4b0b49..fed2ed1 100644 --- a/apps/web/src/pages/ActivityList.tsx +++ b/apps/web/src/pages/ActivityList.tsx @@ -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 ( +
    + No recent activity +
    + ); + } return (
    diff --git a/apps/web/src/pages/CallView.tsx b/apps/web/src/pages/CallView.tsx index 04fa459..881d86f 100644 --- a/apps/web/src/pages/CallView.tsx +++ b/apps/web/src/pages/CallView.tsx @@ -1,6 +1,11 @@ import { useLingui } from "@lingui/react/macro"; import type { ThreadMessage, ThreadSnapshot } from "@rakazo/contracts"; -import { isSecretAskBlock, speechFromBlocks, spokenDecision } from "@rakazo/core"; +import { + isSecretAskBlock, + speechFromBlocks, + spokenDecision, + spokenSummaryFromBlocks, +} from "@rakazo/core"; import { useEffect, useMemo, useRef, useState } from "react"; import type { VoicePhase, VoiceTranscriptLine } from "../components/voice/VoiceStage"; import { VoiceStage } from "../components/voice/VoiceStage"; @@ -417,7 +422,7 @@ export function CallView({ (block) => block.kind === "ask" && block.status !== "answered", ); const secretAsk = ask && isSecretAskBlock(ask); - const rawText = speechFromBlocks(lastBot.blocks); + const rawText = spokenSummaryFromBlocks(lastBot.blocks); const text = rawText ? secretAsk ? `${rawText}. ${secretPromptRef.current}` @@ -477,7 +482,7 @@ export function CallView({ hasUnspokenBotMessage: Boolean( lastBot && lastBot.id !== spokenMessage.current && - speechFromBlocks(lastBot.blocks).trim(), + spokenSummaryFromBlocks(lastBot.blocks).trim(), ), runActive: isActiveRunStatus(snapshot?.run?.status), userTurnPending: userTurnPendingRef.current, diff --git a/apps/web/src/pages/HeuteBrief.tsx b/apps/web/src/pages/HeuteBrief.tsx index 99660bc..552eafa 100644 --- a/apps/web/src/pages/HeuteBrief.tsx +++ b/apps/web/src/pages/HeuteBrief.tsx @@ -7,6 +7,9 @@ import type { Group, HabitToday, RunActivityRow, + Space, + SpaceBot, + SpaceGroup, } from "@rakazo/contracts"; import { runChatTitle } from "@rakazo/core"; import { botColors } from "@rakazo/ui-tokens"; @@ -30,6 +33,7 @@ import { rpc } from "../lib/rpc"; type HeuteBriefProps = { bots: Bot[]; groups: Group[]; + spaces?: Space[]; onOpenBot: (botId: string, spaceId: string) => void; onOpenGroup: (groupId: string, spaceId: string) => void; onOpenRun: (run: RunActivityRow) => void; @@ -41,6 +45,7 @@ const DATE_LABEL: Intl.DateTimeFormatOptions = { weekday: "long", day: "numeric" export function HeuteBrief({ bots, groups, + spaces = [], onOpenBot, onOpenGroup, onOpenRun, @@ -135,13 +140,21 @@ export function HeuteBrief({ buildHeuteBrief({ bots, groups, + spaces, activeRuns, recentRuns, }), - [activeRuns, bots, groups, recentRuns], + [activeRuns, bots, groups, recentRuns, spaces], ); - const botColorById = useMemo(() => new Map(bots.map((bot) => [bot.id, bot.color])), [bots]); + const botColorById = useMemo( + () => + new Map([ + ...spaces.flatMap((space) => space.bots).map((bot) => [bot.id, bot.color] as const), + ...bots.map((bot) => [bot.id, bot.color] as const), + ]), + [bots, spaces], + ); const locale = i18n.locale || "de"; const dateLabel = coach @@ -391,13 +404,13 @@ function NeedsYouRow({ > {chatKind === "bot" ? ( ) : ( - + )}
    {title}
    diff --git a/apps/web/src/pages/PluginsOverlay.tsx b/apps/web/src/pages/PluginsOverlay.tsx index 199eda0..36b6d4e 100644 --- a/apps/web/src/pages/PluginsOverlay.tsx +++ b/apps/web/src/pages/PluginsOverlay.tsx @@ -10,7 +10,6 @@ import { abortableDelay, buildFeaturedConnectorTiles, CONNECTION_CATALOG_PAGE_SIZE, - EMPTY_PLUGIN_CATALOG_MESSAGE, filterConnectionCatalogItems, humanizeToolName, isGithubInstall, @@ -730,7 +729,7 @@ export function PluginsOverlay({
    {!loading && catalog.length === 0 ? (

    - {EMPTY_PLUGIN_CATALOG_MESSAGE} + Configure a plugin catalog on the server to connect apps.

    ) : (
    diff --git a/apps/web/src/pages/SettingsOverlay.tsx b/apps/web/src/pages/SettingsOverlay.tsx index 1236e3a..cd18699 100644 --- a/apps/web/src/pages/SettingsOverlay.tsx +++ b/apps/web/src/pages/SettingsOverlay.tsx @@ -176,7 +176,7 @@ export function SettingsOverlay({