From cf7ca9b2f7f13fabd87e2ff41264d12ddd4f85ff Mon Sep 17 00:00:00 2001 From: Chris Yang <18487241+ysm-dev@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:40:06 +0900 Subject: [PATCH 001/245] fix(app): skip editor reconcile during IME composition (#17041) --- packages/app/src/components/prompt-input.tsx | 49 +++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 532edd3bcd..3ee8f43513 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -490,6 +490,18 @@ export const PromptInput: Component = (props) => { setComposing(false) } + const handleCompositionStart = () => { + setComposing(true) + } + + const handleCompositionEnd = () => { + setComposing(false) + requestAnimationFrame(() => { + if (composing()) return + reconcile(prompt.current().filter((part) => part.type !== "image")) + }) + } + const agentList = createMemo(() => sync.data.agent .filter((agent) => !agent.hidden && agent.mode !== "primary") @@ -680,24 +692,27 @@ export const PromptInput: Component = (props) => { } } + const reconcile = (input: Prompt) => { + if (mirror.input) { + mirror.input = false + if (isNormalizedEditor()) return + + renderEditorWithCursor(input) + return + } + + const dom = parseFromDOM() + if (isNormalizedEditor() && isPromptEqual(input, dom)) return + + renderEditorWithCursor(input) + } + createEffect( on( () => prompt.current(), - (currentParts) => { - const inputParts = currentParts.filter((part) => part.type !== "image") - - if (mirror.input) { - mirror.input = false - if (isNormalizedEditor()) return - - renderEditorWithCursor(inputParts) - return - } - - const domParts = parseFromDOM() - if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return - - renderEditorWithCursor(inputParts) + (parts) => { + if (composing()) return + reconcile(parts.filter((part) => part.type !== "image")) }, ), ) @@ -1208,8 +1223,8 @@ export const PromptInput: Component = (props) => { spellcheck={store.mode === "normal"} onInput={handleInput} onPaste={handlePaste} - onCompositionStart={() => setComposing(true)} - onCompositionEnd={() => setComposing(false)} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} onBlur={handleBlur} onKeyDown={handleKeyDown} classList={{ From c37f7b9d997fcbbf4f331e1965425375e7606ab6 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:42:29 -0500 Subject: [PATCH 002/245] fix(app): todos not clearing --- .../composer/session-composer-helpers.ts | 10 ++++ .../composer/session-composer-state.test.ts | 23 ++++++++ .../composer/session-composer-state.ts | 58 ++++++++++++++----- 3 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 packages/app/src/pages/session/composer/session-composer-helpers.ts diff --git a/packages/app/src/pages/session/composer/session-composer-helpers.ts b/packages/app/src/pages/session/composer/session-composer-helpers.ts new file mode 100644 index 0000000000..90c238af46 --- /dev/null +++ b/packages/app/src/pages/session/composer/session-composer-helpers.ts @@ -0,0 +1,10 @@ +export const todoState = (input: { + count: number + done: boolean + live: boolean +}): "hide" | "clear" | "open" | "close" => { + if (input.count === 0) return "hide" + if (!input.live) return "clear" + if (!input.done) return "open" + return "close" +} diff --git a/packages/app/src/pages/session/composer/session-composer-state.test.ts b/packages/app/src/pages/session/composer/session-composer-state.test.ts index 934d3152a9..f7c11715c2 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.test.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client" +import { todoState } from "./session-composer-helpers" import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" const session = (input: { id: string; parentID?: string }) => @@ -103,3 +104,25 @@ describe("sessionQuestionRequest", () => { expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand") }) }) + +describe("todoState", () => { + test("hides when there are no todos", () => { + expect(todoState({ count: 0, done: false, live: true })).toBe("hide") + }) + + test("opens while the session is still working", () => { + expect(todoState({ count: 2, done: false, live: true })).toBe("open") + }) + + test("closes completed todos after a running turn", () => { + expect(todoState({ count: 2, done: true, live: true })).toBe("close") + }) + + test("clears stale todos when the turn ends", () => { + expect(todoState({ count: 2, done: false, live: false })).toBe("clear") + }) + + test("clears completed todos when the session is no longer live", () => { + expect(todoState({ count: 2, done: true, live: false })).toBe("clear") + }) +}) diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index f70bc4bbdd..a007e4c849 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -8,8 +8,11 @@ import { useLanguage } from "@/context/language" import { usePermission } from "@/context/permission" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" +import { todoState } from "./session-composer-helpers" import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" +const idle = { type: "idle" as const } + export function createSessionComposerBlocked() { const params = useParams() const permission = usePermission() @@ -59,9 +62,22 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => return globalSync.data.session_todo[id] ?? [] }) + const done = createMemo( + () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"), + ) + + const status = createMemo(() => { + const id = params.id + if (!id) return idle + return sync.data.session_status[id] ?? idle + }) + + const busy = createMemo(() => status().type !== "idle") + const live = createMemo(() => busy() || blocked()) + const [store, setStore] = createStore({ responding: undefined as string | undefined, - dock: todos().length > 0, + dock: todos().length > 0 && live(), closing: false, opening: false, }) @@ -89,10 +105,6 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => }) } - const done = createMemo( - () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"), - ) - let timer: number | undefined let raf: number | undefined @@ -111,21 +123,42 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => }, closeMs()) } + // Keep stale turn todos from reopening if the model never clears them. + const clear = () => { + const id = params.id + if (!id) return + globalSync.todo.set(id, []) + sync.set("todo", id, []) + } + createEffect( on( - () => [todos().length, done()] as const, - ([count, complete], prev) => { + () => [todos().length, done(), live()] as const, + ([count, complete, active]) => { if (raf) cancelAnimationFrame(raf) raf = undefined - if (count === 0) { + const next = todoState({ + count, + done: complete, + live: active, + }) + + if (next === "hide") { if (timer) window.clearTimeout(timer) timer = undefined setStore({ dock: false, closing: false, opening: false }) return } - if (!complete) { + if (next === "clear") { + if (timer) window.clearTimeout(timer) + timer = undefined + clear() + return + } + + if (next === "open") { if (timer) window.clearTimeout(timer) timer = undefined const hidden = !store.dock || store.closing @@ -142,13 +175,8 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => return } - if (prev && prev[1]) { - if (store.closing && !timer) scheduleClose() - return - } - setStore({ dock: true, opening: false, closing: true }) - scheduleClose() + if (!timer) scheduleClose() }, ), ) From dbc00aa8e04cd9fbfb0534e24751c38232862244 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 11 Mar 2026 16:44:26 -0400 Subject: [PATCH 003/245] feat(id): brand ProjectID through Drizzle and Zod schemas (#16948) --- packages/opencode/src/cli/cmd/import.ts | 4 +- packages/opencode/src/control-plane/types.ts | 3 +- .../src/control-plane/workspace.sql.ts | 2 + .../opencode/src/control-plane/workspace.ts | 3 +- packages/opencode/src/permission/next.ts | 3 +- packages/opencode/src/project/project.sql.ts | 3 +- packages/opencode/src/project/project.ts | 43 ++++++++++--------- packages/opencode/src/project/schema.ts | 16 +++++++ .../opencode/src/server/routes/project.ts | 3 +- packages/opencode/src/session/index.ts | 5 ++- packages/opencode/src/session/session.sql.ts | 2 + packages/opencode/src/worktree/index.ts | 5 ++- packages/opencode/test/config/config.test.ts | 3 +- .../opencode/test/project/project.test.ts | 9 ++-- .../test/storage/json-migration.test.ts | 17 ++++---- 15 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 packages/opencode/src/project/schema.ts diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index eb5964379a..cfea5fdbb3 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -86,7 +86,7 @@ export const ImportCommand = cmd({ await bootstrap(process.cwd(), async () => { let exportData: | { - info: Session.Info + info: SDKSession messages: Array<{ info: Message parts: Part[] @@ -152,7 +152,7 @@ export const ImportCommand = cmd({ return } - const row = { ...Session.toRow(exportData.info), project_id: Instance.project.id } + const row = Session.toRow({ ...exportData.info, projectID: Instance.project.id }) Database.use((db) => db .insert(SessionTable) diff --git a/packages/opencode/src/control-plane/types.ts b/packages/opencode/src/control-plane/types.ts index 3d27757fd1..53a6c8a073 100644 --- a/packages/opencode/src/control-plane/types.ts +++ b/packages/opencode/src/control-plane/types.ts @@ -1,5 +1,6 @@ import z from "zod" import { Identifier } from "@/id/id" +import { ProjectID } from "@/project/schema" export const WorkspaceInfo = z.object({ id: Identifier.schema("workspace"), @@ -8,7 +9,7 @@ export const WorkspaceInfo = z.object({ name: z.string().nullable(), directory: z.string().nullable(), extra: z.unknown().nullable(), - projectID: z.string(), + projectID: ProjectID.zod, }) export type WorkspaceInfo = z.infer diff --git a/packages/opencode/src/control-plane/workspace.sql.ts b/packages/opencode/src/control-plane/workspace.sql.ts index 7639620691..eb7d21d1e8 100644 --- a/packages/opencode/src/control-plane/workspace.sql.ts +++ b/packages/opencode/src/control-plane/workspace.sql.ts @@ -1,5 +1,6 @@ import { sqliteTable, text } from "drizzle-orm/sqlite-core" import { ProjectTable } from "../project/project.sql" +import type { ProjectID } from "../project/schema" export const WorkspaceTable = sqliteTable("workspace", { id: text().primaryKey(), @@ -9,6 +10,7 @@ export const WorkspaceTable = sqliteTable("workspace", { directory: text(), extra: text({ mode: "json" }), project_id: text() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), }) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 8c76fbdab9..f3af985cac 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -6,6 +6,7 @@ import { Project } from "@/project/project" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { Log } from "@/util/log" +import { ProjectID } from "@/project/schema" import { WorkspaceTable } from "./workspace.sql" import { getAdaptor } from "./adaptors" import { WorkspaceInfo } from "./types" @@ -48,7 +49,7 @@ export namespace Workspace { id: Identifier.schema("workspace").optional(), type: Info.shape.type, branch: Info.shape.branch, - projectID: Info.shape.projectID, + projectID: ProjectID.zod, extra: Info.shape.extra, }) diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 1e1df62a3c..4e47e43c8f 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -7,6 +7,7 @@ import { Database, eq } from "@/storage/db" import { PermissionTable } from "@/session/session.sql" import { fn } from "@/util/fn" import { Log } from "@/util/log" +import { ProjectID } from "@/project/schema" import { Wildcard } from "@/util/wildcard" import os from "os" import z from "zod" @@ -90,7 +91,7 @@ export namespace PermissionNext { export type Reply = z.infer export const Approval = z.object({ - projectID: z.string(), + projectID: ProjectID.zod, patterns: z.string().array(), }) diff --git a/packages/opencode/src/project/project.sql.ts b/packages/opencode/src/project/project.sql.ts index 7f0f8ca532..efbc400b5e 100644 --- a/packages/opencode/src/project/project.sql.ts +++ b/packages/opencode/src/project/project.sql.ts @@ -1,8 +1,9 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" import { Timestamps } from "../storage/schema.sql" +import type { ProjectID } from "./schema" export const ProjectTable = sqliteTable("project", { - id: text().primaryKey(), + id: text().$type().primaryKey(), worktree: text().notNull(), vcs: text(), name: text(), diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 68cece0a52..196dc8da61 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -15,6 +15,7 @@ import { existsSync } from "fs" import { git } from "../util/git" import { Glob } from "../util/glob" import { which } from "../util/which" +import { ProjectID } from "./schema" export namespace Project { const log = Log.create({ service: "project" }) @@ -33,7 +34,7 @@ export namespace Project { export const Info = z .object({ - id: z.string(), + id: ProjectID.zod, worktree: z.string(), vcs: z.literal("git").optional(), name: z.string().optional(), @@ -73,7 +74,7 @@ export namespace Project { ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined } : undefined return { - id: row.id, + id: ProjectID.make(row.id), worktree: row.worktree, vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined, name: row.name ?? undefined, @@ -91,6 +92,7 @@ export namespace Project { function readCachedId(dir: string) { return Filesystem.readText(path.join(dir, "opencode")) .then((x) => x.trim()) + .then(ProjectID.make) .catch(() => undefined) } @@ -111,7 +113,7 @@ export namespace Project { if (!gitBinary) { return { - id: id ?? "global", + id: id ?? ProjectID.global, worktree: sandbox, sandbox, vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), @@ -130,7 +132,7 @@ export namespace Project { if (!worktree) { return { - id: id ?? "global", + id: id ?? ProjectID.global, worktree: sandbox, sandbox, vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), @@ -160,14 +162,14 @@ export namespace Project { if (!roots) { return { - id: "global", + id: ProjectID.global, worktree: sandbox, sandbox, vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), } } - id = roots[0] + id = roots[0] ? ProjectID.make(roots[0]) : undefined if (id) { await Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined) } @@ -175,7 +177,7 @@ export namespace Project { if (!id) { return { - id: "global", + id: ProjectID.global, worktree: sandbox, sandbox, vcs: "git", @@ -208,7 +210,7 @@ export namespace Project { } return { - id: "global", + id: ProjectID.global, worktree: "/", sandbox: "/", vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), @@ -228,7 +230,7 @@ export namespace Project { updated: Date.now(), }, } - if (data.id !== "global") { + if (data.id !== ProjectID.global) { await migrateFromGlobal(data.id, data.worktree) } return fresh @@ -308,12 +310,12 @@ export namespace Project { return } - async function migrateFromGlobal(id: string, worktree: string) { - const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get()) + async function migrateFromGlobal(id: ProjectID, worktree: string) { + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, ProjectID.global)).get()) if (!row) return const sessions = Database.use((db) => - db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(), + db.select().from(SessionTable).where(eq(SessionTable.project_id, ProjectID.global)).all(), ) if (sessions.length === 0) return @@ -323,14 +325,14 @@ export namespace Project { // Skip sessions that belong to a different directory if (row.directory && row.directory !== worktree) return - log.info("migrating session", { sessionID: row.id, from: "global", to: id }) + log.info("migrating session", { sessionID: row.id, from: ProjectID.global, to: id }) Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run()) }).catch((error) => { log.error("failed to migrate sessions from global to project", { error, projectId: id }) }) } - export function setInitialized(id: string) { + export function setInitialized(id: ProjectID) { Database.use((db) => db .update(ProjectTable) @@ -352,7 +354,7 @@ export namespace Project { ) } - export function get(id: string): Info | undefined { + export function get(id: ProjectID): Info | undefined { const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) if (!row) return undefined return fromRow(row) @@ -375,12 +377,13 @@ export namespace Project { export const update = fn( z.object({ - projectID: z.string(), + projectID: ProjectID.zod, name: z.string().optional(), icon: Info.shape.icon.optional(), commands: Info.shape.commands.optional(), }), async (input) => { + const id = ProjectID.make(input.projectID) const result = Database.use((db) => db .update(ProjectTable) @@ -391,7 +394,7 @@ export namespace Project { commands: input.commands, time_updated: Date.now(), }) - .where(eq(ProjectTable.id, input.projectID)) + .where(eq(ProjectTable.id, id)) .returning() .get(), ) @@ -407,7 +410,7 @@ export namespace Project { }, ) - export async function sandboxes(id: string) { + export async function sandboxes(id: ProjectID) { const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) if (!row) return [] const data = fromRow(row) @@ -419,7 +422,7 @@ export namespace Project { return valid } - export async function addSandbox(id: string, directory: string) { + export async function addSandbox(id: ProjectID, directory: string) { const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) if (!row) throw new Error(`Project not found: ${id}`) const sandboxes = [...row.sandboxes] @@ -443,7 +446,7 @@ export namespace Project { return data } - export async function removeSandbox(id: string, directory: string) { + export async function removeSandbox(id: ProjectID, directory: string) { const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) if (!row) throw new Error(`Project not found: ${id}`) const sandboxes = row.sandboxes.filter((s) => s !== directory) diff --git a/packages/opencode/src/project/schema.ts b/packages/opencode/src/project/schema.ts new file mode 100644 index 0000000000..adaa9a8314 --- /dev/null +++ b/packages/opencode/src/project/schema.ts @@ -0,0 +1,16 @@ +import { Schema } from "effect" +import z from "zod" + +import { withStatics } from "@/util/schema" + +const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectId")) + +export type ProjectID = typeof projectIdSchema.Type + +export const ProjectID = projectIdSchema.pipe( + withStatics((schema: typeof projectIdSchema) => ({ + global: schema.makeUnsafe("global"), + make: (id: string) => schema.makeUnsafe(id), + zod: z.string().pipe(z.custom()), + })), +) diff --git a/packages/opencode/src/server/routes/project.ts b/packages/opencode/src/server/routes/project.ts index 85314df937..994d58b0ca 100644 --- a/packages/opencode/src/server/routes/project.ts +++ b/packages/opencode/src/server/routes/project.ts @@ -4,6 +4,7 @@ import { resolver } from "hono-openapi" import { Instance } from "../../project/instance" import { Project } from "../../project/project" import z from "zod" +import { ProjectID } from "../../project/schema" import { errors } from "../error" import { lazy } from "../../util/lazy" import { InstanceBootstrap } from "../../project/bootstrap" @@ -105,7 +106,7 @@ export const ProjectRoutes = lazy(() => ...errors(400, 404), }, }), - validator("param", z.object({ projectID: z.string() })), + validator("param", z.object({ projectID: ProjectID.zod })), validator("json", Project.update.schema.omit({ projectID: true })), async (c) => { const projectID = c.req.valid("param").projectID diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 5cc4d7da8d..ff499fe2e7 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -23,6 +23,7 @@ import { fn } from "@/util/fn" import { Command } from "../command" import { Snapshot } from "@/snapshot" import { WorkspaceContext } from "../control-plane/workspace-context" +import { ProjectID } from "../project/schema" import type { Provider } from "@/provider/provider" import { PermissionNext } from "@/permission/next" @@ -120,7 +121,7 @@ export namespace Session { .object({ id: Identifier.schema("session"), slug: z.string(), - projectID: z.string(), + projectID: ProjectID.zod, workspaceID: z.string().optional(), directory: z.string(), parentID: Identifier.schema("session").optional(), @@ -162,7 +163,7 @@ export namespace Session { export const ProjectInfo = z .object({ - id: z.string(), + id: ProjectID.zod, name: z.string().optional(), worktree: z.string(), }) diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index b3228f400f..0b62dbbd8c 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -3,6 +3,7 @@ import { ProjectTable } from "../project/project.sql" import type { MessageV2 } from "./message-v2" import type { Snapshot } from "../snapshot" import type { PermissionNext } from "../permission/next" +import type { ProjectID } from "../project/schema" import { Timestamps } from "../storage/schema.sql" type PartData = Omit @@ -13,6 +14,7 @@ export const SessionTable = sqliteTable( { id: text().primaryKey(), project_id: text() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), workspace_id: text(), diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index aa5613010c..6ed0e48202 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -8,6 +8,7 @@ import { InstanceBootstrap } from "../project/bootstrap" import { Project } from "../project/project" import { Database, eq } from "../storage/db" import { ProjectTable } from "../project/project.sql" +import type { ProjectID } from "../project/schema" import { fn } from "../util/fn" import { Log } from "../util/log" import { Process } from "../util/process" @@ -310,7 +311,7 @@ export namespace Worktree { return false } - async function runStartScripts(directory: string, input: { projectID: string; extra?: string }) { + async function runStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) { const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get()) const project = row ? Project.fromRow(row) : undefined const startup = project?.commands?.start?.trim() ?? "" @@ -322,7 +323,7 @@ export namespace Worktree { return true } - function queueStartScripts(directory: string, input: { projectID: string; extra?: string }) { + function queueStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) { setTimeout(() => { const start = async () => { await runStartScripts(directory, input) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 80394fbff5..90727cf8a0 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -8,6 +8,7 @@ import path from "path" import fs from "fs/promises" import { pathToFileURL } from "url" import { Global } from "../../src/global" +import { ProjectID } from "../../src/project/schema" import { Filesystem } from "../../src/util/filesystem" // Get managed config directory from environment (set in preload.ts) @@ -44,7 +45,7 @@ async function check(map: (dir: string) => string) { const cfg = await Config.get() expect(cfg.snapshot).toBe(true) expect(Instance.directory).toBe(Filesystem.resolve(tmp.path)) - expect(Instance.project.id).not.toBe("global") + expect(Instance.project.id).not.toBe(ProjectID.global) }, }) } finally { diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index fef9e4190e..cc6b8dde5b 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -6,6 +6,7 @@ import path from "path" import { tmpdir } from "../fixture/fixture" import { Filesystem } from "../../src/util/filesystem" import { GlobalBus } from "../../src/bus/global" +import { ProjectID } from "../../src/project/schema" Log.init({ print: false }) @@ -74,7 +75,7 @@ describe("Project.fromDirectory", () => { const { project } = await p.fromDirectory(tmp.path) expect(project).toBeDefined() - expect(project.id).toBe("global") + expect(project.id).toBe(ProjectID.global) expect(project.vcs).toBe("git") expect(project.worktree).toBe(tmp.path) @@ -90,7 +91,7 @@ describe("Project.fromDirectory", () => { const { project } = await p.fromDirectory(tmp.path) expect(project).toBeDefined() - expect(project.id).not.toBe("global") + expect(project.id).not.toBe(ProjectID.global) expect(project.vcs).toBe("git") expect(project.worktree).toBe(tmp.path) @@ -107,7 +108,7 @@ describe("Project.fromDirectory", () => { await withMode("rev-list-fail", async () => { const { project } = await p.fromDirectory(tmp.path) expect(project.vcs).toBe("git") - expect(project.id).toBe("global") + expect(project.id).toBe(ProjectID.global) expect(project.worktree).toBe(tmp.path) }) }) @@ -301,7 +302,7 @@ describe("Project.update", () => { await expect( Project.update({ - projectID: "nonexistent-project-id", + projectID: ProjectID.make("nonexistent-project-id"), name: "Should Fail", }), ).rejects.toThrow("Project not found: nonexistent-project-id") diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 40dd611453..76dfb8b450 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -8,6 +8,7 @@ import { readFileSync, readdirSync } from "fs" import { JsonMigration } from "../../src/storage/json-migration" import { Global } from "../../src/global" import { ProjectTable } from "../../src/project/project.sql" +import { ProjectID } from "../../src/project/schema" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" import { SessionShareTable } from "../../src/share/share.sql" @@ -123,7 +124,7 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const projects = db.select().from(ProjectTable).all() expect(projects.length).toBe(1) - expect(projects[0].id).toBe("proj_test123abc") + expect(projects[0].id).toBe(ProjectID.make("proj_test123abc")) expect(projects[0].worktree).toBe("/test/path") expect(projects[0].name).toBe("Test Project") expect(projects[0].sandboxes).toEqual(["/test/sandbox"]) @@ -148,7 +149,7 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const projects = db.select().from(ProjectTable).all() expect(projects.length).toBe(1) - expect(projects[0].id).toBe("proj_filename") // Uses filename, not JSON id + expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id }) test("migrates project with commands", async () => { @@ -169,7 +170,7 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const projects = db.select().from(ProjectTable).all() expect(projects.length).toBe(1) - expect(projects[0].id).toBe("proj_with_commands") + expect(projects[0].id).toBe(ProjectID.make("proj_with_commands")) expect(projects[0].commands).toEqual({ start: "npm run dev" }) }) @@ -190,7 +191,7 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const projects = db.select().from(ProjectTable).all() expect(projects.length).toBe(1) - expect(projects[0].id).toBe("proj_no_commands") + expect(projects[0].id).toBe(ProjectID.make("proj_no_commands")) expect(projects[0].commands).toBeNull() }) @@ -220,7 +221,7 @@ describe("JSON to SQLite migration", () => { const sessions = db.select().from(SessionTable).all() expect(sessions.length).toBe(1) expect(sessions[0].id).toBe("ses_test456def") - expect(sessions[0].project_id).toBe("proj_test123abc") + expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc")) expect(sessions[0].slug).toBe("test-session") expect(sessions[0].title).toBe("Test Session Title") expect(sessions[0].summary_additions).toBe(10) @@ -426,7 +427,7 @@ describe("JSON to SQLite migration", () => { const sessions = db.select().from(SessionTable).all() expect(sessions.length).toBe(1) expect(sessions[0].id).toBe("ses_migrated") - expect(sessions[0].project_id).toBe(gitBasedProjectID) // Uses directory, not stale JSON + expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON }) test("uses filename for session id when JSON has different value", async () => { @@ -458,7 +459,7 @@ describe("JSON to SQLite migration", () => { const sessions = db.select().from(SessionTable).all() expect(sessions.length).toBe(1) expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id - expect(sessions[0].project_id).toBe("proj_test123abc") + expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc")) }) test("is idempotent (running twice doesn't duplicate)", async () => { @@ -643,7 +644,7 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const projects = db.select().from(ProjectTable).all() expect(projects.length).toBe(1) - expect(projects[0].id).toBe("proj_test123abc") + expect(projects[0].id).toBe(ProjectID.make("proj_test123abc")) }) test("skips invalid todo entries while preserving source positions", async () => { From 387ab78bf69a130b87e8d428e796c1663ba6e745 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:01:56 -0500 Subject: [PATCH 004/245] chore: fix test --- .../app/e2e/session/session-review.spec.ts | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/app/e2e/session/session-review.spec.ts b/packages/app/e2e/session/session-review.spec.ts index 4198c733c0..89d04e67d9 100644 --- a/packages/app/e2e/session/session-review.spec.ts +++ b/packages/app/e2e/session/session-review.spec.ts @@ -101,12 +101,31 @@ async function waitMark(page: Parameters[0]["page"], file: string, ) } +async function spot(page: Parameters[0]["page"], file: string) { + return page.evaluate((file) => { + const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport') + if (!(view instanceof HTMLElement)) return null + + const row = Array.from(document.querySelectorAll("h3")).find( + (node) => node instanceof HTMLElement && node.textContent?.includes(file), + ) + if (!(row instanceof HTMLElement)) return null + + const a = row.getBoundingClientRect() + const b = view.getBoundingClientRect() + return { + top: a.top - b.top, + y: view.scrollTop, + } + }, file) +} + test("review keeps scroll position after a live diff update", async ({ page, withProject }) => { test.setTimeout(180_000) const tag = `review-${Date.now()}` const list = files(tag) - const hit = list[list.length - 2]! + const hit = list[list.length - 4]! const next = `${tag}-live` await page.setViewportSize({ width: 1600, height: 1000 }) @@ -160,8 +179,9 @@ test("review keeps scroll position after a live diff update", async ({ page, wit await expect(row).toBeVisible() await row.evaluate((el) => el.scrollIntoView({ block: "center" })) - await expect.poll(() => view.evaluate((el) => el.scrollTop)).toBeGreaterThan(200) - const prev = await view.evaluate((el) => el.scrollTop) + await expect.poll(async () => (await spot(page, hit.file))?.y ?? 0).toBeGreaterThan(200) + const prev = await spot(page, hit.file) + if (!prev) throw new Error(`missing review row for ${hit.file}`) await patch(sdk, session.id, edit(hit.file, hit.mark, next)) @@ -179,8 +199,15 @@ test("review keeps scroll position after a live diff update", async ({ page, wit await waitMark(page, hit.file, next) await expect - .poll(async () => Math.abs((await view.evaluate((el) => el.scrollTop)) - prev), { timeout: 60_000 }) - .toBeLessThanOrEqual(16) + .poll( + async () => { + const next = await spot(page, hit.file) + if (!next) return Number.POSITIVE_INFINITY + return Math.max(Math.abs(next.top - prev.top), Math.abs(next.y - prev.y)) + }, + { timeout: 60_000 }, + ) + .toBeLessThanOrEqual(32) }) }) }) From f96e2d4222d70849ef8d4992565dc94e64832243 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:03:15 -0500 Subject: [PATCH 005/245] tweak: adjust skill presentation to be a little less token heavy (#17098) --- packages/opencode/src/session/system.ts | 4 ++- packages/opencode/src/skill/skill.ts | 30 ++++++++++++++--------- packages/opencode/src/tool/skill.ts | 2 +- packages/opencode/test/tool/skill.test.ts | 2 +- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 0f0f6b51b3..a4c4684ffe 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -64,7 +64,9 @@ export namespace SystemPrompt { return [ "Skills provide specialized instructions and workflows for specific tasks.", "Use the skill tool to load a skill when a task matches its description.", - list.length === 0 ? "No skills are currently available." : "\n" + Skill.fmt(list), + // the agents seem to ingest the information about skills a bit better if we present a more verbose + // version of them here and a less verbose version in tool description, rather than vice versa. + Skill.fmt(list, { verbose: true }), ].join("\n") } } diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 09cc787c80..fa984b3e11 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -196,17 +196,23 @@ export namespace Skill { return list.filter((skill) => PermissionNext.evaluate("skill", skill.name, agent.permission).action !== "deny") } - export function fmt(list: Info[]) { - return [ - "", - ...list.flatMap((skill) => [ - ` `, - ` ${skill.name}`, - ` ${skill.description}`, - ` ${pathToFileURL(skill.location).href}`, - ` `, - ]), - "", - ].join("\n") + export function fmt(list: Info[], opts: { verbose: boolean }) { + if (list.length === 0) { + return "No skills are currently available." + } + if (opts.verbose) { + return [ + "", + ...list.flatMap((skill) => [ + ` `, + ` ${skill.name}`, + ` ${skill.description}`, + ` ${pathToFileURL(skill.location).href}`, + ` `, + ]), + "", + ].join("\n") + } + return ["## Available Skills", ...list.flatMap((skill) => `- **${skill.name}**: ${skill.description}`)].join("\n") } } diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 6d2a48b0ed..17016b06f8 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -24,7 +24,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { "The following skills provide specialized sets of instructions for particular tasks", "Invoke this tool to load a skill when a task matches one of the available skills listed below:", "", - Skill.fmt(list), + Skill.fmt(list, { verbose: false }), ].join("\n") const examples = list diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index d5057ba9e7..c47259f155 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -45,7 +45,7 @@ description: Skill for tool tests. fn: async () => { const tool = await SkillTool.init() const skillPath = path.join(tmp.path, ".opencode", "skill", "tool-skill", "SKILL.md") - expect(tool.description).toContain(`${pathToFileURL(skillPath).href}`) + expect(tool.description).toContain(`**tool-skill**: Skill for tool tests.`) }, }) } finally { From a1cda29012e9521b1453950b5490fe18db1db4d5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:10:58 -0500 Subject: [PATCH 006/245] chore: fix test --- packages/app/e2e/session/session-review.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/session/session-review.spec.ts b/packages/app/e2e/session/session-review.spec.ts index 89d04e67d9..fbca8d33ad 100644 --- a/packages/app/e2e/session/session-review.spec.ts +++ b/packages/app/e2e/session/session-review.spec.ts @@ -85,7 +85,10 @@ async function expand(page: Parameters[0]["page"]) { async function waitMark(page: Parameters[0]["page"], file: string, mark: string) { await page.waitForFunction( ({ file, mark }) => { - const head = Array.from(document.querySelectorAll("h3")).find( + const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport') + if (!(view instanceof HTMLElement)) return false + + const head = Array.from(view.querySelectorAll("h3")).find( (node) => node instanceof HTMLElement && node.textContent?.includes(file), ) if (!(head instanceof HTMLElement)) return false @@ -106,7 +109,7 @@ async function spot(page: Parameters[0]["page"], file: string) { const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport') if (!(view instanceof HTMLElement)) return null - const row = Array.from(document.querySelectorAll("h3")).find( + const row = Array.from(view.querySelectorAll("h3")).find( (node) => node instanceof HTMLElement && node.textContent?.includes(file), ) if (!(row instanceof HTMLElement)) return null From 440405dbddd62c57d983fdcf09d9ed55cc6711a8 Mon Sep 17 00:00:00 2001 From: Noam Bressler Date: Wed, 11 Mar 2026 23:18:40 +0200 Subject: [PATCH 007/245] fix: re-enable snapshot in acp (#14918) --- packages/opencode/src/snapshot/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 848f2694e0..72252b7b4c 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -29,7 +29,7 @@ export namespace Snapshot { } export async function cleanup() { - if (Instance.project.vcs !== "git" || Flag.OPENCODE_CLIENT === "acp") return + if (Instance.project.vcs !== "git") return const cfg = await Config.get() if (cfg.snapshot === false) return const git = gitdir() @@ -54,7 +54,7 @@ export namespace Snapshot { } export async function track() { - if (Instance.project.vcs !== "git" || Flag.OPENCODE_CLIENT === "acp") return + if (Instance.project.vcs !== "git") return const cfg = await Config.get() if (cfg.snapshot === false) return const git = gitdir() From 58f45ae22b0faeba4a98910b0d3b42146846462b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:20:56 -0500 Subject: [PATCH 008/245] chore: skip test --- packages/app/e2e/session/session-review.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app/e2e/session/session-review.spec.ts b/packages/app/e2e/session/session-review.spec.ts index fbca8d33ad..28c85edb0b 100644 --- a/packages/app/e2e/session/session-review.spec.ts +++ b/packages/app/e2e/session/session-review.spec.ts @@ -124,6 +124,7 @@ async function spot(page: Parameters[0]["page"], file: string) { } test("review keeps scroll position after a live diff update", async ({ page, withProject }) => { + test.skip(Boolean(process.env.CI), "Flaky in CI for now.") test.setTimeout(180_000) const tag = `review-${Date.now()}` From fbd9b7cf4fb27433a066a10a197c7edb420336d1 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:34:48 -0500 Subject: [PATCH 009/245] feat(app): restore to message and fork session (#17092) --- packages/app/src/i18n/en.ts | 5 + packages/app/src/pages/session.tsx | 116 ++++++++++++++++++ .../composer/session-composer-region.tsx | 40 +++++- .../session/composer/session-revert-dock.tsx | 92 ++++++++++++++ .../src/pages/session/message-timeline.tsx | 7 ++ packages/ui/src/components/icon.tsx | 2 + packages/ui/src/components/message-part.tsx | 59 +++++++++ packages/ui/src/components/session-turn.tsx | 11 +- packages/ui/src/i18n/en.ts | 2 + 9 files changed, 328 insertions(+), 6 deletions(-) create mode 100644 packages/app/src/pages/session/composer/session-revert-dock.tsx diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 97a572f1cf..c87e7cb9db 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -530,6 +530,11 @@ export const dict = { "session.todo.title": "Todos", "session.todo.collapse": "Collapse", "session.todo.expand": "Expand", + "session.revertDock.summary.one": "{{count}} rolled back message", + "session.revertDock.summary.other": "{{count}} rolled back messages", + "session.revertDock.collapse": "Collapse rolled back messages", + "session.revertDock.expand": "Expand rolled back messages", + "session.revertDock.restore": "Restore message", "session.new.title": "Build anything", "session.new.worktree.main": "Main branch", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index a5c7bf90b3..1b62b94294 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -43,6 +43,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel" import { TerminalPanel } from "@/pages/session/terminal-panel" import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" +import { extractPromptFromParts } from "@/utils/prompt" import { same } from "@/utils/same" import { formatServerError } from "@/utils/server-errors" @@ -286,6 +287,7 @@ export default function Page() { const [ui, setUi] = createStore({ git: false, pendingMessage: undefined as string | undefined, + restoring: undefined as string | undefined, reviewSnap: false, scrollGesture: 0, scroll: { @@ -1179,6 +1181,110 @@ export default function Page() { scroller: () => scroller, }) + const draft = (id: string) => + extractPromptFromParts(sync.data.part[id] ?? [], { + directory: sdk.directory, + attachmentName: language.t("common.attachment"), + }) + + const line = (id: string) => { + const text = draft(id) + .map((part) => (part.type === "image" ? `[image:${part.filename}]` : part.content)) + .join("") + .replace(/\s+/g, " ") + .trim() + if (text) return text + return `[${language.t("common.attachment")}]` + } + + const fail = (err: unknown) => { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: formatServerError(err, language.t), + }) + } + + const busy = (sessionID: string) => { + if (sync.data.session_status[sessionID]?.type !== "idle") return true + return (sync.data.message[sessionID] ?? []).some( + (item) => item.role === "assistant" && typeof item.time.completed !== "number", + ) + } + + const halt = (sessionID: string) => + busy(sessionID) ? sdk.client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve() + + const fork = (input: { sessionID: string; messageID: string }) => { + const value = draft(input.messageID) + return sdk.client.session + .fork(input) + .then((result) => { + const next = result.data + if (!next) { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + }) + return + } + navigate(`/${base64Encode(sdk.directory)}/session/${next.id}`) + requestAnimationFrame(() => { + prompt.set(value) + }) + }) + .catch(fail) + } + + const revert = (input: { sessionID: string; messageID: string }) => { + const value = draft(input.messageID) + return halt(input.sessionID) + .then(() => sdk.client.session.revert(input)) + .then(() => { + prompt.set(value) + }) + .catch(fail) + } + + const restore = (id: string) => { + const sessionID = params.id + if (!sessionID || ui.restoring) return + + const next = userMessages().find((item) => item.id > id) + setUi("restoring", id) + + const task = !next + ? halt(sessionID) + .then(() => sdk.client.session.unrevert({ sessionID })) + .then(() => { + prompt.reset() + }) + : halt(sessionID) + .then(() => + sdk.client.session.revert({ + sessionID, + messageID: next.id, + }), + ) + .then(() => { + prompt.set(draft(next.id)) + }) + + return task.catch(fail).finally(() => { + setUi("restoring", (value) => (value === id ? undefined : value)) + }) + } + + const rolled = createMemo(() => { + const id = revertMessageID() + if (!id) return [] + return userMessages() + .filter((item) => item.id >= id) + .map((item) => ({ id: item.id, text: line(item.id) })) + }) + + const actions = { fork, revert } + createResizeObserver( () => promptDock, ({ height }) => { @@ -1268,6 +1374,7 @@ export default function Page() { loadingClass: "px-4 py-4 text-text-weak", emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6", })} + actions={actions} scroll={ui.scroll} onResumeScroll={resumeScroll} setScrollRef={setScrollRef} @@ -1333,6 +1440,15 @@ export default function Page() { resumeScroll() }} onResponseSubmit={resumeScroll} + revert={ + rolled().length > 0 + ? { + items: rolled(), + restoring: ui.restoring, + onRestore: restore, + } + : undefined + } setPromptDockRef={(el) => { promptDock = el }} diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index 93ea3d465c..08746b51a5 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -8,6 +8,7 @@ import { usePrompt } from "@/context/prompt" import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock" import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock" +import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" import type { SessionComposerState } from "@/pages/session/composer/session-composer-state" import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock" @@ -20,6 +21,11 @@ export function SessionComposerRegion(props: { onNewSessionWorktreeReset: () => void onSubmit: () => void onResponseSubmit: () => void + revert?: { + items: { id: string; text: string }[] + restoring?: string + onRestore: (id: string) => void + } setPromptDockRef: (el: HTMLDivElement) => void visualDuration?: number bounce?: number @@ -116,6 +122,8 @@ export function SessionComposerRegion(props: { const value = createMemo(() => Math.max(0, Math.min(1, progress()))) const [height, setHeight] = createSignal(320) const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001) + const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined)) + const lift = createMemo(() => (rolled() ? 18 : 36 * value())) const full = createMemo(() => Math.max(78, height())) const [contentRef, setContentRef] = createSignal() @@ -170,9 +178,22 @@ export function SessionComposerRegion(props: { - {handoffPrompt() || language.t("prompt.loading")} - + <> + + {(revert) => ( +
+ +
+ )} +
+
+ {handoffPrompt() || language.t("prompt.loading")} +
+ } > @@ -209,12 +230,23 @@ export function SessionComposerRegion(props: { + + {(revert) => ( +
+ +
+ )} +
void +}) { + const language = useLanguage() + const [store, setStore] = createStore({ + collapsed: false, + }) + + const toggle = () => setStore("collapsed", (value) => !value) + const total = createMemo(() => props.items.length) + const label = createMemo(() => + language.t(total() === 1 ? "session.revertDock.summary.one" : "session.revertDock.summary.other", { + count: total(), + }), + ) + const preview = createMemo(() => props.items[0]?.text ?? "") + + return ( + +
{ + if (event.key !== "Enter" && event.key !== " ") return + event.preventDefault() + toggle() + }} + > + {label()} + + {preview()} + +
+ { + event.preventDefault() + event.stopPropagation() + }} + onClick={(event) => { + event.stopPropagation() + toggle() + }} + aria-label={ + store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse") + } + /> +
+
+ + + @@ -728,7 +745,7 @@ export const Playground = { max={2.2} step={0.05} value={bounce()} - onInput={(e) => setBounce(Number(e.currentTarget.value))} + onInput={(e) => setState("bounce", Number(e.currentTarget.value))} /> {bounce().toFixed(2)} {bounce() <= 1.05 ? "(none)" : bounce() >= 1.9 ? "(heavy)" : ""} @@ -744,7 +761,7 @@ export const Playground = { max={50} step={1} value={maskSize()} - onInput={(e) => setMaskSize(Number(e.currentTarget.value))} + onInput={(e) => setState("maskSize", Number(e.currentTarget.value))} /> {maskSize()}px {maskSize() === 0 ? "(hard)" : ""} @@ -760,7 +777,7 @@ export const Playground = { max={60} step={1} value={maskPad()} - onInput={(e) => setMaskPad(Number(e.currentTarget.value))} + onInput={(e) => setState("maskPad", Number(e.currentTarget.value))} /> {maskPad()}px
@@ -774,7 +791,7 @@ export const Playground = { max={80} step={1} value={maskHeight()} - onInput={(e) => setMaskHeight(Number(e.currentTarget.value))} + onInput={(e) => setState("maskHeight", Number(e.currentTarget.value))} /> {maskHeight()}px @@ -795,13 +812,13 @@ export const Playground = { - - - @@ -810,8 +827,8 @@ export const Playground = { {HEADINGS.map((h, i) => ( {[0, 1, 2, 3].map((value) => ( - ))} @@ -307,7 +328,7 @@ export const Playground = { max="1" step="0.01" value={dockOpenDuration()} - onInput={(event) => setDockOpenDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("dockOpenDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -324,7 +345,7 @@ export const Playground = { max="1" step="0.01" value={dockOpenBounce()} - onInput={(event) => setDockOpenBounce(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("dockOpenBounce", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -345,7 +366,7 @@ export const Playground = { max="1" step="0.01" value={dockCloseDuration()} - onInput={(event) => setDockCloseDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("dockCloseDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -362,7 +383,7 @@ export const Playground = { max="1" step="0.01" value={dockCloseBounce()} - onInput={(event) => setDockCloseBounce(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("dockCloseBounce", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -383,7 +404,7 @@ export const Playground = { max="1" step="0.01" value={drawerExpandDuration()} - onInput={(event) => setDrawerExpandDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("drawerExpandDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -400,7 +421,7 @@ export const Playground = { max="1" step="0.01" value={drawerExpandBounce()} - onInput={(event) => setDrawerExpandBounce(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("drawerExpandBounce", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -421,7 +442,7 @@ export const Playground = { max="1" step="0.01" value={drawerCollapseDuration()} - onInput={(event) => setDrawerCollapseDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("drawerCollapseDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -438,7 +459,7 @@ export const Playground = { max="1" step="0.01" value={drawerCollapseBounce()} - onInput={(event) => setDrawerCollapseBounce(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("drawerCollapseBounce", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -459,7 +480,7 @@ export const Playground = { max="1400" step="10" value={subtitleDuration()} - onInput={(event) => setSubtitleDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("subtitleDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -473,7 +494,7 @@ export const Playground = { setSubtitleAuto(event.currentTarget.checked)} + onInput={(event) => setCfg("subtitleAuto", event.currentTarget.checked)} /> {subtitleAuto() ? "on" : "off"} @@ -489,7 +510,7 @@ export const Playground = { max="40" step="1" value={subtitleTravel()} - onInput={(event) => setSubtitleTravel(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("subtitleTravel", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {subtitleTravel()}px @@ -504,7 +525,7 @@ export const Playground = { max="40" step="1" value={subtitleEdge()} - onInput={(event) => setSubtitleEdge(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("subtitleEdge", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {subtitleEdge()}% @@ -523,7 +544,7 @@ export const Playground = { max="1400" step="10" value={countDuration()} - onInput={(event) => setCountDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("countDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -540,7 +561,7 @@ export const Playground = { max="40" step="1" value={countMask()} - onInput={(event) => setCountMask(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("countMask", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {countMask()}% @@ -555,7 +576,7 @@ export const Playground = { max="14" step="1" value={countMaskHeight()} - onInput={(event) => setCountMaskHeight(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("countMaskHeight", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {countMaskHeight()}px @@ -570,7 +591,7 @@ export const Playground = { max="1200" step="10" value={countWidthDuration()} - onInput={(event) => setCountWidthDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("countWidthDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> diff --git a/packages/ui/src/components/tool-count-summary.stories.tsx b/packages/ui/src/components/tool-count-summary.stories.tsx index 4be3a02bbe..cf160b188b 100644 --- a/packages/ui/src/components/tool-count-summary.stories.tsx +++ b/packages/ui/src/components/tool-count-summary.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck -import { createSignal, onCleanup } from "solid-js" +import { onCleanup } from "solid-js" +import { createStore } from "solid-js/store" import { AnimatedCountList, type CountItem } from "./tool-count-summary" import { ToolStatusTitle } from "./tool-status-title" @@ -57,11 +58,18 @@ const smallBtn = (active?: boolean) => export const Playground = { render: () => { - const [reads, setReads] = createSignal(0) - const [searches, setSearches] = createSignal(0) - const [lists, setLists] = createSignal(0) - const [active, setActive] = createSignal(false) - const [reducedMotion, setReducedMotion] = createSignal(false) + const [state, setState] = createStore({ + reads: 0, + searches: 0, + lists: 0, + active: false, + reducedMotion: false, + }) + const reads = () => state.reads + const searches = () => state.searches + const lists = () => state.lists + const active = () => state.active + const reducedMotion = () => state.reducedMotion let timeouts: ReturnType[] = [] @@ -74,10 +82,10 @@ export const Playground = { const startSim = () => { clearAll() - setReads(0) - setSearches(0) - setLists(0) - setActive(true) + setState("reads", 0) + setState("searches", 0) + setState("lists", 0) + setState("active", true) const steps = rand(3, 10) let elapsed = 0 @@ -86,27 +94,27 @@ export const Playground = { elapsed += delay const t = setTimeout(() => { const pick = rand(0, 2) - if (pick === 0) setReads((n) => n + 1) - else if (pick === 1) setSearches((n) => n + 1) - else setLists((n) => n + 1) + if (pick === 0) setState("reads", (value) => value + 1) + else if (pick === 1) setState("searches", (value) => value + 1) + else setState("lists", (value) => value + 1) }, elapsed) timeouts.push(t) } - const end = setTimeout(() => setActive(false), elapsed + 100) + const end = setTimeout(() => setState("active", false), elapsed + 100) timeouts.push(end) } const stopSim = () => { clearAll() - setActive(false) + setState("active", false) } const reset = () => { stopSim() - setReads(0) - setSearches(0) - setLists(0) + setState("reads", 0) + setState("searches", 0) + setState("lists", 0) } const items = (): CountItem[] => [ @@ -164,19 +172,19 @@ export const Playground = { -
- - -
diff --git a/packages/ui/src/components/tool-error-card.tsx b/packages/ui/src/components/tool-error-card.tsx index ba39ae586a..0c99924de9 100644 --- a/packages/ui/src/components/tool-error-card.tsx +++ b/packages/ui/src/components/tool-error-card.tsx @@ -1,4 +1,5 @@ -import { type ComponentProps, createMemo, createSignal, Show, splitProps } from "solid-js" +import { type ComponentProps, createMemo, Show, splitProps } from "solid-js" +import { createStore } from "solid-js/store" import { Card, CardDescription } from "./card" import { Collapsible } from "./collapsible" import { Icon } from "./icon" @@ -16,8 +17,12 @@ export interface ToolErrorCardProps extends Omit, "c export function ToolErrorCard(props: ToolErrorCardProps) { const i18n = useI18n() - const [open, setOpen] = createSignal(props.defaultOpen ?? false) - const [copied, setCopied] = createSignal(false) + const [state, setState] = createStore({ + open: props.defaultOpen ?? false, + copied: false, + }) + const open = () => state.open + const copied = () => state.copied const [split, rest] = splitProps(props, ["tool", "error", "defaultOpen", "subtitle", "href"]) const name = createMemo(() => { const map: Record = { @@ -65,13 +70,18 @@ export function ToolErrorCard(props: ToolErrorCardProps) { const text = cleaned() if (!text) return await navigator.clipboard.writeText(text) - setCopied(true) - setTimeout(() => setCopied(false), 2000) + setState("copied", true) + setTimeout(() => setState("copied", false), 2000) } return ( - + setState("open", value)} + >
diff --git a/packages/ui/src/components/tool-status-title.tsx b/packages/ui/src/components/tool-status-title.tsx index 68440b6c63..2a58e0e5bb 100644 --- a/packages/ui/src/components/tool-status-title.tsx +++ b/packages/ui/src/components/tool-status-title.tsx @@ -1,4 +1,5 @@ -import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from "solid-js" +import { Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" import { TextShimmer } from "./text-shimmer" function common(active: string, done: string) { @@ -35,8 +36,12 @@ export function ToolStatusTitle(props: { const activeTail = createMemo(() => (suffix() ? split().active : props.activeText)) const doneTail = createMemo(() => (suffix() ? split().done : props.doneText)) - const [width, setWidth] = createSignal("auto") - const [ready, setReady] = createSignal(false) + const [state, setState] = createStore({ + width: "auto", + ready: false, + }) + const width = () => state.width + const ready = () => state.ready let activeRef: HTMLSpanElement | undefined let doneRef: HTMLSpanElement | undefined let frame: number | undefined @@ -45,7 +50,7 @@ export function ToolStatusTitle(props: { const measure = () => { const target = props.active ? activeRef : doneRef const px = contentWidth(target) - if (px > 0) setWidth(`${px}px`) + if (px > 0) setState("width", `${px}px`) } const schedule = () => { @@ -62,13 +67,13 @@ export function ToolStatusTitle(props: { const finish = () => { if (typeof requestAnimationFrame !== "function") { - setReady(true) + setState("ready", true) return } if (readyFrame !== undefined) cancelAnimationFrame(readyFrame) readyFrame = requestAnimationFrame(() => { readyFrame = undefined - setReady(true) + setState("ready", true) }) } diff --git a/packages/ui/src/pierre/file-find.ts b/packages/ui/src/pierre/file-find.ts index ee608152d5..692ab31670 100644 --- a/packages/ui/src/pierre/file-find.ts +++ b/packages/ui/src/pierre/file-find.ts @@ -1,4 +1,5 @@ -import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { createEffect, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" export type FindHost = { element: () => HTMLElement | undefined @@ -107,11 +108,18 @@ export function createFileFind(opts: CreateFileFindOptions) { let mode: "highlights" | "overlay" = "overlay" let hits: Range[] = [] - const [open, setOpen] = createSignal(false) - const [query, setQuery] = createSignal("") - const [index, setIndex] = createSignal(0) - const [count, setCount] = createSignal(0) - const [pos, setPos] = createSignal({ top: 8, right: 8 }) + const [state, setState] = createStore({ + open: false, + query: "", + index: 0, + count: 0, + pos: { top: 8, right: 8 }, + }) + const open = () => state.open + const query = () => state.query + const index = () => state.index + const count = () => state.count + const pos = () => state.pos const clearOverlayScroll = () => { for (const el of overlayScroll) el.removeEventListener("scroll", scheduleOverlay) @@ -200,8 +208,8 @@ export function createFileFind(opts: CreateFileFindOptions) { clearOverlay() clearOverlayScroll() hits = [] - setCount(0) - setIndex(0) + setState("count", 0) + setState("index", 0) } const positionBar = () => { @@ -214,7 +222,7 @@ export function createFileFind(opts: CreateFileFindOptions) { const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height")) const header = Number.isNaN(title) ? 0 : title - setPos({ + setState("pos", { top: Math.round(rect.top) + header - 4, right: Math.round(window.innerWidth - rect.right) + 8, }) @@ -318,8 +326,8 @@ export function createFileFind(opts: CreateFileFindOptions) { const currentIndex = total ? Math.min(desired, total - 1) : 0 hits = ranges - setCount(total) - setIndex(currentIndex) + setState("count", total) + setState("index", currentIndex) const active = ranges[currentIndex] if (mode === "highlights") { @@ -342,8 +350,8 @@ export function createFileFind(opts: CreateFileFindOptions) { } const close = () => { - setOpen(false) - setQuery("") + setState("open", false) + setState("query", "") clearFind() if (current === host) current = undefined } @@ -352,7 +360,7 @@ export function createFileFind(opts: CreateFileFindOptions) { if (current && current !== host) current.close() current = host target = host - if (!open()) setOpen(true) + if (!open()) setState("open", true) requestAnimationFrame(() => { apply({ scroll: true }) input?.focus() @@ -366,7 +374,7 @@ export function createFileFind(opts: CreateFileFindOptions) { if (total <= 0) return const currentIndex = (index() + dir + total) % total - setIndex(currentIndex) + setState("index", currentIndex) const active = hits[currentIndex] if (!active) return @@ -449,8 +457,8 @@ export function createFileFind(opts: CreateFileFindOptions) { input = el }, setQuery: (value: string) => { - setQuery(value) - setIndex(0) + setState("query", value) + setState("index", 0) apply({ reset: true, scroll: true }) }, focus, From 05cb3c87ca387be41aceb5ccad978c6848a56f70 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:48:38 -0500 Subject: [PATCH 104/245] chore(app): i18n sync (#17283) --- packages/app/src/app.tsx | 15 +++- packages/app/src/components/debug-bar.tsx | 70 +++++++++-------- .../app/src/components/dialog-select-file.tsx | 2 +- .../src/components/dialog-select-server.tsx | 4 +- .../app/src/components/server/server-row.tsx | 4 +- .../src/components/session/session-header.tsx | 54 ++++++------- .../session/session-sortable-terminal-tab.tsx | 7 +- .../app/src/components/settings-keybinds.tsx | 2 +- packages/app/src/components/terminal.tsx | 2 +- packages/app/src/context/command.tsx | 61 ++++++++++++--- packages/app/src/context/file.tsx | 6 +- packages/app/src/context/global-sdk.tsx | 6 +- packages/app/src/context/global-sync.tsx | 1 + .../app/src/context/global-sync/bootstrap.ts | 2 +- .../context/global-sync/child-store.test.ts | 1 + .../src/context/global-sync/child-store.ts | 9 ++- packages/app/src/context/terminal-title.ts | 51 ++++++++++++ packages/app/src/context/terminal.tsx | 11 +-- packages/app/src/i18n/ar.ts | 73 ++++++++++++++++++ packages/app/src/i18n/br.ts | 75 ++++++++++++++++++ packages/app/src/i18n/bs.ts | 75 ++++++++++++++++++ packages/app/src/i18n/da.ts | 75 ++++++++++++++++++ packages/app/src/i18n/de.ts | 76 ++++++++++++++++++ packages/app/src/i18n/en.ts | 76 ++++++++++++++++++ packages/app/src/i18n/es.ts | 75 ++++++++++++++++++ packages/app/src/i18n/fr.ts | 77 +++++++++++++++++++ packages/app/src/i18n/ja.ts | 74 ++++++++++++++++++ packages/app/src/i18n/ko.ts | 74 ++++++++++++++++++ packages/app/src/i18n/no.ts | 75 ++++++++++++++++++ packages/app/src/i18n/pl.ts | 76 ++++++++++++++++++ packages/app/src/i18n/ru.ts | 75 ++++++++++++++++++ packages/app/src/i18n/th.ts | 75 ++++++++++++++++++ packages/app/src/i18n/tr.ts | 74 ++++++++++++++++++ packages/app/src/i18n/zh.ts | 73 ++++++++++++++++++ packages/app/src/i18n/zht.ts | 73 ++++++++++++++++++ packages/app/src/pages/error.tsx | 32 ++++---- packages/app/src/pages/layout.tsx | 2 +- packages/app/src/pages/session.tsx | 8 +- .../composer/session-composer-region.tsx | 1 - .../composer/session-question-dock.tsx | 2 +- .../session/composer/session-todo-dock.tsx | 28 +++++-- .../app/src/pages/session/terminal-label.ts | 6 +- packages/ui/src/components/basic-tool.tsx | 5 +- packages/ui/src/components/file-search.tsx | 11 ++- .../components/line-comment-annotations.tsx | 8 +- packages/ui/src/components/message-part.tsx | 20 ++--- .../ui/src/components/tool-error-card.tsx | 16 ++-- packages/ui/src/i18n/ar.ts | 12 +++ packages/ui/src/i18n/br.ts | 12 +++ packages/ui/src/i18n/bs.ts | 12 +++ packages/ui/src/i18n/da.ts | 12 +++ packages/ui/src/i18n/de.ts | 12 +++ packages/ui/src/i18n/en.ts | 13 ++++ packages/ui/src/i18n/es.ts | 12 +++ packages/ui/src/i18n/fr.ts | 12 +++ packages/ui/src/i18n/ja.ts | 12 +++ packages/ui/src/i18n/ko.ts | 12 +++ packages/ui/src/i18n/no.ts | 12 +++ packages/ui/src/i18n/pl.ts | 12 +++ packages/ui/src/i18n/ru.ts | 12 +++ packages/ui/src/i18n/th.ts | 12 +++ packages/ui/src/i18n/tr.ts | 12 +++ packages/ui/src/i18n/zh.ts | 12 +++ packages/ui/src/i18n/zht.ts | 12 +++ packages/ui/src/pierre/selection-bridge.ts | 9 ++- 65 files changed, 1776 insertions(+), 156 deletions(-) create mode 100644 packages/app/src/context/terminal-title.ts diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index c6fca36d59..e370862212 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -12,6 +12,7 @@ import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" import { type Duration, Effect } from "effect" import { type Component, + createMemo, createResource, createSignal, ErrorBoundary, @@ -67,7 +68,7 @@ const SessionIndexRoute = () => function UiI18nBridge(props: ParentProps) { const language = useLanguage() - return {props.children} + return {props.children} } declare global { @@ -218,8 +219,12 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { } function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) { + const language = useLanguage() const server = useServer() const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key) + const name = createMemo(() => server.name || server.key) + const serverToken = "\u0000server\u0000" + const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken)) const timer = setInterval(() => props.onRetry?.(), 1000) onCleanup(() => clearInterval(timer)) @@ -229,13 +234,15 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:

- Could not reach {server.name || server.key} + {unreachable()[0]} + {name()} + {unreachable()[1]}

-

Retrying automatically...

+

{language.t("app.server.retrying")}

0}>
- Other servers + {language.t("app.server.otherServers")}
{(conn) => { diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx index 6fde71f3b2..cbb24f77bc 100644 --- a/packages/app/src/components/debug-bar.tsx +++ b/packages/app/src/components/debug-bar.tsx @@ -2,6 +2,7 @@ import { useIsRouting, useLocation } from "@solidjs/router" import { batch, createEffect, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" import { Tooltip } from "@opencode-ai/ui/tooltip" +import { useLanguage } from "@/context/language" type Mem = Performance & { memory?: { @@ -27,17 +28,17 @@ type Obs = PerformanceObserverInit & { const span = 5000 const ms = (n?: number, d = 0) => { - if (n === undefined || Number.isNaN(n)) return "n/a" + if (n === undefined || Number.isNaN(n)) return return `${n.toFixed(d)}ms` } const time = (n?: number) => { - if (n === undefined || Number.isNaN(n)) return "n/a" + if (n === undefined || Number.isNaN(n)) return return `${Math.round(n)}` } const mb = (n?: number) => { - if (n === undefined || Number.isNaN(n)) return "n/a" + if (n === undefined || Number.isNaN(n)) return const v = n / 1024 / 1024 return `${v >= 1024 ? v.toFixed(0) : v.toFixed(1)}MB` } @@ -74,6 +75,7 @@ function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; } export function DebugBar() { + const language = useLanguage() const location = useLocation() const routing = useIsRouting() const [state, setState] = createStore({ @@ -98,14 +100,15 @@ export function DebugBar() { }, }) + const na = () => language.t("debugBar.na") const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined) const heapv = () => { const value = heap() - if (value === undefined) return "n/a" + if (value === undefined) return na() return `${Math.round(value * 100)}%` } - const longv = () => (state.long.count === undefined ? "n/a" : `${time(state.long.block)}/${state.long.count}`) - const navv = () => (state.nav.pending ? "..." : time(state.nav.dur)) + const longv = () => (state.long.count === undefined ? na() : `${time(state.long.block) ?? na()}/${state.long.count}`) + const navv = () => (state.nav.pending ? "..." : (time(state.nav.dur) ?? na())) let prev = "" let start = 0 @@ -359,7 +362,7 @@ export function DebugBar() { return (
diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 655aba0b02..eb039c14d6 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -149,7 +149,7 @@ function ServerForm(props: ServerFormProps) { {conn().http.username} ) : ( - no username + {language.t("server.row.noUsername")} )} {conn().http.password && ••••••••} diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 8cb704bf1d..ae9d2800ed 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -46,63 +46,63 @@ type OS = "macos" | "windows" | "linux" | "unknown" const MAC_APPS = [ { id: "vscode", - label: "VS Code", + label: "session.header.open.app.vscode", icon: "vscode", openWith: "Visual Studio Code", }, - { id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" }, - { id: "zed", label: "Zed", icon: "zed", openWith: "Zed" }, - { id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" }, + { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" }, + { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" }, + { id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" }, { id: "antigravity", - label: "Antigravity", + label: "session.header.open.app.antigravity", icon: "antigravity", openWith: "Antigravity", }, - { id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" }, - { id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" }, - { id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" }, - { id: "warp", label: "Warp", icon: "warp", openWith: "Warp" }, - { id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" }, + { id: "terminal", label: "session.header.open.app.terminal", icon: "terminal", openWith: "Terminal" }, + { id: "iterm2", label: "session.header.open.app.iterm2", icon: "iterm2", openWith: "iTerm" }, + { id: "ghostty", label: "session.header.open.app.ghostty", icon: "ghostty", openWith: "Ghostty" }, + { id: "warp", label: "session.header.open.app.warp", icon: "warp", openWith: "Warp" }, + { id: "xcode", label: "session.header.open.app.xcode", icon: "xcode", openWith: "Xcode" }, { id: "android-studio", - label: "Android Studio", + label: "session.header.open.app.androidStudio", icon: "android-studio", openWith: "Android Studio", }, { id: "sublime-text", - label: "Sublime Text", + label: "session.header.open.app.sublimeText", icon: "sublime-text", openWith: "Sublime Text", }, ] as const const WINDOWS_APPS = [ - { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" }, - { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" }, - { id: "zed", label: "Zed", icon: "zed", openWith: "zed" }, + { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" }, + { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" }, + { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" }, { id: "powershell", - label: "PowerShell", + label: "session.header.open.app.powershell", icon: "powershell", openWith: "powershell", }, { id: "sublime-text", - label: "Sublime Text", + label: "session.header.open.app.sublimeText", icon: "sublime-text", openWith: "Sublime Text", }, ] as const const LINUX_APPS = [ - { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" }, - { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" }, - { id: "zed", label: "Zed", icon: "zed", openWith: "zed" }, + { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" }, + { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" }, + { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" }, { id: "sublime-text", - label: "Sublime Text", + label: "session.header.open.app.sublimeText", icon: "sublime-text", openWith: "Sublime Text", }, @@ -160,9 +160,9 @@ export function SessionHeader() { }) const fileManager = createMemo(() => { - if (os() === "macos") return { label: "Finder", icon: "finder" as const } - if (os() === "windows") return { label: "File Explorer", icon: "file-explorer" as const } - return { label: "File Manager", icon: "finder" as const } + if (os() === "macos") return { label: "session.header.open.finder", icon: "finder" as const } + if (os() === "windows") return { label: "session.header.open.fileExplorer", icon: "file-explorer" as const } + return { label: "session.header.open.fileManager", icon: "finder" as const } }) createEffect(() => { @@ -187,8 +187,10 @@ export function SessionHeader() { const options = createMemo(() => { return [ - { id: "finder", label: fileManager().label, icon: fileManager().icon }, - ...apps().filter((app) => exists[app.id]), + { id: "finder", label: language.t(fileManager().label), icon: fileManager().icon }, + ...apps() + .filter((app) => exists[app.id]) + .map((app) => ({ ...app, label: language.t(app.label) })), ] as const }) diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx index 4f49911c12..8989587425 100644 --- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx +++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx @@ -6,6 +6,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Tabs } from "@opencode-ai/ui/tabs" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Icon } from "@opencode-ai/ui/icon" +import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title" import { useTerminal, type LocalPTY } from "@/context/terminal" import { useLanguage } from "@/context/language" import { focusTerminalById } from "@/pages/session/helpers" @@ -27,11 +28,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => const isDefaultTitle = () => { const number = props.terminal.titleNumber if (!Number.isFinite(number) || number <= 0) return false - const match = props.terminal.title.match(/^Terminal (\d+)$/) - if (!match) return false - const parsed = Number(match[1]) - if (!Number.isFinite(parsed) || parsed <= 0) return false - return parsed === number + return isDefaultTerminalTitle(props.terminal.title, number) } const label = () => { diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index 1e42447895..7e2a48110c 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -239,7 +239,7 @@ function useKeyCapture(input: { showToast({ title: input.language.t("settings.shortcuts.conflict.title"), description: input.language.t("settings.shortcuts.conflict.description", { - keybind: formatKeybind(next), + keybind: formatKeybind(next, input.language.t), titles: [...conflicts.values()].join(", "), }), }) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 8409032938..ff455ebe20 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -519,7 +519,7 @@ export const Terminal = (props: TerminalProps) => { if (event.code !== 1000) { if (once.value) return once.value = true - local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`)) + local.onConnectError?.(new Error(language.t("terminal.connectionLost.abnormalClose", { code: event.code }))) } } socket.addEventListener("close", handleClose) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 03bd6318da..2c6799d12b 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -2,6 +2,7 @@ import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "sol import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { dict as en } from "@/i18n/en" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { Persist, persisted } from "@/utils/persist" @@ -13,6 +14,27 @@ const DEFAULT_PALETTE_KEYBIND = "mod+shift+p" const SUGGESTED_PREFIX = "suggested." const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"]) +type KeyLabel = + | "common.key.ctrl" + | "common.key.alt" + | "common.key.shift" + | "common.key.meta" + | "common.key.space" + | "common.key.backspace" + | "common.key.enter" + | "common.key.tab" + | "common.key.delete" + | "common.key.home" + | "common.key.end" + | "common.key.pageUp" + | "common.key.pageDown" + | "common.key.insert" + | "common.key.esc" + +function keyText(key: KeyLabel, t?: (key: KeyLabel) => string) { + return t ? t(key) : en[key] +} + function actionId(id: string) { if (!id.startsWith(SUGGESTED_PREFIX)) return id return id.slice(SUGGESTED_PREFIX.length) @@ -145,7 +167,7 @@ export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean return false } -export function formatKeybind(config: string): string { +export function formatKeybind(config: string, t?: (key: KeyLabel) => string): string { if (!config || config === "none") return "" const keybinds = parseKeybind(config) @@ -154,10 +176,10 @@ export function formatKeybind(config: string): string { const kb = keybinds[0] const parts: string[] = [] - if (kb.ctrl) parts.push(IS_MAC ? "⌃" : "Ctrl") - if (kb.alt) parts.push(IS_MAC ? "⌥" : "Alt") - if (kb.shift) parts.push(IS_MAC ? "⇧" : "Shift") - if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta") + if (kb.ctrl) parts.push(IS_MAC ? "⌃" : keyText("common.key.ctrl", t)) + if (kb.alt) parts.push(IS_MAC ? "⌥" : keyText("common.key.alt", t)) + if (kb.shift) parts.push(IS_MAC ? "⇧" : keyText("common.key.shift", t)) + if (kb.meta) parts.push(IS_MAC ? "⌘" : keyText("common.key.meta", t)) if (kb.key) { const keys: Record = { @@ -167,10 +189,29 @@ export function formatKeybind(config: string): string { arrowright: "→", comma: ",", plus: "+", - space: "Space", + } + const named: Record = { + backspace: "common.key.backspace", + delete: "common.key.delete", + end: "common.key.end", + enter: "common.key.enter", + esc: "common.key.esc", + escape: "common.key.esc", + home: "common.key.home", + insert: "common.key.insert", + pagedown: "common.key.pageDown", + pageup: "common.key.pageUp", + space: "common.key.space", + tab: "common.key.tab", } const key = kb.key.toLowerCase() - const displayKey = keys[key] ?? (key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1)) + const displayKey = + keys[key] ?? + (named[key] + ? keyText(named[key], t) + : key.length === 1 + ? key.toUpperCase() + : key.charAt(0).toUpperCase() + key.slice(1)) parts.push(displayKey) } @@ -364,17 +405,17 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex }, keybind(id: string) { if (id === PALETTE_ID) { - return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND) + return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND, language.t) } const base = actionId(id) const option = options().find((x) => actionId(x.id) === base) - if (option?.keybind) return formatKeybind(option.keybind) + if (option?.keybind) return formatKeybind(option.keybind, language.t) const meta = catalog[base] const config = bind(base, meta?.keybind) if (!config) return "" - return formatKeybind(config) + return formatKeybind(config, language.t) }, show: showPalette, keybinds(enabled: boolean) { diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 99c6d2e421..f8fec7142d 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -43,10 +43,10 @@ export { touchFileContent, } -function errorMessage(error: unknown) { +function errorMessage(error: unknown, fallback: string) { if (error instanceof Error && error.message) return error.message if (typeof error === "string" && error) return error - return "Unknown error" + return fallback } export const { use: useFile, provider: FileProvider } = createSimpleContext({ @@ -184,7 +184,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ }) .catch((e) => { if (scope() !== directory) return - setLoadError(file, errorMessage(e)) + setLoadError(file, errorMessage(e, language.t("error.chain.unknown"))) }) .finally(() => { inflight.delete(key) diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index c1a87b95b8..60e9fd6d54 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -4,6 +4,7 @@ import { createGlobalEmitter } from "@solid-primitives/event-bus" import { batch, onCleanup } from "solid-js" import z from "zod" import { createSdkForServer } from "@/utils/server" +import { useLanguage } from "./language" import { usePlatform } from "./platform" import { useServer } from "./server" @@ -14,6 +15,7 @@ const abortError = z.object({ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({ name: "GlobalSDK", init: () => { + const language = useLanguage() const server = useServer() const platform = usePlatform() const abort = new AbortController() @@ -30,7 +32,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo })() const currentServer = server.current - if (!currentServer) throw new Error("No server available") + if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable")) const eventSdk = createSdkForServer({ signal: abort.signal, @@ -218,7 +220,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo event: emitter, createClient(opts: Omit[0], "server" | "fetch">) { const s = server.current - if (!s) throw new Error("Server not available") + if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable")) return createSdkForServer({ server: s.http, fetch: platform.fetch, diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 1b6cdf530a..c840988692 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -164,6 +164,7 @@ function createGlobalSync() { sdkCache.delete(directory) clearSessionPrefetchDirectory(directory) }, + translate: language.t, }) const sdkFor = (directory: string) => { diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 8b1a3c48c5..13494b7ade 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -139,7 +139,7 @@ export async function bootstrapDirectory(input: { const project = getFilename(input.directory) showToast({ variant: "error", - title: `Failed to reload ${project}`, + title: input.translate("toast.project.reloadFailed.title", { project }), description: formatServerError(err, input.translate), }) input.setStore("status", "partial") diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts index cec76ff87e..eee763f16d 100644 --- a/packages/app/src/context/global-sync/child-store.test.ts +++ b/packages/app/src/context/global-sync/child-store.test.ts @@ -21,6 +21,7 @@ describe("createChildStoreManager", () => { isLoadingSessions: () => false, onBootstrap() {}, onDispose() {}, + translate: (key) => key, }) Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => { diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index e2ada244fb..d5904c6096 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -21,6 +21,7 @@ export function createChildStoreManager(input: { isLoadingSessions: (directory: string) => boolean onBootstrap: (directory: string) => void onDispose: (directory: string) => void + translate: (key: string, vars?: Record) => string }) { const children: Record, SetStoreFunction]> = {} const vcsCache = new Map() @@ -129,7 +130,7 @@ export function createChildStoreManager(input: { createStore({ value: undefined as VcsInfo | undefined }), ), ) - if (!vcs) throw new Error("Failed to create persisted cache") + if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed")) const vcsStore = vcs[0] vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] }) @@ -139,7 +140,7 @@ export function createChildStoreManager(input: { createStore({ value: undefined as ProjectMeta | undefined }), ), ) - if (!meta) throw new Error("Failed to create persisted project metadata") + if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed")) metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] }) const icon = runWithOwner(input.owner, () => @@ -148,7 +149,7 @@ export function createChildStoreManager(input: { createStore({ value: undefined as string | undefined }), ), ) - if (!icon) throw new Error("Failed to create persisted project icon") + if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed")) iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] }) const init = () => @@ -211,7 +212,7 @@ export function createChildStoreManager(input: { } mark(directory) const childStore = children[directory] - if (!childStore) throw new Error("Failed to create store") + if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed")) return childStore } diff --git a/packages/app/src/context/terminal-title.ts b/packages/app/src/context/terminal-title.ts new file mode 100644 index 0000000000..3e8fa9af25 --- /dev/null +++ b/packages/app/src/context/terminal-title.ts @@ -0,0 +1,51 @@ +import { dict as ar } from "@/i18n/ar" +import { dict as br } from "@/i18n/br" +import { dict as bs } from "@/i18n/bs" +import { dict as da } from "@/i18n/da" +import { dict as de } from "@/i18n/de" +import { dict as en } from "@/i18n/en" +import { dict as es } from "@/i18n/es" +import { dict as fr } from "@/i18n/fr" +import { dict as ja } from "@/i18n/ja" +import { dict as ko } from "@/i18n/ko" +import { dict as no } from "@/i18n/no" +import { dict as pl } from "@/i18n/pl" +import { dict as ru } from "@/i18n/ru" +import { dict as th } from "@/i18n/th" +import { dict as tr } from "@/i18n/tr" +import { dict as zh } from "@/i18n/zh" +import { dict as zht } from "@/i18n/zht" + +const numbered = Array.from( + new Set([ + en["terminal.title.numbered"], + ar["terminal.title.numbered"], + br["terminal.title.numbered"], + bs["terminal.title.numbered"], + da["terminal.title.numbered"], + de["terminal.title.numbered"], + es["terminal.title.numbered"], + fr["terminal.title.numbered"], + ja["terminal.title.numbered"], + ko["terminal.title.numbered"], + no["terminal.title.numbered"], + pl["terminal.title.numbered"], + ru["terminal.title.numbered"], + th["terminal.title.numbered"], + tr["terminal.title.numbered"], + zh["terminal.title.numbered"], + zht["terminal.title.numbered"], + ]), +) + +export function defaultTitle(number: number) { + return en["terminal.title.numbered"].replace("{{number}}", String(number)) +} + +export function isDefaultTitle(title: string, number: number) { + return numbered.some((text) => title === text.replace("{{number}}", String(number))) +} + +export function titleNumber(title: string, max: number) { + return Array.from({ length: max }, (_, idx) => idx + 1).find((number) => isDefaultTitle(title, number)) +} diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index a2807375ff..e65c167884 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -4,6 +4,7 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli import { useParams } from "@solidjs/router" import { useSDK } from "./sdk" import type { Platform } from "./platform" +import { defaultTitle, titleNumber } from "./terminal-title" import { Persist, persisted, removePersisted } from "@/utils/persist" export type LocalPTY = { @@ -33,11 +34,7 @@ function num(value: unknown) { } function numberFromTitle(title: string) { - const match = title.match(/^Terminal (\d+)$/) - if (!match) return - const value = Number(match[1]) - if (!Number.isFinite(value) || value <= 0) return - return value + return titleNumber(title, MAX_TERMINAL_SESSIONS) } function pty(value: unknown): LocalPTY | undefined { @@ -202,13 +199,13 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str const nextNumber = pickNextTerminalNumber() sdk.client.pty - .create({ title: `Terminal ${nextNumber}` }) + .create({ title: defaultTitle(nextNumber) }) .then((pty: { data?: { id?: string; title?: string } }) => { const id = pty.data?.id if (!id) return const newTerminal = { id, - title: pty.data?.title ?? "Terminal", + title: pty.data?.title ?? defaultTitle(nextNumber), titleNumber: nextNumber, } setStore("all", store.all.length, newTerminal) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 1b87208267..99a2d03d09 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -778,4 +778,77 @@ export const dict = { "common.time.daysAgo.short": "قبل {{count}} ي", "settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك", "settings.providers.custom.description": "أضف مزود متوافق مع OpenAI بواسطة عنوان URL الأساسي.", + + "app.server.unreachable": "تعذر الوصول إلى {{server}}", + "app.server.retrying": "جاري إعادة المحاولة تلقائيًا...", + "app.server.otherServers": "خوادم أخرى", + "dialog.server.add.usernamePlaceholder": "اسم المستخدم", + "dialog.server.add.passwordPlaceholder": "كلمة المرور", + "server.row.noUsername": "لا يوجد اسم مستخدم", + "session.review.noVcs.createGit.title": "إنشاء مستودع Git", + "session.review.noVcs.createGit.description": "تتبع ومراجعة والتراجع عن التغييرات في هذا المشروع", + "session.review.noVcs.createGit.actionLoading": "جاري إنشاء مستودع Git...", + "session.review.noVcs.createGit.action": "إنشاء مستودع Git", + "session.todo.progress": "تم إكمال {{done}} من {{total}} مهام", + "session.question.progress": "{{current}} من {{total}} أسئلة", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "مستكشف الملفات", + "session.header.open.fileManager": "مدير الملفات", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "المحطة الطرفية", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "تشخيص أداء التطوير", + "debugBar.na": "غير متاح", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": "آخر انتقال مكتمل للمسار يمس صفحة جلسة، مُقاسًا من بدء التوجيه حتى أول رسم بعد استقراره.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "الإطارات المتجددة في الثانية خلال آخر 5 ثوانٍ.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "أسوأ وقت للإطار خلال آخر 5 ثوانٍ.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "الإطارات التي تزيد عن 32 مللي ثانية في آخر 5 ثوانٍ.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "الوقت المحظور وعدد المهام الطويلة في آخر 5 ثوانٍ. أقصى مهمة: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "أسوأ تأخير إدخال تمت ملاحظته في آخر 5 ثوانٍ.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "مدة التفاعل التقريبية خلال آخر 5 ثوانٍ. هذا يشبه INP، وليس Web Vitals INP الرسمي.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "التحول التخطيطي التراكمي لعمر التطبيق الحالي.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "كومة JS المستخدمة مقابل حد الكومة. Chromium فقط.", + "debugBar.mem.tip": "كومة JS المستخدمة مقابل حد الكومة. {{used}} من {{limit}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "غير معروف", + "error.page.circular": "[دائري]", + "error.globalSDK.noServerAvailable": "لا يوجد خادم متاح", + "error.globalSDK.serverNotAvailable": "الخادم غير متاح", + "error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة", + "error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة", + "error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة", + "error.childStore.storeCreateFailed": "فشل إنشاء المخزن", + "terminal.connectionLost.abnormalClose": "تم إغلاق WebSocket بشكل غير طبيعي: {{code}}", } diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 9e1f7f2a0e..46ee7f114e 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -788,4 +788,79 @@ export const dict = { "common.time.daysAgo.short": "{{count}}d atrás", "settings.providers.connected.environmentDescription": "Conectado a partir de suas variáveis de ambiente", "settings.providers.custom.description": "Adicionar um provedor compatível com a OpenAI através do URL base.", + + "app.server.unreachable": "Não foi possível conectar a {{server}}", + "app.server.retrying": "Tentando novamente automaticamente...", + "app.server.otherServers": "Outros servidores", + "dialog.server.add.usernamePlaceholder": "nome de usuário", + "dialog.server.add.passwordPlaceholder": "senha", + "server.row.noUsername": "sem nome de usuário", + "session.review.noVcs.createGit.title": "Criar um repositório Git", + "session.review.noVcs.createGit.description": "Rastreie, revise e desfaça alterações neste projeto", + "session.review.noVcs.createGit.actionLoading": "Criando repositório Git...", + "session.review.noVcs.createGit.action": "Criar repositório Git", + "session.todo.progress": "{{done}} de {{total}} tarefas concluídas", + "session.question.progress": "{{current}} de {{total}} perguntas", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Explorador de Arquivos", + "session.header.open.fileManager": "Gerenciador de Arquivos", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Diagnóstico de desempenho de desenvolvimento", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Última transição de rota concluída tocando em uma página de sessão, medida desde o início do roteador até a primeira pintura após o estabelecimento.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Quadros por segundo nos últimos 5 segundos.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Pior tempo de quadro nos últimos 5 segundos.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Quadros acima de 32ms nos últimos 5 segundos.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Tempo bloqueado e contagem de tarefas longas nos últimos 5 segundos. Tarefa máx: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Pior atraso de entrada observado nos últimos 5 segundos.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Duração aproximada da interação nos últimos 5 segundos. Isso é semelhante ao INP, não o INP oficial do Web Vitals.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Mudança cumulativa de layout para o tempo de vida atual do aplicativo.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Heap JS usado vs limite de heap. Apenas Chromium.", + "debugBar.mem.tip": "Heap JS usado vs limite de heap. {{used}} de {{limit}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Espaço", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "desconhecido", + "error.page.circular": "[Circular]", + "error.globalSDK.noServerAvailable": "Nenhum servidor disponível", + "error.globalSDK.serverNotAvailable": "Servidor indisponível", + "error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente", + "error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes", + "error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente", + "error.childStore.storeCreateFailed": "Falha ao criar armazenamento", + "terminal.connectionLost.abnormalClose": "WebSocket fechado anormalmente: {{code}}", } diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 3151c9b221..140b838103 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -864,4 +864,79 @@ export const dict = { "common.time.daysAgo.short": "prije {{count}} d", "settings.providers.connected.environmentDescription": "Povezano sa vašim varijablama okruženja", "settings.providers.custom.description": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.", + + "app.server.unreachable": "Nije moguće pristupiti {{server}}", + "app.server.retrying": "Automatski ponovni pokušaj...", + "app.server.otherServers": "Drugi serveri", + "dialog.server.add.usernamePlaceholder": "korisničko ime", + "dialog.server.add.passwordPlaceholder": "lozinka", + "server.row.noUsername": "nema korisničkog imena", + "session.review.noVcs.createGit.title": "Kreiraj Git repozitorij", + "session.review.noVcs.createGit.description": "Pratite, pregledajte i poništite promjene u ovom projektu", + "session.review.noVcs.createGit.actionLoading": "Kreiranje Git repozitorija...", + "session.review.noVcs.createGit.action": "Kreiraj Git repozitorij", + "session.todo.progress": "{{done}} od {{total}} zadataka završeno", + "session.question.progress": "{{current}} od {{total}} pitanja", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "File Manager", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Dijagnostika performansi razvoja", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Posljednji završeni prelazak rute koji dotiče stranicu sesije, mjeren od početka rutera do prvog iscrtavanja nakon smirivanja.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Kadrovi u sekundi tokom posljednjih 5 sekundi.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Najgore vrijeme kadra u posljednjih 5 sekundi.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Kadrovi duži od 32ms u posljednjih 5 sekundi.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blokirano vrijeme i broj dugih zadataka u posljednjih 5 sekundi. Maks zadatak: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Najgore zabilježeno kašnjenje unosa u posljednjih 5 sekundi.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Približno trajanje interakcije tokom posljednjih 5 sekundi. Ovo je slično INP-u, nije službeni Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulativni pomak rasporeda za trenutni životni vijek aplikacije.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Korišteni JS heap naspram limita heapa. Samo Chromium.", + "debugBar.mem.tip": "Korišteni JS heap naspram limita heapa. {{used}} od {{limit}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "nepoznato", + "error.page.circular": "[Kružno]", + "error.globalSDK.noServerAvailable": "Nema dostupnog servera", + "error.globalSDK.serverNotAvailable": "Server nije dostupan", + "error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša", + "error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta", + "error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta", + "error.childStore.storeCreateFailed": "Nije uspjelo kreiranje skladišta", + "terminal.connectionLost.abnormalClose": "WebSocket zatvoren nenormalno: {{code}}", } diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 8d9331ab68..9b776c143e 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -858,4 +858,79 @@ export const dict = { "common.time.daysAgo.short": "{{count}}d siden", "settings.providers.connected.environmentDescription": "Tilsluttet fra dine miljøvariabler", "settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.", + + "app.server.unreachable": "Kunne ikke nå {{server}}", + "app.server.retrying": "Prøver igen automatisk...", + "app.server.otherServers": "Andre servere", + "dialog.server.add.usernamePlaceholder": "brugernavn", + "dialog.server.add.passwordPlaceholder": "adgangskode", + "server.row.noUsername": "intet brugernavn", + "session.review.noVcs.createGit.title": "Opret et Git-repository", + "session.review.noVcs.createGit.description": "Spor, gennemgå og fortryd ændringer i dette projekt", + "session.review.noVcs.createGit.actionLoading": "Opretter Git-repository...", + "session.review.noVcs.createGit.action": "Opret Git-repository", + "session.todo.progress": "{{done}} af {{total}} opgaver fuldført", + "session.question.progress": "{{current}} af {{total}} spørgsmål", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Stifinder", + "session.header.open.fileManager": "Filhåndtering", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Udviklingsydelsesdiagnostik", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Sidste gennemførte ruteovergang, der berører en sessionsside, målt fra routerstart til den første optegning efter den falder til ro.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Rullende billeder pr. sekund over de sidste 5 sekunder.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Værste billedtid over de sidste 5 sekunder.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Billeder over 32ms i de sidste 5 sekunder.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blokeret tid og antal lange opgaver i de sidste 5 sekunder. Maks opgave: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Værste observerede inputforsinkelse i de sidste 5 sekunder.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Omtrentlig interaktionsvarighed over de sidste 5 sekunder. Dette er INP-lignende, ikke den officielle Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulativt layoutskift for den nuværende app-levetid.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Brugt JS-heap vs heap-grænse. Kun Chromium.", + "debugBar.mem.tip": "Brugt JS-heap vs heap-grænse. {{used}} af {{limit}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Mellemrum", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "ukendt", + "error.page.circular": "[Cirkulær]", + "error.globalSDK.noServerAvailable": "Ingen server tilgængelig", + "error.globalSDK.serverNotAvailable": "Server ikke tilgængelig", + "error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache", + "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata", + "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon", + "error.childStore.storeCreateFailed": "Kunne ikke oprette lager", + "terminal.connectionLost.abnormalClose": "WebSocket lukkede unormalt: {{code}}", } diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 782b67262f..5031748b46 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -799,4 +799,80 @@ export const dict = { "common.time.daysAgo.short": "vor {{count}} Tg", "settings.providers.connected.environmentDescription": "Verbunden aus Ihren Umgebungsvariablen", "settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.", + + "app.server.unreachable": "Konnte {{server}} nicht erreichen", + "app.server.retrying": "Automatische erneute Verbindung...", + "app.server.otherServers": "Andere Server", + "dialog.server.add.usernamePlaceholder": "Benutzername", + "dialog.server.add.passwordPlaceholder": "Passwort", + "server.row.noUsername": "Kein Benutzername", + "session.review.noVcs.createGit.title": "Git-Repository erstellen", + "session.review.noVcs.createGit.description": + "Änderungen in diesem Projekt verfolgen, überprüfen und rückgängig machen", + "session.review.noVcs.createGit.actionLoading": "Git-Repository wird erstellt...", + "session.review.noVcs.createGit.action": "Git-Repository erstellen", + "session.todo.progress": "{{done}} von {{total}} Aufgaben erledigt", + "session.question.progress": "{{current}} von {{total}} Fragen", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Datei-Explorer", + "session.header.open.fileManager": "Dateimanager", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Entwicklungs-Leistungsdiagnose", + "debugBar.na": "n.v.", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Letzter abgeschlossener Routenübergang, der eine Sitzungsseite berührt, gemessen vom Start des Routers bis zum ersten Rendern nach dem Einschwingen.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Gleitende Bilder pro Sekunde in den letzten 5 Sekunden.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Schlechteste Frame-Zeit in den letzten 5 Sekunden.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Frames über 32ms in den letzten 5 Sekunden.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blockierte Zeit und Anzahl langer Aufgaben in den letzten 5 Sekunden. Max Aufgabe: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Schlechteste beobachtete Eingabeverzögerung in den letzten 5 Sekunden.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Ungefähre Interaktionsdauer in den letzten 5 Sekunden. Dies ist INP-ähnlich, nicht das offizielle Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulative Layoutverschiebung für die aktuelle App-Lebensdauer.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Verwendeter JS-Heap vs Heap-Limit. Nur Chromium.", + "debugBar.mem.tip": "Verwendeter JS-Heap vs Heap-Limit. {{used}} von {{limit}}.", + "common.key.ctrl": "Strg", + "common.key.alt": "Alt", + "common.key.shift": "Umschalt", + "common.key.meta": "Meta", + "common.key.space": "Leertaste", + "common.key.backspace": "Rücktaste", + "common.key.enter": "Eingabe", + "common.key.tab": "Tab", + "common.key.delete": "Entf", + "common.key.home": "Pos1", + "common.key.end": "Ende", + "common.key.pageUp": "Bild auf", + "common.key.pageDown": "Bild ab", + "common.key.insert": "Einfg", + "common.unknown": "unbekannt", + "error.page.circular": "[Zirkulär]", + "error.globalSDK.noServerAvailable": "Kein Server verfügbar", + "error.globalSDK.serverNotAvailable": "Server nicht verfügbar", + "error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden", + "error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden", + "error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden", + "error.childStore.storeCreateFailed": "Speicher konnte nicht erstellt werden", + "terminal.connectionLost.abnormalClose": "WebSocket abnormal geschlossen: {{code}}", } satisfies Partial> diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index b950cab8d6..65e878b4e9 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -306,6 +306,10 @@ export const dict = { "dialog.directory.search.placeholder": "Search folders", "dialog.directory.empty": "No folders found", + "app.server.unreachable": "Could not reach {{server}}", + "app.server.retrying": "Retrying automatically...", + "app.server.otherServers": "Other servers", + "dialog.server.title": "Servers", "dialog.server.description": "Switch which OpenCode server this app connects to.", "dialog.server.search.placeholder": "Search servers", @@ -319,7 +323,9 @@ export const dict = { "dialog.server.add.name": "Server name (optional)", "dialog.server.add.namePlaceholder": "Localhost", "dialog.server.add.username": "Username (optional)", + "dialog.server.add.usernamePlaceholder": "username", "dialog.server.add.password": "Password (optional)", + "dialog.server.add.passwordPlaceholder": "password", "dialog.server.edit.title": "Edit server", "dialog.server.default.title": "Default server", "dialog.server.default.description": @@ -335,6 +341,7 @@ export const dict = { "dialog.server.menu.delete": "Delete", "dialog.server.current": "Current Server", "dialog.server.status.default": "Default", + "server.row.noUsername": "no username", "dialog.project.edit.title": "Edit project", "dialog.project.edit.name": "Name", @@ -456,6 +463,7 @@ export const dict = { "error.page.action.checking": "Checking...", "error.page.action.checkUpdates": "Check for updates", "error.page.action.updateTo": "Update to {{version}}", + "error.page.circular": "[Circular]", "error.page.report.prefix": "Please report this error to the OpenCode team", "error.page.report.discord": "on Discord", "error.page.version": "Version: {{version}}", @@ -464,6 +472,12 @@ export const dict = { "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", "error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?", + "error.globalSDK.noServerAvailable": "No server available", + "error.globalSDK.serverNotAvailable": "Server not available", + "error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache", + "error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata", + "error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon", + "error.childStore.storeCreateFailed": "Failed to create store", "directory.error.invalidUrl": "Invalid directory in URL.", "error.chain.unknown": "Unknown error", @@ -512,6 +526,10 @@ export const dict = { "session.review.loadingChanges": "Loading changes...", "session.review.empty": "No changes in this session yet", "session.review.noVcs": "No Git Version Control System detected, changes not displayed", + "session.review.noVcs.createGit.title": "Create a Git repository", + "session.review.noVcs.createGit.description": "Track, review, and undo changes in this project", + "session.review.noVcs.createGit.actionLoading": "Creating Git repository...", + "session.review.noVcs.createGit.action": "Create Git repository", "session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable", "session.review.noChanges": "No changes", @@ -530,6 +548,8 @@ export const dict = { "session.todo.title": "Todos", "session.todo.collapse": "Collapse", "session.todo.expand": "Expand", + "session.todo.progress": "{{done}} of {{total}} todos completed", + "session.question.progress": "{{current}} of {{total}} questions", "session.followupDock.summary.one": "{{count}} queued message", "session.followupDock.summary.other": "{{count}} queued messages", "session.followupDock.sendNow": "Send now", @@ -555,6 +575,22 @@ export const dict = { "session.header.open.ariaLabel": "Open in {{app}}", "session.header.open.menu": "Open options", "session.header.open.copyPath": "Copy path", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "File Manager", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", "status.popover.trigger": "Status", "status.popover.ariaLabel": "Server configurations", @@ -587,6 +623,7 @@ export const dict = { "terminal.title.numbered": "Terminal {{number}}", "terminal.close": "Close terminal", "terminal.connectionLost.title": "Connection Lost", + "terminal.connectionLost.abnormalClose": "WebSocket closed abnormally: {{code}}", "terminal.connectionLost.description": "The terminal connection was interrupted. This can happen when the server restarts.", @@ -604,6 +641,21 @@ export const dict = { "common.edit": "Edit", "common.loadMore": "Load more", "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "unknown", "common.time.justNow": "Just now", "common.time.minutesAgo.short": "{{count}}m ago", @@ -623,6 +675,30 @@ export const dict = { "sidebar.project.viewAllSessions": "View all sessions", "sidebar.project.clearNotifications": "Clear notifications", + "debugBar.ariaLabel": "Development performance diagnostics", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Last completed route transition touching a session page, measured from router start until the first paint after it settles.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Rolling frames per second over the last 5 seconds.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Worst frame time over the last 5 seconds.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Frames over 32ms in the last 5 seconds.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blocked time and long-task count in the last 5 seconds. Max task: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Worst observed input delay in the last 5 seconds.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Cumulative layout shift for the current app lifetime.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Used JS heap vs heap limit. Chromium only.", + "debugBar.mem.tip": "Used JS heap vs heap limit. {{used}} of {{limit}}.", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index a2633004cf..2fabd6d4c8 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -871,4 +871,79 @@ export const dict = { "common.time.daysAgo.short": "hace {{count}} d", "settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno", "settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por su URL base.", + + "app.server.unreachable": "No se pudo conectar con {{server}}", + "app.server.retrying": "Reintentando automáticamente...", + "app.server.otherServers": "Otros servidores", + "dialog.server.add.usernamePlaceholder": "usuario", + "dialog.server.add.passwordPlaceholder": "contraseña", + "server.row.noUsername": "sin usuario", + "session.review.noVcs.createGit.title": "Crear repositorio Git", + "session.review.noVcs.createGit.description": "Rastrea, revisa y deshaz cambios en este proyecto", + "session.review.noVcs.createGit.actionLoading": "Creando repositorio Git...", + "session.review.noVcs.createGit.action": "Crear repositorio Git", + "session.todo.progress": "{{done}} de {{total}} tareas completadas", + "session.question.progress": "{{current}} de {{total}} preguntas", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Explorador de archivos", + "session.header.open.fileManager": "Gestor de archivos", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Diagnóstico de rendimiento de desarrollo", + "debugBar.na": "n/d", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Última transición de ruta completada tocando una página de sesión, medida desde el inicio del router hasta el primer pintado después de asentarse.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Cuadros por segundo en los últimos 5 segundos.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Peor tiempo de cuadro en los últimos 5 segundos.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Cuadros superiores a 32ms en los últimos 5 segundos.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Tiempo bloqueado y recuento de tareas largas en los últimos 5 segundos. Tarea máx: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Peor retraso de entrada observado en los últimos 5 segundos.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Duración aproximada de la interacción en los últimos 5 segundos. Esto es similar a INP, no el INP oficial de Web Vitals.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Cambio de diseño acumulativo para la vida útil actual de la aplicación.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Heap JS usado vs límite de heap. Solo Chromium.", + "debugBar.mem.tip": "Heap JS usado vs límite de heap. {{used}} de {{limit}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Mayús", + "common.key.meta": "Meta", + "common.key.space": "Espacio", + "common.key.backspace": "Retroceso", + "common.key.enter": "Intro", + "common.key.tab": "Tab", + "common.key.delete": "Supr", + "common.key.home": "Inicio", + "common.key.end": "Fin", + "common.key.pageUp": "RePág", + "common.key.pageDown": "AvPág", + "common.key.insert": "Insert", + "common.unknown": "desconocido", + "error.page.circular": "[Circular]", + "error.globalSDK.noServerAvailable": "Ningún servidor disponible", + "error.globalSDK.serverNotAvailable": "Servidor no disponible", + "error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente", + "error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes", + "error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente", + "error.childStore.storeCreateFailed": "Error al crear almacén", + "terminal.connectionLost.abnormalClose": "WebSocket cerrado anormalmente: {{code}}", } diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index cbde19316d..dc30a0e537 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -796,4 +796,81 @@ export const dict = { "common.time.daysAgo.short": "il y a {{count}}j", "settings.providers.connected.environmentDescription": "Connecté à partir de vos variables d'environnement", "settings.providers.custom.description": "Ajouter un fournisseur compatible avec OpenAI via l'URL de base.", + + "app.server.unreachable": "Impossible de joindre {{server}}", + "app.server.retrying": "Nouvelle tentative automatique...", + "app.server.otherServers": "Autres serveurs", + "dialog.server.add.usernamePlaceholder": "nom d'utilisateur", + "dialog.server.add.passwordPlaceholder": "mot de passe", + "server.row.noUsername": "aucun nom d'utilisateur", + "session.review.noVcs.createGit.title": "Créer un dépôt Git", + "session.review.noVcs.createGit.description": "Suivre, examiner et annuler les modifications dans ce projet", + "session.review.noVcs.createGit.actionLoading": "Création du dépôt Git...", + "session.review.noVcs.createGit.action": "Créer un dépôt Git", + "session.todo.progress": "{{done}} tâches sur {{total}} terminées", + "session.question.progress": "{{current}} questions sur {{total}}", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Explorateur de fichiers", + "session.header.open.fileManager": "Gestionnaire de fichiers", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Diagnostics de performance de développement", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Dernière transition de route terminée touchant une page de session, mesurée du début du routeur jusqu'au premier affichage après stabilisation.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Images par seconde glissantes sur les 5 dernières secondes.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Pire temps d'image sur les 5 dernières secondes.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Images de plus de 32ms au cours des 5 dernières secondes.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "Temps bloqué et nombre de tâches longues au cours des 5 dernières secondes. Tâche max : {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Pire délai d'entrée observé au cours des 5 dernières secondes.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Durée approximative d'interaction au cours des 5 dernières secondes. Ceci est similaire à INP, pas le INP officiel des Web Vitals.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Décalage cumulatif de la mise en page pour la durée de vie actuelle de l'application.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Tas JS utilisé vs limite de tas. Chromium uniquement.", + "debugBar.mem.tip": "Tas JS utilisé vs limite de tas. {{used}} sur {{limit}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Maj", + "common.key.meta": "Méta", + "common.key.space": "Espace", + "common.key.backspace": "Retour arrière", + "common.key.enter": "Entrée", + "common.key.tab": "Tab", + "common.key.delete": "Suppr", + "common.key.home": "Début", + "common.key.end": "Fin", + "common.key.pageUp": "Page précédente", + "common.key.pageDown": "Page suivante", + "common.key.insert": "Inser", + "common.unknown": "inconnu", + "error.page.circular": "[Circulaire]", + "error.globalSDK.noServerAvailable": "Aucun serveur disponible", + "error.globalSDK.serverNotAvailable": "Serveur non disponible", + "error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant", + "error.childStore.persistedProjectMetadataCreateFailed": + "Échec de la création des métadonnées de projet persistantes", + "error.childStore.persistedProjectIconCreateFailed": "Échec de la création de l'icône de projet persistante", + "error.childStore.storeCreateFailed": "Échec de la création du stockage", + "terminal.connectionLost.abnormalClose": "WebSocket fermé anormalement : {{code}}", } diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 914ac5cd79..1f5615c9bd 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -783,4 +783,78 @@ export const dict = { "common.time.daysAgo.short": "{{count}} 日前", "settings.providers.connected.environmentDescription": "環境変数から接続されました", "settings.providers.custom.description": "ベース URL を指定して OpenAI 互換のプロバイダーを追加します。", + + "app.server.unreachable": "{{server}} に到達できませんでした", + "app.server.retrying": "自動的に再試行中...", + "app.server.otherServers": "その他のサーバー", + "dialog.server.add.usernamePlaceholder": "ユーザー名", + "dialog.server.add.passwordPlaceholder": "パスワード", + "server.row.noUsername": "ユーザー名なし", + "session.review.noVcs.createGit.title": "Git リポジトリを作成", + "session.review.noVcs.createGit.description": "このプロジェクトの変更を追跡、レビュー、元に戻す", + "session.review.noVcs.createGit.actionLoading": "Git リポジトリを作成中...", + "session.review.noVcs.createGit.action": "Git リポジトリを作成", + "session.todo.progress": "{{done}} 個中 {{total}} 個の Todo が完了", + "session.question.progress": "{{total}} 問中 {{current}} 問", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "エクスプローラー", + "session.header.open.fileManager": "ファイルマネージャー", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "ターミナル", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "開発パフォーマンス診断", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": "セッションページに触れる最後に完了したルート遷移。ルーター開始から安定後の最初の描画まで測定。", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "過去5秒間のローリングフレーム/秒。", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "過去5秒間の最悪フレーム時間。", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "過去5秒間で32msを超えたフレーム。", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "過去5秒間のブロック時間と長時間タスク数。最大タスク: {{max}}。", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "過去5秒間で観測された最悪の入力遅延。", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "過去5秒間の概算インタラクション時間。これは INP に似ていますが、公式の Web Vitals INP ではありません。", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "現在のアプリ寿命の累積レイアウトシフト。", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "使用中の JS ヒープ対ヒープ制限。Chromium のみ。", + "debugBar.mem.tip": "使用中の JS ヒープ対ヒープ制限。{{limit}} 中 {{used}}。", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "不明", + "error.page.circular": "[循環]", + "error.globalSDK.noServerAvailable": "利用可能なサーバーがありません", + "error.globalSDK.serverNotAvailable": "サーバーが利用できません", + "error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました", + "error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました", + "error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました", + "error.childStore.storeCreateFailed": "ストアの作成に失敗しました", + "terminal.connectionLost.abnormalClose": "WebSocket が異常終了しました: {{code}}", } diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index f0a3f3ae6b..a2f5e5c7c8 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -782,4 +782,78 @@ export const dict = { "common.time.daysAgo.short": "{{count}}일 전", "settings.providers.connected.environmentDescription": "환경 변수에서 연결됨", "settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.", + + "app.server.unreachable": "{{server}}에 연결할 수 없습니다", + "app.server.retrying": "자동으로 재시도 중...", + "app.server.otherServers": "다른 서버", + "dialog.server.add.usernamePlaceholder": "사용자 이름", + "dialog.server.add.passwordPlaceholder": "비밀번호", + "server.row.noUsername": "사용자 이름 없음", + "session.review.noVcs.createGit.title": "Git 저장소 생성", + "session.review.noVcs.createGit.description": "이 프로젝트의 변경 사항을 추적, 검토 및 실행 취소", + "session.review.noVcs.createGit.actionLoading": "Git 저장소 생성 중...", + "session.review.noVcs.createGit.action": "Git 저장소 생성", + "session.todo.progress": "{{total}}개의 할 일 중 {{done}}개 완료", + "session.question.progress": "{{total}}개의 질문 중 {{current}}개", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "파일 탐색기", + "session.header.open.fileManager": "파일 관리자", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "터미널", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "개발 성능 진단", + "debugBar.na": "해당 없음", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "세션 페이지에 닿은 마지막 완료된 라우트 전환. 라우터 시작부터 정착 후 첫 번째 페인트까지 측정됨.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "지난 5초간의 초당 프레임 수.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "지난 5초간의 최악의 프레임 시간.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "지난 5초간 32ms를 초과한 프레임.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "지난 5초간의 차단된 시간 및 긴 작업 수. 최대 작업: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "지난 5초간 관찰된 최악의 입력 지연.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "지난 5초간의 대략적인 상호작용 지속 시간. 이것은 공식 Web Vitals INP가 아닌 INP와 유사합니다.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "현재 앱 수명 동안의 누적 레이아웃 이동.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "사용된 JS 힙 대 힙 제한. Chromium 전용.", + "debugBar.mem.tip": "사용된 JS 힙 대 힙 제한. {{limit}} 중 {{used}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "알 수 없음", + "error.page.circular": "[순환]", + "error.globalSDK.noServerAvailable": "사용 가능한 서버 없음", + "error.globalSDK.serverNotAvailable": "서버를 사용할 수 없음", + "error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패", + "error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패", + "error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패", + "error.childStore.storeCreateFailed": "저장소 생성 실패", + "terminal.connectionLost.abnormalClose": "WebSocket이 비정상적으로 닫힘: {{code}}", } diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 17ba96058b..ed75e556ea 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -865,4 +865,79 @@ export const dict = { "common.time.daysAgo.short": "{{count}} d siden", "settings.providers.connected.environmentDescription": "Koblet til fra miljøvariablene dine", "settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.", + + "app.server.unreachable": "Kunne ikke nå {{server}}", + "app.server.retrying": "Prøver på nytt automatisk...", + "app.server.otherServers": "Andre servere", + "dialog.server.add.usernamePlaceholder": "brukernavn", + "dialog.server.add.passwordPlaceholder": "passord", + "server.row.noUsername": "inget brukernavn", + "session.review.noVcs.createGit.title": "Opprett et Git-depot", + "session.review.noVcs.createGit.description": "Spor, gjennomgå og angre endringer i dette prosjektet", + "session.review.noVcs.createGit.actionLoading": "Oppretter Git-depot...", + "session.review.noVcs.createGit.action": "Opprett Git-depot", + "session.todo.progress": "{{done}} av {{total}} oppgaver fullført", + "session.question.progress": "{{current}} av {{total}} spørsmål", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Filutforsker", + "session.header.open.fileManager": "Filbehandler", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Utviklingsytelsesdiagnostikk", + "debugBar.na": "i/t", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Siste fullførte ruteovergang som berører en sesjonsside, målt fra ruterstart til første opptegning etter at den har roet seg.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Rullende bilder per sekund over de siste 5 sekundene.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Verste bildetid over de siste 5 sekundene.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Bilder over 32ms i de siste 5 sekundene.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blokkert tid og antall lange oppgaver i de siste 5 sekundene. Maks oppgave: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Verste observerte inndataforsinkelse i de siste 5 sekundene.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Omtrentlig interaksjonsvarighet over de siste 5 sekundene. Dette er INP-lignende, ikke den offisielle Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulativ layoutforskyvning for gjeldende app-levetid.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Brukt JS-heap vs heap-grense. Kun Chromium.", + "debugBar.mem.tip": "Brukt JS-heap vs heap-grense. {{used}} av {{limit}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Mellomrom", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "ukjent", + "error.page.circular": "[Sirkulær]", + "error.globalSDK.noServerAvailable": "Ingen server tilgjengelig", + "error.globalSDK.serverNotAvailable": "Server ikke tilgjengelig", + "error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer", + "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata", + "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon", + "error.childStore.storeCreateFailed": "Kunne ikke opprette lager", + "terminal.connectionLost.abnormalClose": "WebSocket lukket unormalt: {{code}}", } satisfies Partial> diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 38111c8738..2507acd9d2 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -785,4 +785,80 @@ export const dict = { "common.time.daysAgo.short": "{{count}} dni temu", "settings.providers.connected.environmentDescription": "Połączono ze zmiennymi środowiskowymi", "settings.providers.custom.description": "Dodaj dostawcę zgodnego z OpenAI poprzez podstawowy URL.", + + "app.server.unreachable": "Nie można połączyć z {{server}}", + "app.server.retrying": "Ponawianie automatycznie...", + "app.server.otherServers": "Inne serwery", + "dialog.server.add.usernamePlaceholder": "nazwa użytkownika", + "dialog.server.add.passwordPlaceholder": "hasło", + "server.row.noUsername": "brak nazwy użytkownika", + "session.review.noVcs.createGit.title": "Utwórz repozytorium Git", + "session.review.noVcs.createGit.description": "Śledź, przeglądaj i cofaj zmiany w tym projekcie", + "session.review.noVcs.createGit.actionLoading": "Tworzenie repozytorium Git...", + "session.review.noVcs.createGit.action": "Utwórz repozytorium Git", + "session.todo.progress": "Ukończono {{done}} z {{total}} zadań", + "session.question.progress": "{{current}} z {{total}} pytań", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Eksplorator plików", + "session.header.open.fileManager": "Menedżer plików", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Diagnostyka wydajności deweloperskiej", + "debugBar.na": "n.d.", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Ostatnie zakończone przejście trasy dotykające strony sesji, mierzone od startu routera do pierwszego odrysowania po ustaleniu.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Średnia liczba klatek na sekundę w ciągu ostatnich 5 sekund.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Najgorszy czas klatki w ciągu ostatnich 5 sekund.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Klatki powyżej 32ms w ciągu ostatnich 5 sekund.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "Zablokowany czas i liczba długich zadań w ciągu ostatnich 5 sekund. Maksymalne zadanie: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Najgorsze zaobserwowane opóźnienie wejścia w ciągu ostatnich 5 sekund.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Przybliżony czas trwania interakcji w ciągu ostatnich 5 sekund. Jest to podobne do INP, a nie oficjalne Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Skumulowane przesunięcie układu dla bieżącego czasu życia aplikacji.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Użyta sterta JS vs limit sterty. Tylko Chromium.", + "debugBar.mem.tip": "Użyta sterta JS vs limit sterty. {{used}} z {{limit}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Spacja", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "nieznany", + "error.page.circular": "[Cykliczne]", + "error.globalSDK.noServerAvailable": "Brak dostępnego serwera", + "error.globalSDK.serverNotAvailable": "Serwer niedostępny", + "error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej", + "error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu", + "error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu", + "error.childStore.storeCreateFailed": "Nie udało się utworzyć magazynu", + "terminal.connectionLost.abnormalClose": "WebSocket zamknięty nieprawidłowo: {{code}}", } diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 0b63e84227..6145b3011b 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -867,4 +867,79 @@ export const dict = { "common.time.daysAgo.short": "{{count}} д назад", "settings.providers.connected.environmentDescription": "Подключено из ваших переменных окружения", "settings.providers.custom.description": "Добавить провайдера, совместимого с OpenAI, по базовому URL.", + + "app.server.unreachable": "Не удалось связаться с {{server}}", + "app.server.retrying": "Автоматическая повторная попытка...", + "app.server.otherServers": "Другие серверы", + "dialog.server.add.usernamePlaceholder": "имя пользователя", + "dialog.server.add.passwordPlaceholder": "пароль", + "server.row.noUsername": "нет имени пользователя", + "session.review.noVcs.createGit.title": "Создать репозиторий Git", + "session.review.noVcs.createGit.description": "Отслеживайте, просматривайте и отменяйте изменения в этом проекте", + "session.review.noVcs.createGit.actionLoading": "Создание репозитория Git...", + "session.review.noVcs.createGit.action": "Создать репозиторий Git", + "session.todo.progress": "Выполнено {{done}} из {{total}} задач", + "session.question.progress": "{{current}} из {{total}} вопросов", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Проводник", + "session.header.open.fileManager": "Файловый менеджер", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Терминал", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Диагностика производительности разработки", + "debugBar.na": "н/д", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Последний завершенный переход маршрута, затрагивающий страницу сеанса, измеренный от запуска маршрутизатора до первой отрисовки после стабилизации.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Скользящая частота кадров в секунду за последние 5 секунд.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Худшее время кадра за последние 5 секунд.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Кадры более 32 мс за последние 5 секунд.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Заблокированное время и количество длинных задач за последние 5 секунд. Макс. задача: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Худшая наблюдаемая задержка ввода за последние 5 секунд.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Приблизительная продолжительность взаимодействия за последние 5 секунд. Это похоже на INP, а не официальный Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Кумулятивный сдвиг макета за текущее время жизни приложения.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Используемая куча JS по сравнению с лимитом кучи. Только Chromium.", + "debugBar.mem.tip": "Используемая куча JS по сравнению с лимитом кучи. {{used}} из {{limit}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Пробел", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "неизвестно", + "error.page.circular": "[Циклично]", + "error.globalSDK.noServerAvailable": "Нет доступного сервера", + "error.globalSDK.serverNotAvailable": "Сервер недоступен", + "error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш", + "error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта", + "error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта", + "error.childStore.storeCreateFailed": "Не удалось создать хранилище", + "terminal.connectionLost.abnormalClose": "WebSocket закрыт аварийно: {{code}}", } diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 94bb2c2c31..9cc3c5be1d 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -854,4 +854,79 @@ export const dict = { "common.time.daysAgo.short": "{{count}} วันที่แล้ว", "settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ", "settings.providers.custom.description": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย URL หลัก", + + "app.server.unreachable": "ไม่สามารถติดต่อ {{server}}", + "app.server.retrying": "กำลังลองใหม่โดยอัตโนมัติ...", + "app.server.otherServers": "เซิร์ฟเวอร์อื่น ๆ", + "dialog.server.add.usernamePlaceholder": "ชื่อผู้ใช้", + "dialog.server.add.passwordPlaceholder": "รหัสผ่าน", + "server.row.noUsername": "ไม่มีชื่อผู้ใช้", + "session.review.noVcs.createGit.title": "สร้าง Git repository", + "session.review.noVcs.createGit.description": "ติดตาม ตรวจสอบ และเลิกทำสิ่งเปลี่ยนแปลงในโปรเจกต์นี้", + "session.review.noVcs.createGit.actionLoading": "กำลังสร้าง Git repository...", + "session.review.noVcs.createGit.action": "สร้าง Git repository", + "session.todo.progress": "เสร็จสิ้น {{done}} จาก {{total}} รายการ", + "session.question.progress": "{{current}} จาก {{total}} คำถาม", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "File Manager", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "การวินิจฉัยประสิทธิภาพการพัฒนา", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "การเปลี่ยนเส้นทางที่เสร็จสมบูรณ์ล่าสุดที่สัมผัสหน้าเซสชัน วัดจากจุดเริ่มต้นเราเตอร์จนถึงการวาดครั้งแรกหลังจากที่นิ่ง", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "เฟรมต่อวินาทีแบบต่อเนื่องในช่วง 5 วินาทีที่ผ่านมา", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "เวลาเฟรมที่แย่ที่สุดในช่วง 5 วินาทีที่ผ่านมา", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "เฟรมที่เกิน 32ms ในช่วง 5 วินาทีที่ผ่านมา", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "เวลาที่ถูกบล็อกและจำนวนงานยาวในช่วง 5 วินาทีที่ผ่านมา งานสูงสุด: {{max}}", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "ความล่าช้าในการป้อนข้อมูลที่แย่ที่สุดที่สังเกตได้ในช่วง 5 วินาทีที่ผ่านมา", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "ระยะเวลาการโต้ตอบโดยประมาณในช่วง 5 วินาทีที่ผ่านมา นี่เป็นเหมือน INP ไม่ใช่ Web Vitals INP อย่างเป็นทางการ", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "การเลื่อนเลย์เอาต์สะสมสำหรับอายุการใช้งานของแอปปัจจุบัน", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "JS heap ที่ใช้เทียบกับขีดจำกัด heap เฉพาะ Chromium", + "debugBar.mem.tip": "JS heap ที่ใช้เทียบกับขีดจำกัด heap {{used}} จาก {{limit}}", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "ไม่ทราบ", + "error.page.circular": "[วงกลม]", + "error.globalSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์", + "error.globalSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน", + "error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร", + "error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร", + "error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร", + "error.childStore.storeCreateFailed": "ไม่สามารถสร้างที่เก็บ", + "terminal.connectionLost.abnormalClose": "WebSocket ปิดอย่างผิดปกติ: {{code}}", } diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 66835c1c57..373f26ad6f 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -874,4 +874,78 @@ export const dict = { "common.time.daysAgo.short": "{{count}}g önce", "settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı", "settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.", + + "app.server.unreachable": "{{server}} sunucusuna ulaşılamadı", + "app.server.retrying": "Otomatik olarak tekrar deneniyor...", + "app.server.otherServers": "Diğer sunucular", + "dialog.server.add.usernamePlaceholder": "kullanıcı adı", + "dialog.server.add.passwordPlaceholder": "parola", + "server.row.noUsername": "kullanıcı adı yok", + "session.review.noVcs.createGit.title": "Git deposu oluştur", + "session.review.noVcs.createGit.description": "Bu projedeki değişiklikleri takip et, incele ve geri al", + "session.review.noVcs.createGit.actionLoading": "Git deposu oluşturuluyor...", + "session.review.noVcs.createGit.action": "Git deposu oluştur", + "session.todo.progress": "{{total}} görevin {{done}} tanesi tamamlandı", + "session.question.progress": "{{total}} sorunun {{current}} tanesi", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Dosya Gezgini", + "session.header.open.fileManager": "Dosya Yöneticisi", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Geliştirme performansı teşhisi", + "debugBar.na": "yok", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Yönlendirici başlangıcından yerleşme sonrası ilk boyamaya kadar ölçülen, bir oturum sayfasına dokunan son tamamlanmış rota geçişi.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Son 5 saniyedeki kayan saniye başına kare sayısı.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Son 5 saniyedeki en kötü kare süresi.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Son 5 saniyede 32ms üzerindeki kareler.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Son 5 saniyedeki engellenen süre ve uzun görev sayısı. Maksimum görev: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Son 5 saniyede gözlemlenen en kötü giriş gecikmesi.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "Son 5 saniyedeki yaklaşık etkileşim süresi. Bu INP benzeridir, resmi Web Vitals INP değildir.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Mevcut uygulama ömrü için kümülatif düzen kayması.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Kullanılan JS yığını vs yığın sınırı. Yalnızca Chromium.", + "debugBar.mem.tip": "Kullanılan JS yığını vs yığın sınırı. {{limit}} içinde {{used}}.", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Boşluk", + "common.key.backspace": "Geri", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "bilinmiyor", + "error.page.circular": "[Döngüsel]", + "error.globalSDK.noServerAvailable": "Sunucu yok", + "error.globalSDK.serverNotAvailable": "Sunucu mevcut değil", + "error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı", + "error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı", + "error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı", + "error.childStore.storeCreateFailed": "Depo oluşturulamadı", + "terminal.connectionLost.abnormalClose": "WebSocket anormal şekilde kapandı: {{code}}", } satisfies Partial> diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index d6b179c5c5..819e1cd87d 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -853,4 +853,77 @@ export const dict = { "common.time.daysAgo.short": "{{count}}天前", "settings.providers.connected.environmentDescription": "已通过环境变量连接", "settings.providers.custom.description": "通过基础 URL 添加与 OpenAI 兼容的提供商。", + + "app.server.unreachable": "无法连接到 {{server}}", + "app.server.retrying": "正在自动重试...", + "app.server.otherServers": "其他服务器", + "dialog.server.add.usernamePlaceholder": "用户名", + "dialog.server.add.passwordPlaceholder": "密码", + "server.row.noUsername": "无用户名", + "session.review.noVcs.createGit.title": "创建 Git 仓库", + "session.review.noVcs.createGit.description": "在此项目中跟踪、审查和撤消更改", + "session.review.noVcs.createGit.actionLoading": "正在创建 Git 仓库...", + "session.review.noVcs.createGit.action": "创建 Git 仓库", + "session.todo.progress": "已完成 {{done}} 个任务(共 {{total}} 个)", + "session.question.progress": "{{current}}/{{total}} 个问题", + "session.header.open.finder": "访达", + "session.header.open.fileExplorer": "文件资源管理器", + "session.header.open.fileManager": "文件管理器", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "终端", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "开发性能诊断", + "debugBar.na": "不适用", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": "最后一次完成的涉及会话页面的路由转换,从路由器启动到稳定后的第一次绘制。", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "过去 5 秒内的滚动帧率。", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "过去 5 秒内最差的帧时间。", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "过去 5 秒内超过 32ms 的帧。", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "过去 5 秒内的阻塞时间和长任务计数。最大任务:{{max}}。", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "过去 5 秒内观察到的最差输入延迟。", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "过去 5 秒内的近似交互持续时间。这类似于 INP,而非官方的 Web Vitals INP。", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "当前应用生命周期的累积布局偏移。", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "使用的 JS 堆与堆限制。仅限 Chromium。", + "debugBar.mem.tip": "使用的 JS 堆与堆限制。{{used}} / {{limit}}。", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "空格", + "common.key.backspace": "退格", + "common.key.enter": "回车", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "未知", + "error.page.circular": "[循环]", + "error.globalSDK.noServerAvailable": "无可用服务器", + "error.globalSDK.serverNotAvailable": "服务器不可用", + "error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败", + "error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败", + "error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败", + "error.childStore.storeCreateFailed": "创建存储失败", + "terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}", } satisfies Partial> diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 3796350d2c..8c80cd3235 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -848,4 +848,77 @@ export const dict = { "common.time.daysAgo.short": "{{count}}天前", "settings.providers.connected.environmentDescription": "已從環境變數連線", "settings.providers.custom.description": "透過基本 URL 新增與 OpenAI 相容的提供者。", + + "app.server.unreachable": "無法連線至 {{server}}", + "app.server.retrying": "正在自動重試...", + "app.server.otherServers": "其他伺服器", + "dialog.server.add.usernamePlaceholder": "使用者名稱", + "dialog.server.add.passwordPlaceholder": "密碼", + "server.row.noUsername": "無使用者名稱", + "session.review.noVcs.createGit.title": "建立 Git 儲存庫", + "session.review.noVcs.createGit.description": "追蹤、檢閱及復原此專案中的變更", + "session.review.noVcs.createGit.actionLoading": "正在建立 Git 儲存庫...", + "session.review.noVcs.createGit.action": "建立 Git 儲存庫", + "session.todo.progress": "已完成 {{done}} 個待辦事項(共 {{total}} 個)", + "session.question.progress": "{{current}}/{{total}} 個問題", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "檔案總管", + "session.header.open.fileManager": "檔案管理員", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "終端機", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "開發效能診斷", + "debugBar.na": "不適用", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": "最後一次完成的涉及工作階段頁面的路由轉換,從路由器啟動到穩定後的第一次繪製。", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "過去 5 秒內的滾動幀率。", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "過去 5 秒內最差的幀時間。", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "過去 5 秒內超過 32ms 的幀。", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "過去 5 秒內的阻塞時間和長任務計數。最大任務:{{max}}。", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "過去 5 秒內觀察到的最差輸入延遲。", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "過去 5 秒內的近似互動持續時間。這類似於 INP,而非官方的 Web Vitals INP。", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "目前應用程式生命週期的累積版面配置位移。", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "使用的 JS 堆積與堆積限制。僅限 Chromium。", + "debugBar.mem.tip": "使用的 JS 堆積與堆積限制。{{used}} / {{limit}}。", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "空白鍵", + "common.key.backspace": "退格鍵", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "未知", + "error.page.circular": "[循環]", + "error.globalSDK.noServerAvailable": "無可用的伺服器", + "error.globalSDK.serverNotAvailable": "伺服器無法使用", + "error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗", + "error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗", + "error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗", + "error.childStore.storeCreateFailed": "建立儲存區失敗", + "terminal.connectionLost.abnormalClose": "WebSocket 異常關閉:{{code}}", } satisfies Partial> diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx index a30d86d180..11284b3d2d 100644 --- a/packages/app/src/pages/error.tsx +++ b/packages/app/src/pages/error.tsx @@ -35,14 +35,14 @@ function isInitError(error: unknown): error is InitError { ) } -function safeJson(value: unknown): string { +function safeJson(value: unknown, circular: string): string { const seen = new WeakSet() const json = JSON.stringify( value, (_key, val) => { if (typeof val === "bigint") return val.toString() if (typeof val === "object" && val) { - if (seen.has(val)) return "[Circular]" + if (seen.has(val)) return circular seen.add(val) } return val @@ -54,14 +54,15 @@ function safeJson(value: unknown): string { function formatInitError(error: InitError, t: Translator): string { const data = error.data + const json = (value: unknown) => safeJson(value, t("error.page.circular")) switch (error.name) { case "MCPFailed": { const name = typeof data.name === "string" ? data.name : "" return t("error.chain.mcpFailed", { name }) } case "ProviderAuthError": { - const providerID = typeof data.providerID === "string" ? data.providerID : "unknown" - const message = typeof data.message === "string" ? data.message : safeJson(data.message) + const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown") + const message = typeof data.message === "string" ? data.message : json(data.message) return t("error.chain.providerAuthFailed", { provider: providerID, message }) } case "APIError": { @@ -101,24 +102,24 @@ function formatInitError(error: InitError, t: Translator): string { ].join("\n") } case "ProviderInitError": { - const providerID = typeof data.providerID === "string" ? data.providerID : "unknown" + const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown") return t("error.chain.providerInitFailed", { provider: providerID }) } case "ConfigJsonError": { - const path = typeof data.path === "string" ? data.path : safeJson(data.path) + const path = typeof data.path === "string" ? data.path : json(data.path) const message = typeof data.message === "string" ? data.message : "" if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message }) return t("error.chain.configJsonInvalid", { path }) } case "ConfigDirectoryTypoError": { - const path = typeof data.path === "string" ? data.path : safeJson(data.path) - const dir = typeof data.dir === "string" ? data.dir : safeJson(data.dir) - const suggestion = typeof data.suggestion === "string" ? data.suggestion : safeJson(data.suggestion) + const path = typeof data.path === "string" ? data.path : json(data.path) + const dir = typeof data.dir === "string" ? data.dir : json(data.dir) + const suggestion = typeof data.suggestion === "string" ? data.suggestion : json(data.suggestion) return t("error.chain.configDirectoryTypo", { dir, path, suggestion }) } case "ConfigFrontmatterError": { - const path = typeof data.path === "string" ? data.path : safeJson(data.path) - const message = typeof data.message === "string" ? data.message : safeJson(data.message) + const path = typeof data.path === "string" ? data.path : json(data.path) + const message = typeof data.message === "string" ? data.message : json(data.message) return t("error.chain.configFrontmatterError", { path, message }) } case "ConfigInvalidError": { @@ -126,7 +127,7 @@ function formatInitError(error: InitError, t: Translator): string { ? data.issues.filter(isIssue).map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) : [] const message = typeof data.message === "string" ? data.message : "" - const path = typeof data.path === "string" ? data.path : safeJson(data.path) + const path = typeof data.path === "string" ? data.path : json(data.path) const line = message ? t("error.chain.configInvalidWithMessage", { path, message }) @@ -135,14 +136,15 @@ function formatInitError(error: InitError, t: Translator): string { return [line, ...issues].join("\n") } case "UnknownError": - return typeof data.message === "string" ? data.message : safeJson(data) + return typeof data.message === "string" ? data.message : json(data) default: if (typeof data.message === "string") return data.message - return safeJson(data) + return json(data) } } function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string { + const json = (value: unknown) => safeJson(value, t("error.page.circular")) if (!error) return t("error.chain.unknown") if (isInitError(error)) { @@ -204,7 +206,7 @@ function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessag } const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" - return indent + safeJson(error) + return indent + json(error) } function formatError(error: unknown, t: Translator): string { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index c1088622a2..bc04f9ecf2 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -2159,7 +2159,7 @@ export default function Layout(props: ParentProps) { {language.t("command.provider.connect")} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 57ef1853d1..d917ce4c7a 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -956,13 +956,15 @@ export default function Page() { return (
-
Create a Git repository
+
{language.t("session.review.noVcs.createGit.title")}
- Track, review, and undo changes in this project + {language.t("session.review.noVcs.createGit.description")}
) diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index 84f77ea4a3..a5263cd743 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -196,7 +196,6 @@ export function SessionComposerRegion(props: { { const n = Math.min(store.tab + 1, total()) - return `${n} of ${total()} questions` + return language.t("session.question.progress", { current: n, total: total() }) }) const last = createMemo(() => store.tab >= total() - 1) diff --git a/packages/app/src/pages/session/composer/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx index 5500de97a4..2cd660b39f 100644 --- a/packages/app/src/pages/session/composer/session-todo-dock.tsx +++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx @@ -9,6 +9,10 @@ import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough" import { Index, createEffect, createMemo, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { composerEnabled, composerProbe } from "@/testing/session-composer" +import { useLanguage } from "@/context/language" + +const doneToken = "\u0000done\u0000" +const totalToken = "\u0000total\u0000" function dot(status: Todo["status"]) { if (status !== "in_progress") return undefined @@ -38,11 +42,11 @@ function dot(status: Todo["status"]) { export function SessionTodoDock(props: { sessionID?: string todos: Todo[] - title: string collapseLabel: string expandLabel: string dockProgress: number }) { + const language = useLanguage() const [store, setStore] = createStore({ collapsed: false, height: 320, @@ -52,7 +56,12 @@ export function SessionTodoDock(props: { const total = createMemo(() => props.todos.length) const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length) - const label = createMemo(() => `${done()} of ${total()} ${props.title.toLowerCase()} completed`) + const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() })) + const progress = createMemo(() => + language + .t("session.todo.progress", { done: doneToken, total: totalToken }) + .split(/(\u0000done\u0000|\u0000total\u0000)/), + ) const active = createMemo( () => @@ -137,10 +146,17 @@ export function SessionTodoDock(props: { opacity: `${Math.max(0, Math.min(1, 1 - shut()))}`, }} > - - of - -  {props.title.toLowerCase()} completed + + {(item) => + item() === doneToken ? ( + + ) : item() === totalToken ? ( + + ) : ( + {item()} + ) + } +
{ const title = input.title ?? "" const number = input.titleNumber ?? 0 - const match = title.match(/^Terminal (\d+)$/) - const parsed = match ? Number(match[1]) : undefined - const isDefaultTitle = Number.isFinite(number) && number > 0 && Number.isFinite(parsed) && parsed === number + const isDefaultTitle = Number.isFinite(number) && number > 0 && isDefaultTerminalTitle(title, number) if (title && !isDefaultTitle) return title if (number > 0) return input.t("terminal.title.numbered", { number }) diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index 3f009f4e0f..0b2c1e1ce4 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -1,5 +1,6 @@ import { createEffect, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js" import { animate, type AnimationPlaybackControls } from "motion" +import { useI18n } from "../context/i18n" import { createStore } from "solid-js/store" import { Collapsible } from "./collapsible" import type { IconProps } from "./icon" @@ -233,12 +234,14 @@ export function GenericTool(props: { hideDetails?: boolean input?: Record }) { + const i18n = useI18n() + return ( void onNext: () => void }) { + const i18n = useI18n() + return (
props.onInput(e.currentTarget.value)} @@ -40,7 +43,7 @@ export function FileSearchBar(props: { type="button" class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none" disabled={props.count() === 0} - aria-label="Previous match" + aria-label={i18n.t("ui.fileSearch.previousMatch")} onClick={props.onPrev} > @@ -49,7 +52,7 @@ export function FileSearchBar(props: { type="button" class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none" disabled={props.count() === 0} - aria-label="Next match" + aria-label={i18n.t("ui.fileSearch.nextMatch")} onClick={props.onNext} > @@ -58,7 +61,7 @@ export function FileSearchBar(props: { diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 17572923eb..04d898134c 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -9,7 +9,8 @@ import { Tooltip } from "@opencode-ai/ui/tooltip" import { base64Encode } from "@opencode-ai/util/encode" import { getFilename } from "@opencode-ai/util/path" import { A, useNavigate, useParams } from "@solidjs/router" -import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js" +import { type Accessor, createEffect, createMemo, For, type JSX, on, onCleanup, Show } from "solid-js" +import { createStore } from "solid-js/store" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout" @@ -101,46 +102,94 @@ const SessionRow = (props: { warmPress: () => void warmFocus: () => void cancelHoverPrefetch: () => void -}): JSX.Element => ( - { - props.setHoverSession(undefined) - if (props.sidebarOpened()) return - props.clearHoverProjectSoon() - }} - > -
-
- }> - - - - -
- - -
- - 0}> -
- - +}): JSX.Element => { + const [slot, setSlot] = createStore({ + open: false, + show: false, + fade: false, + }) + + let f: number | undefined + const clear = () => { + if (f !== undefined) window.clearTimeout(f) + f = undefined + } + + onCleanup(clear) + createEffect( + on( + () => props.isWorking(), + (on, prev) => { + clear() + if (on) { + setSlot({ open: true, show: true, fade: false }) + return + } + if (prev) { + setSlot({ open: false, show: true, fade: true }) + f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260) + return + } + setSlot({ open: false, show: false, fade: false }) + }, + { defer: true }, + ), + ) + + return ( + { + props.setHoverSession(undefined) + if (props.sidebarOpened()) return + props.clearHoverProjectSoon() + }} + > + 0)}> +
0, + }} + aria-hidden="true" + /> + + +
+ + + + {props.session.title} +
- - {props.session.title} - -
-
-) + + ) +} const SessionHoverPreview = (props: { mobile?: boolean @@ -204,8 +253,18 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { }) const isWorking = createMemo(() => { if (hasPermissions()) return false + const pending = (sessionStore.message[props.session.id] ?? []).findLast( + (message) => + message.role === "assistant" && + typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number", + ) const status = sessionStore.session_status[props.session.id] - return status?.type === "busy" || status?.type === "retry" + return ( + pending !== undefined || + status?.type === "busy" || + status?.type === "retry" || + (status !== undefined && status.type !== "idle") + ) }) const tint = createMemo(() => { @@ -300,7 +359,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { return (
Promise language: ReturnType }): JSX.Element => ( -