From 268855dc5a770e9cd6718ea62b935afdea2dda7f Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 13 Mar 2026 09:54:34 +1000 Subject: [PATCH 01/48] fix(ci): keep test runs on dev (#17260) --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d9eded3f175..c928e822346 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,9 @@ on: workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # Keep every run on dev so cancelled checks do not pollute the default branch + # commit history. PRs and other branches still share a group and cancel stale runs. + group: ${{ case(github.ref == 'refs/heads/dev', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }} cancel-in-progress: true permissions: From c173988aaa2f84ecdfe7d3c7cdfd7d3a525aee02 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:07:19 -0500 Subject: [PATCH 02/48] feat(app): interruption state --- packages/ui/src/components/message-part.css | 8 ----- packages/ui/src/components/message-part.tsx | 34 ++++++++------------- packages/ui/src/components/session-turn.tsx | 13 +++++--- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 3648d5079f1..f01408a38e0 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -23,10 +23,6 @@ max-width: 100%; gap: 0; - &[data-interrupted] { - color: var(--text-weak); - } - [data-slot="user-message-attachments"] { display: flex; flex-wrap: wrap; @@ -165,10 +161,6 @@ text-align: right; } - [data-slot="user-message-copy-wrapper"][data-interrupted] { - gap: 12px; - } - &:hover [data-slot="user-message-copy-wrapper"], &:focus-within [data-slot="user-message-copy-wrapper"] { opacity: 1; diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 500c73c5ee2..ee83ffa15bd 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -131,7 +131,6 @@ export interface MessageProps { parts: PartType[] actions?: UserActions showAssistantCopyPartID?: string | null - interrupted?: boolean showReasoningSummaries?: boolean } @@ -691,12 +690,7 @@ export function Message(props: MessageProps) { {(userMessage) => ( - + )} @@ -887,12 +881,7 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) { ) } -export function UserMessageDisplay(props: { - message: UserMessage - parts: PartType[] - actions?: UserActions - interrupted?: boolean -}) { +export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[]; actions?: UserActions }) { const data = useData() const dialog = useDialog() const i18n = useI18n() @@ -947,10 +936,7 @@ export function UserMessageDisplay(props: { return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0") }) - const metaTail = createMemo(() => { - const items = [stamp(), props.interrupted ? i18n.t("ui.message.interrupted") : ""] - return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0") - }) + const metaTail = stamp const openImagePreview = (url: string, alt?: string) => { dialog.show(() => ) @@ -981,7 +967,7 @@ export function UserMessageDisplay(props: { } return ( -
+
0}>
@@ -1021,7 +1007,7 @@ export function UserMessageDisplay(props: {
-
+
@@ -1305,14 +1291,13 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { ) } -PART_MAPPING["compaction"] = function CompactionPartDisplay() { - const i18n = useI18n() +export function MessageDivider(props: { label: string }) { return (
- {i18n.t("ui.messagePart.compaction")} + {props.label}
@@ -1320,6 +1305,11 @@ PART_MAPPING["compaction"] = function CompactionPartDisplay() { ) } +PART_MAPPING["compaction"] = function CompactionPartDisplay() { + const i18n = useI18n() + return +} + PART_MAPPING["text"] = function TextPartDisplay(props) { const data = useData() const i18n = useI18n() diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 8783e895766..41e160d16f3 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -7,7 +7,7 @@ import { Binary } from "@opencode-ai/util/binary" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" import { Dynamic } from "solid-js/web" -import { AssistantParts, Message, Part, PART_MAPPING, type UserActions } from "./message-part" +import { AssistantParts, Message, MessageDivider, PART_MAPPING, type UserActions } from "./message-part" import { Card } from "./card" import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" @@ -276,6 +276,11 @@ export function SessionTurn( ) const interrupted = createMemo(() => assistantMessages().some((m) => m.error?.name === "MessageAbortedError")) + const divider = createMemo(() => { + if (compaction()) return i18n.t("ui.messagePart.compaction") + if (interrupted()) return i18n.t("ui.message.interrupted") + return "" + }) const error = createMemo( () => assistantMessages().find((m) => m.error && m.error.name !== "MessageAbortedError")?.error, ) @@ -384,11 +389,11 @@ export function SessionTurn( class={props.classes?.container} >
- +
- +
- +
0}> From 7066e2a25eb90fc33736075b08d3babd46e41737 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 12 Mar 2026 20:09:30 -0400 Subject: [PATCH 03/48] reorder provider list in providers login (#17262) --- packages/opencode/src/cli/cmd/providers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 53a9b1280e4..631ca781120 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -318,10 +318,10 @@ export const ProvidersLoginCommand = cmd({ const priority: Record = { opencode: 0, - anthropic: 1, + openai: 1, "github-copilot": 2, - openai: 3, - google: 4, + google: 3, + anthropic: 4, openrouter: 5, vercel: 6, } From 3998df8112d398c2c5cd6253d352e4660589927e Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:15:34 +1000 Subject: [PATCH 04/48] fix(app): increase CI e2e workers (#17263) --- packages/app/playwright.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts index a97c8265144..2667b89a1cd 100644 --- a/packages/app/playwright.config.ts +++ b/packages/app/playwright.config.ts @@ -6,6 +6,7 @@ const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1" const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" const command = `bun run dev -- --host 0.0.0.0 --port ${port}` const reuse = !process.env.CI +const workers = Number(process.env.PLAYWRIGHT_WORKERS ?? (process.env.CI ? 5 : 0)) || undefined export default defineConfig({ testDir: "./e2e", @@ -17,6 +18,7 @@ export default defineConfig({ fullyParallel: process.env.PLAYWRIGHT_FULLY_PARALLEL === "1", forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, + workers, reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]], webServer: { command, From 3016efba47b5e6c46c82f44bb9519b9741a54441 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:42:31 -0500 Subject: [PATCH 05/48] tweak: rm openrouter warning (#17259) --- packages/opencode/src/cli/cmd/tui/app.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4f7c94b1d39..8bb17ff1336 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -677,20 +677,6 @@ function App() { }, ]) - createEffect(() => { - const currentModel = local.model.current() - if (!currentModel) return - if (currentModel.providerID === "openrouter" && !kv.get("openrouter_warning", false)) { - untrack(() => { - DialogAlert.show( - dialog, - "Warning", - "While openrouter is a convenient way to access LLMs your request will often be routed to subpar providers that do not work well in our testing.\n\nFor reliable access to models check out OpenCode Zen\nhttps://opencode.ai/zen", - ).then(() => kv.set("openrouter_warning", true)) - }) - } - }) - sdk.event.on(TuiEvent.CommandExecute.type, (evt) => { command.trigger(evt.properties.command) }) From 0a281c7390e6fd5373c3c61426f524b5e5ec6a6f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 12 Mar 2026 20:43:24 -0400 Subject: [PATCH 06/48] refactor(auth): effectify AuthService (#17212) --- packages/opencode/src/auth/index.ts | 40 +++------- packages/opencode/src/auth/service.ts | 101 ++++++++++++++++++++++++ packages/opencode/src/effect/runtime.ts | 5 +- 3 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 packages/opencode/src/auth/service.ts diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 80253a665e9..79e9e615d21 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,9 +1,13 @@ -import path from "path" -import { Global } from "../global" +import { Effect } from "effect" import z from "zod" -import { Filesystem } from "../util/filesystem" +import { runtime } from "@/effect/runtime" +import * as S from "./service" -export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" +export { OAUTH_DUMMY_KEY } from "./service" + +function runPromise(f: (service: S.AuthService.Service) => Effect.Effect) { + return runtime.runPromise(S.AuthService.use(f)) +} export namespace Auth { export const Oauth = z @@ -35,39 +39,19 @@ export namespace Auth { export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" }) export type Info = z.infer - const filepath = path.join(Global.Path.data, "auth.json") - export async function get(providerID: string) { - const auth = await all() - return auth[providerID] + return runPromise((service) => service.get(providerID)) } export async function all(): Promise> { - const data = await Filesystem.readJson>(filepath).catch(() => ({})) - return Object.entries(data).reduce( - (acc, [key, value]) => { - const parsed = Info.safeParse(value) - if (!parsed.success) return acc - acc[key] = parsed.data - return acc - }, - {} as Record, - ) + return runPromise((service) => service.all()) } export async function set(key: string, info: Info) { - const normalized = key.replace(/\/+$/, "") - const data = await all() - if (normalized !== key) delete data[key] - delete data[normalized + "/"] - await Filesystem.writeJson(filepath, { ...data, [normalized]: info }, 0o600) + return runPromise((service) => service.set(key, info)) } export async function remove(key: string) { - const normalized = key.replace(/\/+$/, "") - const data = await all() - delete data[key] - delete data[normalized] - await Filesystem.writeJson(filepath, data, 0o600) + return runPromise((service) => service.remove(key)) } } diff --git a/packages/opencode/src/auth/service.ts b/packages/opencode/src/auth/service.ts new file mode 100644 index 00000000000..100a132b873 --- /dev/null +++ b/packages/opencode/src/auth/service.ts @@ -0,0 +1,101 @@ +import path from "path" +import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect" +import { Global } from "../global" +import { Filesystem } from "../util/filesystem" + +export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" + +export class Oauth extends Schema.Class("OAuth")({ + type: Schema.Literal("oauth"), + refresh: Schema.String, + access: Schema.String, + expires: Schema.Number, + accountId: Schema.optional(Schema.String), + enterpriseUrl: Schema.optional(Schema.String), +}) {} + +export class Api extends Schema.Class("ApiAuth")({ + type: Schema.Literal("api"), + key: Schema.String, +}) {} + +export class WellKnown extends Schema.Class("WellKnownAuth")({ + type: Schema.Literal("wellknown"), + key: Schema.String, + token: Schema.String, +}) {} + +export const Info = Schema.Union([Oauth, Api, WellKnown]) +export type Info = Schema.Schema.Type + +export class AuthServiceError extends Schema.TaggedErrorClass()("AuthServiceError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +const file = path.join(Global.Path.data, "auth.json") + +const fail = (message: string) => (cause: unknown) => new AuthServiceError({ message, cause }) + +export namespace AuthService { + export interface Service { + readonly get: (providerID: string) => Effect.Effect + readonly all: () => Effect.Effect, AuthServiceError> + readonly set: (key: string, info: Info) => Effect.Effect + readonly remove: (key: string) => Effect.Effect + } +} + +export class AuthService extends ServiceMap.Service()("@opencode/Auth") { + static readonly layer = Layer.effect( + AuthService, + Effect.gen(function* () { + const decode = Schema.decodeUnknownOption(Info) + + const all = Effect.fn("AuthService.all")(() => + Effect.tryPromise({ + try: async () => { + const data = await Filesystem.readJson>(file).catch(() => ({})) + return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined)) + }, + catch: fail("Failed to read auth data"), + }), + ) + + const get = Effect.fn("AuthService.get")(function* (providerID: string) { + return (yield* all())[providerID] + }) + + const set = Effect.fn("AuthService.set")(function* (key: string, info: Info) { + const norm = key.replace(/\/+$/, "") + const data = yield* all() + if (norm !== key) delete data[key] + delete data[norm + "/"] + yield* Effect.tryPromise({ + try: () => Filesystem.writeJson(file, { ...data, [norm]: info }, 0o600), + catch: fail("Failed to write auth data"), + }) + }) + + const remove = Effect.fn("AuthService.remove")(function* (key: string) { + const norm = key.replace(/\/+$/, "") + const data = yield* all() + delete data[key] + delete data[norm] + yield* Effect.tryPromise({ + try: () => Filesystem.writeJson(file, data, 0o600), + catch: fail("Failed to write auth data"), + }) + }) + + return AuthService.of({ + get, + all, + set, + remove, + }) + }), + ) + + static readonly defaultLayer = AuthService.layer +} diff --git a/packages/opencode/src/effect/runtime.ts b/packages/opencode/src/effect/runtime.ts index 1868b38a015..23acff73379 100644 --- a/packages/opencode/src/effect/runtime.ts +++ b/packages/opencode/src/effect/runtime.ts @@ -1,4 +1,5 @@ -import { ManagedRuntime } from "effect" +import { Layer, ManagedRuntime } from "effect" import { AccountService } from "@/account/service" +import { AuthService } from "@/auth/service" -export const runtime = ManagedRuntime.make(AccountService.defaultLayer) +export const runtime = ManagedRuntime.make(Layer.mergeAll(AccountService.defaultLayer, AuthService.defaultLayer)) From d9dd33aeebad49120ea8204e34520db872c9f553 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 12 Mar 2026 20:56:40 -0400 Subject: [PATCH 07/48] feat(cli): add console account subcommands (#17265) --- packages/opencode/src/cli/cmd/account.ts | 25 ++++++++++++++++++++++++ packages/opencode/src/index.ts | 7 ++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index dd0834a3d80..78aadca626b 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -192,3 +192,28 @@ export const OrgsCommand = cmd({ await runtime.runPromise(orgsEffect()) }, }) + +export const ConsoleCommand = cmd({ + command: "console", + describe: "manage console account", + builder: (yargs) => + yargs + .command({ + ...LoginCommand, + describe: "log in to console", + }) + .command({ + ...LogoutCommand, + describe: "log out from console", + }) + .command({ + ...SwitchCommand, + describe: "switch active org", + }) + .command({ + ...OrgsCommand, + describe: "list orgs", + }) + .demandCommand(), + async handler() {}, +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 10ed8530acb..fe7fd63ef12 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -3,7 +3,7 @@ import { hideBin } from "yargs/helpers" import { RunCommand } from "./cli/cmd/run" import { GenerateCommand } from "./cli/cmd/generate" import { Log } from "./util/log" -import { LoginCommand, LogoutCommand, SwitchCommand, OrgsCommand } from "./cli/cmd/account" +import { ConsoleCommand } from "./cli/cmd/account" import { ProvidersCommand } from "./cli/cmd/providers" import { AgentCommand } from "./cli/cmd/agent" import { UpgradeCommand } from "./cli/cmd/upgrade" @@ -135,10 +135,7 @@ let cli = yargs(hideBin(process.argv)) .command(RunCommand) .command(GenerateCommand) .command(DebugCommand) - .command(LoginCommand) - .command(LogoutCommand) - .command(SwitchCommand) - .command(OrgsCommand) + .command(ConsoleCommand) .command(ProvidersCommand) .command(AgentCommand) .command(UpgradeCommand) From 84df96eaefe7a920b48b205501fc849ba027e9ad Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 13 Mar 2026 09:18:27 +0800 Subject: [PATCH 08/48] desktop: multi-window support in electron (#17155) --- packages/app/src/app.tsx | 35 ++++++++++--------- packages/app/src/entry.tsx | 7 ++-- packages/desktop-electron/src/main/index.ts | 9 +++-- packages/desktop-electron/src/main/ipc.ts | 4 +++ packages/desktop-electron/src/main/menu.ts | 6 ++++ packages/desktop-electron/src/main/windows.ts | 14 ++++++-- .../desktop-electron/src/preload/index.ts | 2 ++ .../desktop-electron/src/preload/types.ts | 2 ++ .../desktop-electron/src/renderer/index.tsx | 26 +++++++++++--- packages/desktop/src/menu.ts | 7 ++-- packages/ui/src/theme/context.tsx | 16 ++++++--- 11 files changed, 91 insertions(+), 37 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 1b7ffde46a1..c6fca36d59a 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -159,7 +159,7 @@ const effectMinDuration = (e: Effect.Effect) => Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0])) -function ConnectionGate(props: ParentProps) { +function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { const server = useServer() const checkServerHealth = useCheckServerHealth() @@ -168,21 +168,23 @@ function ConnectionGate(props: ParentProps) { // performs repeated health check with a grace period for // non-http connections, otherwise fails instantly const [startupHealthCheck, healthCheckActions] = createResource(() => - Effect.gen(function* () { - if (!server.current) return true - const { http, type } = server.current + props.disableHealthCheck + ? true + : Effect.gen(function* () { + if (!server.current) return true + const { http, type } = server.current - while (true) { - const res = yield* Effect.promise(() => checkServerHealth(http)) - if (res.healthy) return true - if (checkMode() === "background" || type === "http") return false - } - }).pipe( - effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0), - Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }), - Effect.ensuring(Effect.sync(() => setCheckMode("background"))), - Effect.runPromise, - ), + while (true) { + const res = yield* Effect.promise(() => checkServerHealth(http)) + if (res.healthy) return true + if (checkMode() === "background" || type === "http") return false + } + }).pipe( + effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0), + Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }), + Effect.ensuring(Effect.sync(() => setCheckMode("background"))), + Effect.runPromise, + ), ) return ( @@ -261,10 +263,11 @@ export function AppInterface(props: { defaultServer: ServerConnection.Key servers?: Array router?: Component + disableHealthCheck?: boolean }) { return ( - + ( - + ), diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 64c2eb10f2f..484e4feb207 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -5,7 +5,7 @@ import { createServer } from "node:net" import { homedir } from "node:os" import { join } from "node:path" import type { Event } from "electron" -import { app, type BrowserWindow, dialog } from "electron" +import { app, BrowserWindow, dialog } from "electron" import pkg from "electron-updater" const APP_NAMES: Record = { @@ -32,7 +32,7 @@ import { initLogging } from "./logging" import { parseMarkdown } from "./markdown" import { createMenu } from "./menu" import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server" -import { createLoadingWindow, createMainWindow, setDockIcon } from "./windows" +import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows" const initEmitter = new EventEmitter() let initStep: InitStep = { phase: "server_waiting" } @@ -156,12 +156,9 @@ async function initialize() { const globals = { updaterEnabled: UPDATER_ENABLED, - wsl: getWslConfig().enabled, deepLinks: pendingDeepLinks, } - wireMenu() - if (needsMigration) { const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)]) if (show) { @@ -178,6 +175,7 @@ async function initialize() { } mainWindow = createMainWindow(globals) + wireMenu() overlay?.close() } @@ -231,6 +229,7 @@ registerIpcHandlers({ runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail), checkUpdate: async () => checkUpdate(), installUpdate: async () => installUpdate(), + setBackgroundColor: (color) => setBackgroundColor(color), }) function killSidecar() { diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts index c0d8773c29b..71b3c339580 100644 --- a/packages/desktop-electron/src/main/ipc.ts +++ b/packages/desktop-electron/src/main/ipc.ts @@ -24,6 +24,7 @@ type Deps = { runUpdater: (alertOnFail: boolean) => Promise | void checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> installUpdate: () => Promise | void + setBackgroundColor: (color: string) => void } export function registerIpcHandlers(deps: Deps) { @@ -53,6 +54,7 @@ export function registerIpcHandlers(deps: Deps) { ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail)) ipcMain.handle("check-update", () => deps.checkUpdate()) ipcMain.handle("install-update", () => deps.installUpdate()) + ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color)) ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => { const store = getStore(name) const value = store.get(key) @@ -140,6 +142,8 @@ export function registerIpcHandlers(deps: Deps) { new Notification({ title, body }).show() }) + ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length) + ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => { const win = BrowserWindow.fromWebContents(event.sender) return win?.isFocused() ?? false diff --git a/packages/desktop-electron/src/main/menu.ts b/packages/desktop-electron/src/main/menu.ts index 53707ba7f25..d8997be310f 100644 --- a/packages/desktop-electron/src/main/menu.ts +++ b/packages/desktop-electron/src/main/menu.ts @@ -1,6 +1,7 @@ import { BrowserWindow, Menu, shell } from "electron" import { UPDATER_ENABLED } from "./constants" +import { createMainWindow } from "./windows" type Deps = { trigger: (id: string) => void @@ -48,6 +49,11 @@ export function createMenu(deps: Deps) { submenu: [ { label: "New Session", accelerator: "Shift+Cmd+S", click: () => deps.trigger("session.new") }, { label: "Open Project...", accelerator: "Cmd+O", click: () => deps.trigger("project.open") }, + { + label: "New Window", + accelerator: "Cmd+Shift+N", + click: () => createMainWindow({ updaterEnabled: UPDATER_ENABLED }), + }, { type: "separator" }, { role: "close" }, ], diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts index d4ec5ac7901..0b7783f289e 100644 --- a/packages/desktop-electron/src/main/windows.ts +++ b/packages/desktop-electron/src/main/windows.ts @@ -6,12 +6,21 @@ import type { TitlebarTheme } from "../preload/types" type Globals = { updaterEnabled: boolean - wsl: boolean deepLinks?: string[] } const root = dirname(fileURLToPath(import.meta.url)) +let backgroundColor: string | undefined + +export function setBackgroundColor(color: string) { + backgroundColor = color +} + +export function getBackgroundColor(): string | undefined { + return backgroundColor +} + function iconsDir() { return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons") } @@ -59,6 +68,7 @@ export function createMainWindow(globals: Globals) { show: true, title: "OpenCode", icon: iconPath(), + backgroundColor, ...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const, @@ -95,6 +105,7 @@ export function createLoadingWindow(globals: Globals) { center: true, show: true, icon: iconPath(), + backgroundColor, ...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}), ...(process.platform === "win32" ? { @@ -131,7 +142,6 @@ function injectGlobals(win: BrowserWindow, globals: Globals) { const deepLinks = globals.deepLinks ?? [] const data = { updaterEnabled: globals.updaterEnabled, - wsl: globals.wsl, deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks, } void win.webContents.executeJavaScript( diff --git a/packages/desktop-electron/src/preload/index.ts b/packages/desktop-electron/src/preload/index.ts index c1ed9afd22a..296fcb2f1cc 100644 --- a/packages/desktop-electron/src/preload/index.ts +++ b/packages/desktop-electron/src/preload/index.ts @@ -28,6 +28,7 @@ const api: ElectronAPI = { storeKeys: (name) => ipcRenderer.invoke("store-keys", name), storeLength: (name) => ipcRenderer.invoke("store-length", name), + getWindowCount: () => ipcRenderer.invoke("get-window-count"), onSqliteMigrationProgress: (cb) => { const handler = (_: unknown, progress: SqliteMigrationProgress) => cb(progress) ipcRenderer.on("sqlite-migration-progress", handler) @@ -62,6 +63,7 @@ const api: ElectronAPI = { runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail), checkUpdate: () => ipcRenderer.invoke("check-update"), installUpdate: () => ipcRenderer.invoke("install-update"), + setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color), } contextBridge.exposeInMainWorld("api", api) diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts index ae4ca213ded..100508fcddc 100644 --- a/packages/desktop-electron/src/preload/types.ts +++ b/packages/desktop-electron/src/preload/types.ts @@ -36,6 +36,7 @@ export type ElectronAPI = { storeKeys: (name: string) => Promise storeLength: (name: string) => Promise + getWindowCount: () => Promise onSqliteMigrationProgress: (cb: (progress: SqliteMigrationProgress) => void) => () => void onMenuCommand: (cb: (id: string) => void) => () => void onDeepLink: (cb: (urls: string[]) => void) => () => void @@ -66,4 +67,5 @@ export type ElectronAPI = { runUpdater: (alertOnFail: boolean) => Promise checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> installUpdate: () => Promise + setBackgroundColor: (color: string) => Promise } diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx index e313d5594c4..6719c04bee2 100644 --- a/packages/desktop-electron/src/renderer/index.tsx +++ b/packages/desktop-electron/src/renderer/index.tsx @@ -10,14 +10,15 @@ import { useCommand, } from "@opencode-ai/app" import type { AsyncStorage } from "@solid-primitives/storage" -import { createResource, onCleanup, onMount, Show } from "solid-js" -import { render } from "solid-js/web" import { MemoryRouter } from "@solidjs/router" +import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js" +import { render } from "solid-js/web" import pkg from "../../package.json" import { initI18n, t } from "./i18n" import { UPDATER_ENABLED } from "./updater" import { webviewZoom } from "./webview-zoom" import "./styles.css" +import { useTheme } from "@opencode-ai/ui/theme" const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { @@ -226,7 +227,9 @@ const createPlatform = (): Platform => { const image = await window.api.readClipboardImage().catch(() => null) if (!image) return null const blob = new Blob([image.buffer], { type: "image/png" }) - return new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png" }) + return new File([blob], `pasted-image-${Date.now()}.png`, { + type: "image/png", + }) }, } } @@ -240,6 +243,8 @@ listenForDeepLinks() render(() => { const platform = createPlatform() + const [windowCount] = createResource(() => window.api.getWindowCount()) + // Fetch sidecar credentials (available immediately, before health check) const [sidecar] = createResource(() => window.api.awaitInitialization(() => undefined)) @@ -276,6 +281,18 @@ render(() => { function Inner() { const cmd = useCommand() menuTrigger = (id) => cmd.trigger(id) + + const theme = useTheme() + + createEffect(() => { + theme.themeId() + theme.mode() + const bg = getComputedStyle(document.documentElement).getPropertyValue("--background-base").trim() + if (bg) { + void window.api.setBackgroundColor(bg) + } + }) + return null } @@ -289,13 +306,14 @@ render(() => { return ( - + {(_) => { return ( 1} > diff --git a/packages/desktop/src/menu.ts b/packages/desktop/src/menu.ts index de6a1d6a76c..9005dd702f7 100644 --- a/packages/desktop/src/menu.ts +++ b/packages/desktop/src/menu.ts @@ -1,12 +1,11 @@ import { Menu, MenuItem, PredefinedMenuItem, Submenu } from "@tauri-apps/api/menu" +import { openUrl } from "@tauri-apps/plugin-opener" import { type as ostype } from "@tauri-apps/plugin-os" import { relaunch } from "@tauri-apps/plugin-process" -import { openUrl } from "@tauri-apps/plugin-opener" - -import { runUpdater, UPDATER_ENABLED } from "./updater" +import { commands } from "./bindings" import { installCli } from "./cli" import { initI18n, t } from "./i18n" -import { commands } from "./bindings" +import { runUpdater, UPDATER_ENABLED } from "./updater" export async function createMenu(trigger: (id: string) => void) { if (ostype() !== "macos") return diff --git a/packages/ui/src/theme/context.tsx b/packages/ui/src/theme/context.tsx index ad82a088d1e..9808c8e841e 100644 --- a/packages/ui/src/theme/context.tsx +++ b/packages/ui/src/theme/context.tsx @@ -1,9 +1,9 @@ -import { onMount, onCleanup, createEffect } from "solid-js" +import { createEffect, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" -import type { DesktopTheme } from "./types" -import { resolveThemeVariant, themeToCss } from "./resolve" -import { DEFAULT_THEMES } from "./default-themes" import { createSimpleContext } from "../context/helper" +import { DEFAULT_THEMES } from "./default-themes" +import { resolveThemeVariant, themeToCss } from "./resolve" +import type { DesktopTheme } from "./types" export type ColorScheme = "light" | "dark" | "system" @@ -87,6 +87,14 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ previewScheme: null as ColorScheme | null, }) + window.addEventListener("storage", (e) => { + if (e.key === STORAGE_KEYS.THEME_ID && e.newValue) setStore("themeId", e.newValue) + if (e.key === STORAGE_KEYS.COLOR_SCHEME && e.newValue) { + setStore("colorScheme", e.newValue as ColorScheme) + setStore("mode", e.newValue === "system" ? getSystemMode() : (e.newValue as any)) + } + }) + onMount(() => { const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const handler = () => { From dd68b85f5833212a65c822899f42968a7abba176 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 12 Mar 2026 22:08:37 -0400 Subject: [PATCH 09/48] refactor(provider): effectify ProviderAuthService (#17227) --- packages/opencode/src/project/instance.ts | 6 +- .../opencode/src/provider/auth-service.ts | 169 ++++++++++++++++++ packages/opencode/src/provider/auth.ts | 138 +++----------- packages/opencode/src/util/instance-state.ts | 50 ++++++ packages/opencode/test/project/state.test.ts | 115 ++++++++++++ packages/opencode/test/provider/auth.test.ts | 20 +++ .../opencode/test/util/instance-state.test.ts | 139 ++++++++++++++ 7 files changed, 520 insertions(+), 117 deletions(-) create mode 100644 packages/opencode/src/provider/auth-service.ts create mode 100644 packages/opencode/src/util/instance-state.ts create mode 100644 packages/opencode/test/project/state.test.ts create mode 100644 packages/opencode/test/provider/auth.test.ts create mode 100644 packages/opencode/test/util/instance-state.test.ts diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index df44a3a229c..dac5e71ba13 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -1,3 +1,4 @@ +import { Effect } from "effect" import { Log } from "@/util/log" import { Context } from "../util/context" import { Project } from "./project" @@ -5,6 +6,7 @@ import { State } from "./state" import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" import { Filesystem } from "@/util/filesystem" +import { InstanceState } from "@/util/instance-state" interface Context { directory: string @@ -106,7 +108,7 @@ export const Instance = { async reload(input: { directory: string; init?: () => Promise; project?: Project.Info; worktree?: string }) { const directory = Filesystem.resolve(input.directory) Log.Default.info("reloading instance", { directory }) - await State.dispose(directory) + await Promise.all([State.dispose(directory), Effect.runPromise(InstanceState.dispose(directory))]) cache.delete(directory) const next = track(directory, boot({ ...input, directory })) emit(directory) @@ -114,7 +116,7 @@ export const Instance = { }, async dispose() { Log.Default.info("disposing instance", { directory: Instance.directory }) - await State.dispose(Instance.directory) + await Promise.all([State.dispose(Instance.directory), Effect.runPromise(InstanceState.dispose(Instance.directory))]) cache.delete(Instance.directory) emit(Instance.directory) }, diff --git a/packages/opencode/src/provider/auth-service.ts b/packages/opencode/src/provider/auth-service.ts new file mode 100644 index 00000000000..4b5ac1777a8 --- /dev/null +++ b/packages/opencode/src/provider/auth-service.ts @@ -0,0 +1,169 @@ +import { Effect, Layer, Record, ServiceMap, Struct } from "effect" +import { Instance } from "@/project/instance" +import { Plugin } from "../plugin" +import { filter, fromEntries, map, pipe } from "remeda" +import type { AuthOuathResult } from "@opencode-ai/plugin" +import { NamedError } from "@opencode-ai/util/error" +import * as Auth from "@/auth/service" +import { InstanceState } from "@/util/instance-state" +import { ProviderID } from "./schema" +import z from "zod" + +export const Method = z + .object({ + type: z.union([z.literal("oauth"), z.literal("api")]), + label: z.string(), + }) + .meta({ + ref: "ProviderAuthMethod", + }) +export type Method = z.infer + +export const Authorization = z + .object({ + url: z.string(), + method: z.union([z.literal("auto"), z.literal("code")]), + instructions: z.string(), + }) + .meta({ + ref: "ProviderAuthAuthorization", + }) +export type Authorization = z.infer + +export const OauthMissing = NamedError.create( + "ProviderAuthOauthMissing", + z.object({ + providerID: ProviderID.zod, + }), +) + +export const OauthCodeMissing = NamedError.create( + "ProviderAuthOauthCodeMissing", + z.object({ + providerID: ProviderID.zod, + }), +) + +export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({})) + +export type ProviderAuthError = + | Auth.AuthServiceError + | InstanceType + | InstanceType + | InstanceType + +export namespace ProviderAuthService { + export interface Service { + /** Get available auth methods for each provider (e.g. OAuth, API key). */ + readonly methods: () => Effect.Effect> + + /** Start an OAuth authorization flow for a provider. Returns the URL to redirect to. */ + readonly authorize: (input: { providerID: ProviderID; method: number }) => Effect.Effect + + /** Complete an OAuth flow after the user has authorized. Exchanges the code/callback for credentials. */ + readonly callback: (input: { + providerID: ProviderID + method: number + code?: string + }) => Effect.Effect + + /** Set an API key directly for a provider (no OAuth flow). */ + readonly api: (input: { providerID: ProviderID; key: string }) => Effect.Effect + } +} + +export class ProviderAuthService extends ServiceMap.Service()( + "@opencode/ProviderAuth", +) { + static readonly layer = Layer.effect( + ProviderAuthService, + Effect.gen(function* () { + const auth = yield* Auth.AuthService + const state = yield* InstanceState.make({ + lookup: () => + Effect.promise(async () => { + const methods = pipe( + await Plugin.list(), + filter((x) => x.auth?.provider !== undefined), + map((x) => [x.auth!.provider, x.auth!] as const), + fromEntries(), + ) + return { methods, pending: new Map() } + }), + }) + + const methods = Effect.fn("ProviderAuthService.methods")(function* () { + const x = yield* InstanceState.get(state) + return Record.map(x.methods, (y) => y.methods.map((z): Method => Struct.pick(z, ["type", "label"]))) + }) + + const authorize = Effect.fn("ProviderAuthService.authorize")(function* (input: { + providerID: ProviderID + method: number + }) { + const s = yield* InstanceState.get(state) + const method = s.methods[input.providerID].methods[input.method] + if (method.type !== "oauth") return + const result = yield* Effect.promise(() => method.authorize()) + s.pending.set(input.providerID, result) + return { + url: result.url, + method: result.method, + instructions: result.instructions, + } + }) + + const callback = Effect.fn("ProviderAuthService.callback")(function* (input: { + providerID: ProviderID + method: number + code?: string + }) { + const s = yield* InstanceState.get(state) + const match = s.pending.get(input.providerID) + if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID })) + + if (match.method === "code" && !input.code) + return yield* Effect.fail(new OauthCodeMissing({ providerID: input.providerID })) + + const result = yield* Effect.promise(() => + match.method === "code" ? match.callback(input.code!) : match.callback(), + ) + + if (!result || result.type !== "success") return yield* Effect.fail(new OauthCallbackFailed({})) + + if ("key" in result) { + yield* auth.set(input.providerID, { + type: "api", + key: result.key, + }) + } + + if ("refresh" in result) { + yield* auth.set(input.providerID, { + type: "oauth", + access: result.access, + refresh: result.refresh, + expires: result.expires, + ...(result.accountId ? { accountId: result.accountId } : {}), + }) + } + }) + + const api = Effect.fn("ProviderAuthService.api")(function* (input: { providerID: ProviderID; key: string }) { + yield* auth.set(input.providerID, { + type: "api", + key: input.key, + }) + }) + + return ProviderAuthService.of({ + methods, + authorize, + callback, + api, + }) + }), + ) + + static readonly defaultLayer = ProviderAuthService.layer.pipe(Layer.provide(Auth.AuthService.defaultLayer)) +} diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index f6c25432034..095c6e57ed7 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -1,75 +1,36 @@ -import { Instance } from "@/project/instance" -import { Plugin } from "../plugin" -import { map, filter, pipe, fromEntries, mapValues } from "remeda" +import { Effect, ManagedRuntime } from "effect" import z from "zod" + import { fn } from "@/util/fn" -import type { AuthOuathResult, Hooks } from "@opencode-ai/plugin" -import { NamedError } from "@opencode-ai/util/error" -import { Auth } from "@/auth" +import * as S from "./auth-service" import { ProviderID } from "./schema" -export namespace ProviderAuth { - const state = Instance.state(async () => { - const methods = pipe( - await Plugin.list(), - filter((x) => x.auth?.provider !== undefined), - map((x) => [x.auth!.provider, x.auth!] as const), - fromEntries(), - ) - return { methods, pending: {} as Record } - }) +// Separate runtime: ProviderAuthService can't join the shared runtime because +// runtime.ts → auth-service.ts → provider/auth.ts creates a circular import. +// AuthService is stateless file I/O so the duplicate instance is harmless. +const rt = ManagedRuntime.make(S.ProviderAuthService.defaultLayer) - export const Method = z - .object({ - type: z.union([z.literal("oauth"), z.literal("api")]), - label: z.string(), - }) - .meta({ - ref: "ProviderAuthMethod", - }) - export type Method = z.infer +function runPromise(f: (service: S.ProviderAuthService.Service) => Effect.Effect) { + return rt.runPromise(S.ProviderAuthService.use(f)) +} + +export namespace ProviderAuth { + export const Method = S.Method + export type Method = S.Method export async function methods() { - const s = await state().then((x) => x.methods) - return mapValues(s, (x) => - x.methods.map( - (y): Method => ({ - type: y.type, - label: y.label, - }), - ), - ) + return runPromise((service) => service.methods()) } - export const Authorization = z - .object({ - url: z.string(), - method: z.union([z.literal("auto"), z.literal("code")]), - instructions: z.string(), - }) - .meta({ - ref: "ProviderAuthAuthorization", - }) - export type Authorization = z.infer + export const Authorization = S.Authorization + export type Authorization = S.Authorization export const authorize = fn( z.object({ providerID: ProviderID.zod, method: z.number(), }), - async (input): Promise => { - const auth = await state().then((s) => s.methods[input.providerID]) - const method = auth.methods[input.method] - if (method.type === "oauth") { - const result = await method.authorize() - await state().then((s) => (s.pending[input.providerID] = result)) - return { - url: result.url, - method: result.method, - instructions: result.instructions, - } - } - }, + async (input): Promise => runPromise((service) => service.authorize(input)), ) export const callback = fn( @@ -78,44 +39,7 @@ export namespace ProviderAuth { method: z.number(), code: z.string().optional(), }), - async (input) => { - const match = await state().then((s) => s.pending[input.providerID]) - if (!match) throw new OauthMissing({ providerID: input.providerID }) - let result - - if (match.method === "code") { - if (!input.code) throw new OauthCodeMissing({ providerID: input.providerID }) - result = await match.callback(input.code) - } - - if (match.method === "auto") { - result = await match.callback() - } - - if (result?.type === "success") { - if ("key" in result) { - await Auth.set(input.providerID, { - type: "api", - key: result.key, - }) - } - if ("refresh" in result) { - const info: Auth.Info = { - type: "oauth", - access: result.access, - refresh: result.refresh, - expires: result.expires, - } - if (result.accountId) { - info.accountId = result.accountId - } - await Auth.set(input.providerID, info) - } - return - } - - throw new OauthCallbackFailed({}) - }, + async (input) => runPromise((service) => service.callback(input)), ) export const api = fn( @@ -123,26 +47,10 @@ export namespace ProviderAuth { providerID: ProviderID.zod, key: z.string(), }), - async (input) => { - await Auth.set(input.providerID, { - type: "api", - key: input.key, - }) - }, + async (input) => runPromise((service) => service.api(input)), ) - export const OauthMissing = NamedError.create( - "ProviderAuthOauthMissing", - z.object({ - providerID: ProviderID.zod, - }), - ) - export const OauthCodeMissing = NamedError.create( - "ProviderAuthOauthCodeMissing", - z.object({ - providerID: ProviderID.zod, - }), - ) - - export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({})) + export import OauthMissing = S.OauthMissing + export import OauthCodeMissing = S.OauthCodeMissing + export import OauthCallbackFailed = S.OauthCallbackFailed } diff --git a/packages/opencode/src/util/instance-state.ts b/packages/opencode/src/util/instance-state.ts new file mode 100644 index 00000000000..c7b84adb8ab --- /dev/null +++ b/packages/opencode/src/util/instance-state.ts @@ -0,0 +1,50 @@ +import { Effect, ScopedCache, Scope } from "effect" + +import { Instance } from "@/project/instance" + +const TypeId = Symbol.for("@opencode/InstanceState") + +type Task = (key: string) => Effect.Effect + +const tasks = new Set() + +export namespace InstanceState { + export interface State { + readonly [TypeId]: typeof TypeId + readonly cache: ScopedCache.ScopedCache + } + + export const make = (input: { + lookup: (key: string) => Effect.Effect + release?: (value: A, key: string) => Effect.Effect + }): Effect.Effect, never, R | Scope.Scope> => + Effect.gen(function* () { + const cache = yield* ScopedCache.make({ + capacity: Number.POSITIVE_INFINITY, + lookup: (key) => + Effect.acquireRelease(input.lookup(key), (value) => (input.release ? input.release(value, key) : Effect.void)), + }) + + const task: Task = (key) => ScopedCache.invalidate(cache, key) + tasks.add(task) + yield* Effect.addFinalizer(() => Effect.sync(() => void tasks.delete(task))) + + return { + [TypeId]: TypeId, + cache, + } + }) + + export const get = (self: State) => ScopedCache.get(self.cache, Instance.directory) + + export const has = (self: State) => ScopedCache.has(self.cache, Instance.directory) + + export const invalidate = (self: State) => + ScopedCache.invalidate(self.cache, Instance.directory) + + export const dispose = (key: string) => + Effect.all( + [...tasks].map((task) => task(key)), + { concurrency: "unbounded" }, + ) +} diff --git a/packages/opencode/test/project/state.test.ts b/packages/opencode/test/project/state.test.ts new file mode 100644 index 00000000000..c1a6dab3154 --- /dev/null +++ b/packages/opencode/test/project/state.test.ts @@ -0,0 +1,115 @@ +import { afterEach, expect, test } from "bun:test" + +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" + +afterEach(async () => { + await Instance.disposeAll() +}) + +test("Instance.state caches values for the same instance", async () => { + await using tmp = await tmpdir() + let n = 0 + const state = Instance.state(() => ({ n: ++n })) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const a = state() + const b = state() + expect(a).toBe(b) + expect(n).toBe(1) + }, + }) +}) + +test("Instance.state isolates values by directory", async () => { + await using a = await tmpdir() + await using b = await tmpdir() + let n = 0 + const state = Instance.state(() => ({ n: ++n })) + + const x = await Instance.provide({ + directory: a.path, + fn: async () => state(), + }) + const y = await Instance.provide({ + directory: b.path, + fn: async () => state(), + }) + const z = await Instance.provide({ + directory: a.path, + fn: async () => state(), + }) + + expect(x).toBe(z) + expect(x).not.toBe(y) + expect(n).toBe(2) +}) + +test("Instance.state is disposed on instance reload", async () => { + await using tmp = await tmpdir() + const seen: string[] = [] + let n = 0 + const state = Instance.state( + () => ({ n: ++n }), + async (value) => { + seen.push(String(value.n)) + }, + ) + + const a = await Instance.provide({ + directory: tmp.path, + fn: async () => state(), + }) + await Instance.reload({ directory: tmp.path }) + const b = await Instance.provide({ + directory: tmp.path, + fn: async () => state(), + }) + + expect(a).not.toBe(b) + expect(seen).toEqual(["1"]) +}) + +test("Instance.state is disposed on disposeAll", async () => { + await using a = await tmpdir() + await using b = await tmpdir() + const seen: string[] = [] + const state = Instance.state( + () => ({ dir: Instance.directory }), + async (value) => { + seen.push(value.dir) + }, + ) + + await Instance.provide({ + directory: a.path, + fn: async () => state(), + }) + await Instance.provide({ + directory: b.path, + fn: async () => state(), + }) + await Instance.disposeAll() + + expect(seen.sort()).toEqual([a.path, b.path].sort()) +}) + +test("Instance.state dedupes concurrent promise initialization", async () => { + await using tmp = await tmpdir() + let n = 0 + const state = Instance.state(async () => { + n += 1 + await Bun.sleep(10) + return { n } + }) + + const [a, b] = await Instance.provide({ + directory: tmp.path, + fn: async () => Promise.all([state(), state()]), + }) + + expect(a).toBe(b) + expect(n).toBe(1) +}) diff --git a/packages/opencode/test/provider/auth.test.ts b/packages/opencode/test/provider/auth.test.ts new file mode 100644 index 00000000000..99babd44a69 --- /dev/null +++ b/packages/opencode/test/provider/auth.test.ts @@ -0,0 +1,20 @@ +import { afterEach, expect, test } from "bun:test" +import { Auth } from "../../src/auth" +import { ProviderAuth } from "../../src/provider/auth" +import { ProviderID } from "../../src/provider/schema" + +afterEach(async () => { + await Auth.remove("test-provider-auth") +}) + +test("ProviderAuth.api persists auth via AuthService", async () => { + await ProviderAuth.api({ + providerID: ProviderID.make("test-provider-auth"), + key: "sk-test", + }) + + expect(await Auth.get("test-provider-auth")).toEqual({ + type: "api", + key: "sk-test", + }) +}) diff --git a/packages/opencode/test/util/instance-state.test.ts b/packages/opencode/test/util/instance-state.test.ts new file mode 100644 index 00000000000..e5d2129fb07 --- /dev/null +++ b/packages/opencode/test/util/instance-state.test.ts @@ -0,0 +1,139 @@ +import { afterEach, expect, test } from "bun:test" +import { Effect } from "effect" + +import { Instance } from "../../src/project/instance" +import { InstanceState } from "../../src/util/instance-state" +import { tmpdir } from "../fixture/fixture" + +async function access(state: InstanceState.State, dir: string) { + return Instance.provide({ + directory: dir, + fn: () => Effect.runPromise(InstanceState.get(state)), + }) +} + +afterEach(async () => { + await Instance.disposeAll() +}) + +test("InstanceState caches values for the same instance", async () => { + await using tmp = await tmpdir() + let n = 0 + + await Effect.runPromise( + Effect.scoped( + Effect.gen(function* () { + const state = yield* InstanceState.make({ + lookup: () => Effect.sync(() => ({ n: ++n })), + }) + + const a = yield* Effect.promise(() => access(state, tmp.path)) + const b = yield* Effect.promise(() => access(state, tmp.path)) + + expect(a).toBe(b) + expect(n).toBe(1) + }), + ), + ) +}) + +test("InstanceState isolates values by directory", async () => { + await using a = await tmpdir() + await using b = await tmpdir() + let n = 0 + + await Effect.runPromise( + Effect.scoped( + Effect.gen(function* () { + const state = yield* InstanceState.make({ + lookup: (dir) => Effect.sync(() => ({ dir, n: ++n })), + }) + + const x = yield* Effect.promise(() => access(state, a.path)) + const y = yield* Effect.promise(() => access(state, b.path)) + const z = yield* Effect.promise(() => access(state, a.path)) + + expect(x).toBe(z) + expect(x).not.toBe(y) + expect(n).toBe(2) + }), + ), + ) +}) + +test("InstanceState is disposed on instance reload", async () => { + await using tmp = await tmpdir() + const seen: string[] = [] + let n = 0 + + await Effect.runPromise( + Effect.scoped( + Effect.gen(function* () { + const state = yield* InstanceState.make({ + lookup: () => Effect.sync(() => ({ n: ++n })), + release: (value) => + Effect.sync(() => { + seen.push(String(value.n)) + }), + }) + + const a = yield* Effect.promise(() => access(state, tmp.path)) + yield* Effect.promise(() => Instance.reload({ directory: tmp.path })) + const b = yield* Effect.promise(() => access(state, tmp.path)) + + expect(a).not.toBe(b) + expect(seen).toEqual(["1"]) + }), + ), + ) +}) + +test("InstanceState is disposed on disposeAll", async () => { + await using a = await tmpdir() + await using b = await tmpdir() + const seen: string[] = [] + + await Effect.runPromise( + Effect.scoped( + Effect.gen(function* () { + const state = yield* InstanceState.make({ + lookup: (dir) => Effect.sync(() => ({ dir })), + release: (value) => + Effect.sync(() => { + seen.push(value.dir) + }), + }) + + yield* Effect.promise(() => access(state, a.path)) + yield* Effect.promise(() => access(state, b.path)) + yield* Effect.promise(() => Instance.disposeAll()) + + expect(seen.sort()).toEqual([a.path, b.path].sort()) + }), + ), + ) +}) + +test("InstanceState dedupes concurrent lookups for the same directory", async () => { + await using tmp = await tmpdir() + let n = 0 + + await Effect.runPromise( + Effect.scoped( + Effect.gen(function* () { + const state = yield* InstanceState.make({ + lookup: () => + Effect.promise(async () => { + n += 1 + await Bun.sleep(10) + return { n } + }), + }) + + const [a, b] = yield* Effect.promise(() => Promise.all([access(state, tmp.path), access(state, tmp.path)])) + expect(a).toBe(b) + expect(n).toBe(1) + }), + ), + ) +}) From dcb17c6a678918ce0786640729fcc8cd8adb1746 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 13 Mar 2026 02:10:27 +0000 Subject: [PATCH 10/48] chore: generate --- packages/opencode/src/util/instance-state.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/util/instance-state.ts b/packages/opencode/src/util/instance-state.ts index c7b84adb8ab..5d0ffbf7930 100644 --- a/packages/opencode/src/util/instance-state.ts +++ b/packages/opencode/src/util/instance-state.ts @@ -22,7 +22,9 @@ export namespace InstanceState { const cache = yield* ScopedCache.make({ capacity: Number.POSITIVE_INFINITY, lookup: (key) => - Effect.acquireRelease(input.lookup(key), (value) => (input.release ? input.release(value, key) : Effect.void)), + Effect.acquireRelease(input.lookup(key), (value) => + input.release ? input.release(value, key) : Effect.void, + ), }) const task: Task = (key) => ScopedCache.invalidate(cache, key) @@ -39,8 +41,7 @@ export namespace InstanceState { export const has = (self: State) => ScopedCache.has(self.cache, Instance.directory) - export const invalidate = (self: State) => - ScopedCache.invalidate(self.cache, Instance.directory) + export const invalidate = (self: State) => ScopedCache.invalidate(self.cache, Instance.directory) export const dispose = (key: string) => Effect.all( From 96b1d8f639991e896bc8d31afe64d6309bf3ccd2 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:21:50 +1000 Subject: [PATCH 11/48] fix(app): stabilize todo dock e2e with composer probe (#17267) --- packages/app/e2e/AGENTS.md | 19 ++ .../e2e/session/session-composer-dock.spec.ts | 236 +++++++++++++----- .../composer/session-composer-region.tsx | 9 +- .../composer/session-composer-state.ts | 55 +++- .../session/composer/session-todo-dock.tsx | 21 ++ packages/app/src/testing/session-composer.ts | 84 +++++++ 6 files changed, 350 insertions(+), 74 deletions(-) create mode 100644 packages/app/src/testing/session-composer.ts diff --git a/packages/app/e2e/AGENTS.md b/packages/app/e2e/AGENTS.md index cb8080fb252..4b62634f0bf 100644 --- a/packages/app/e2e/AGENTS.md +++ b/packages/app/e2e/AGENTS.md @@ -176,6 +176,25 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings - These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles. - Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks. +### Wait on state + +- Never use wall-clock waits like `page.waitForTimeout(...)` to make a test pass +- Avoid race-prone flows that assume work is finished after an action +- Wait or poll on observable state with `expect(...)`, `expect.poll(...)`, or existing helpers +- Prefer locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state, and reserve `expect.poll(...)` for probe, mock, or backend state + +### Add hooks + +- If required state is not observable from the UI, add a small test-only driver or probe in app code instead of sleeps or fragile DOM checks +- Keep these hooks minimal and purpose-built, following the style of `packages/app/src/testing/terminal.ts` +- Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony +- When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI + +### Prefer helpers + +- Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise +- Use direct locators when the interaction is simple and a helper would not add clarity + ## Writing New Tests 1. Choose appropriate folder or create new one diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts index 055e8eed292..ffaede92ac7 100644 --- a/packages/app/e2e/session/session-composer-dock.spec.ts +++ b/packages/app/e2e/session/session-composer-dock.spec.ts @@ -1,12 +1,11 @@ import { test, expect } from "../fixtures" -import { cleanupSession, clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions" +import { composerEvent, type ComposerDriverState, type ComposerProbeState, type ComposerWindow } from "../../src/testing/session-composer" +import { cleanupSession, clearSessionDockSeed, seedSessionQuestion } from "../actions" import { permissionDockSelector, promptSelector, questionDockSelector, sessionComposerDockSelector, - sessionTodoDockSelector, - sessionTodoListSelector, sessionTodoToggleButtonSelector, } from "../selectors" @@ -42,12 +41,8 @@ async function withDockSeed(sdk: Sdk, sessionID: string, fn: () => Promise async function clearPermissionDock(page: any, label: RegExp) { const dock = page.locator(permissionDockSelector) - for (let i = 0; i < 3; i++) { - const count = await dock.count() - if (count === 0) return - await dock.getByRole("button", { name: label }).click() - await page.waitForTimeout(150) - } + await expect(dock).toBeVisible() + await dock.getByRole("button", { name: label }).click() } async function setAutoAccept(page: any, enabled: boolean) { @@ -59,6 +54,120 @@ async function setAutoAccept(page: any, enabled: boolean) { await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false") } +async function expectQuestionBlocked(page: any) { + await expect(page.locator(questionDockSelector)).toBeVisible() + await expect(page.locator(promptSelector)).toHaveCount(0) +} + +async function expectQuestionOpen(page: any) { + await expect(page.locator(questionDockSelector)).toHaveCount(0) + await expect(page.locator(promptSelector)).toBeVisible() +} + +async function expectPermissionBlocked(page: any) { + await expect(page.locator(permissionDockSelector)).toBeVisible() + await expect(page.locator(promptSelector)).toHaveCount(0) +} + +async function expectPermissionOpen(page: any) { + await expect(page.locator(permissionDockSelector)).toHaveCount(0) + await expect(page.locator(promptSelector)).toBeVisible() +} + +async function todoDock(page: any, sessionID: string) { + await page.addInitScript(() => { + const win = window as ComposerWindow + win.__opencode_e2e = { + ...win.__opencode_e2e, + composer: { + enabled: true, + sessions: {}, + }, + } + }) + + const write = async (driver: ComposerDriverState | undefined) => { + await page.evaluate( + (input) => { + const win = window as ComposerWindow + const composer = win.__opencode_e2e?.composer + if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled") + composer.sessions ??= {} + const prev = composer.sessions[input.sessionID] ?? {} + if (!input.driver) { + if (!prev.probe) { + delete composer.sessions[input.sessionID] + } else { + composer.sessions[input.sessionID] = { probe: prev.probe } + } + } else { + composer.sessions[input.sessionID] = { + ...prev, + driver: input.driver, + } + } + window.dispatchEvent(new CustomEvent(input.event, { detail: { sessionID: input.sessionID } })) + }, + { event: composerEvent, sessionID, driver }, + ) + } + + const read = () => + page.evaluate((sessionID) => { + const win = window as ComposerWindow + return win.__opencode_e2e?.composer?.sessions?.[sessionID]?.probe ?? null + }, sessionID) as Promise + + const api = { + async clear() { + await write(undefined) + return api + }, + async open(todos: NonNullable) { + await write({ live: true, todos }) + return api + }, + async finish(todos: NonNullable) { + await write({ live: false, todos }) + return api + }, + async expectOpen(states: ComposerProbeState["states"]) { + await expect.poll(read, { timeout: 10_000 }).toMatchObject({ + mounted: true, + collapsed: false, + hidden: false, + count: states.length, + states, + }) + return api + }, + async expectCollapsed(states: ComposerProbeState["states"]) { + await expect.poll(read, { timeout: 10_000 }).toMatchObject({ + mounted: true, + collapsed: true, + hidden: true, + count: states.length, + states, + }) + return api + }, + async expectClosed() { + await expect.poll(read, { timeout: 10_000 }).toMatchObject({ mounted: false }) + return api + }, + async collapse() { + await page.locator(sessionTodoToggleButtonSelector).click() + return api + }, + async expand() { + await page.locator(sessionTodoToggleButtonSelector).click() + return api + }, + } + + return api +} + async function withMockPermission( page: any, request: { @@ -70,7 +179,7 @@ async function withMockPermission( always?: string[] }, opts: { child?: any } | undefined, - fn: () => Promise, + fn: (state: { resolved: () => Promise }) => Promise, ) { let pending = [ { @@ -119,8 +228,14 @@ async function withMockPermission( if (sessionList) await page.route("**/session?*", sessionList) + const state = { + async resolved() { + await expect.poll(() => pending.length, { timeout: 10_000 }).toBe(0) + }, + } + try { - return await fn() + return await fn(state) } finally { await page.unroute("**/permission", list) await page.unroute("**/session/*/permissions/*", reply) @@ -173,14 +288,12 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess }) const dock = page.locator(questionDockSelector) - await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) + await expectQuestionBlocked(page) await dock.locator('[data-slot="question-option"]').first().click() await dock.getByRole("button", { name: /submit/i }).click() - await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() + await expectQuestionOpen(page) }) }) }) @@ -199,15 +312,14 @@ test("blocked permission flow supports allow once", async ({ page, sdk, gotoSess metadata: { description: "Need permission for command" }, }, undefined, - async () => { + async (state) => { await page.goto(page.url()) - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) + await expectPermissionBlocked(page) await clearPermissionDock(page, /allow once/i) + await state.resolved() await page.goto(page.url()) - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() + await expectPermissionOpen(page) }, ) }) @@ -226,15 +338,14 @@ test("blocked permission flow supports reject", async ({ page, sdk, gotoSession patterns: ["/tmp/opencode-e2e-perm-reject"], }, undefined, - async () => { + async (state) => { await page.goto(page.url()) - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) + await expectPermissionBlocked(page) await clearPermissionDock(page, /deny/i) + await state.resolved() await page.goto(page.url()) - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() + await expectPermissionOpen(page) }, ) }) @@ -254,15 +365,14 @@ test("blocked permission flow supports allow always", async ({ page, sdk, gotoSe metadata: { description: "Need permission for command" }, }, undefined, - async () => { + async (state) => { await page.goto(page.url()) - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) + await expectPermissionBlocked(page) await clearPermissionDock(page, /allow always/i) + await state.resolved() await page.goto(page.url()) - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() + await expectPermissionOpen(page) }, ) }) @@ -301,14 +411,12 @@ test("child session question request blocks parent dock and unblocks after submi }) const dock = page.locator(questionDockSelector) - await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) + await expectQuestionBlocked(page) await dock.locator('[data-slot="question-option"]').first().click() await dock.getByRole("button", { name: /submit/i }).click() - await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() + await expectQuestionOpen(page) }) } finally { await cleanupSession({ sdk, sessionID: child.id }) @@ -344,17 +452,15 @@ test("child session permission request blocks parent dock and supports allow onc metadata: { description: "Need child permission" }, }, { child }, - async () => { + async (state) => { await page.goto(page.url()) - const dock = page.locator(permissionDockSelector) - await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) + await expectPermissionBlocked(page) await clearPermissionDock(page, /allow once/i) + await state.resolved() await page.goto(page.url()) - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() + await expectPermissionOpen(page) }, ) } finally { @@ -365,36 +471,31 @@ test("child session permission request blocks parent dock and supports allow onc test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSession }) => { await withDockSession(sdk, "e2e composer dock todo", async (session) => { - await withDockSeed(sdk, session.id, async () => { - await gotoSession(session.id) + const dock = await todoDock(page, session.id) + await gotoSession(session.id) + await expect(page.locator(sessionComposerDockSelector)).toBeVisible() - await seedSessionTodos(sdk, { - sessionID: session.id, - todos: [ - { content: "first task", status: "pending", priority: "high" }, - { content: "second task", status: "in_progress", priority: "medium" }, - ], - }) + try { + await dock.open([ + { content: "first task", status: "pending", priority: "high" }, + { content: "second task", status: "in_progress", priority: "medium" }, + ]) + await dock.expectOpen(["pending", "in_progress"]) - await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(sessionTodoListSelector)).toBeVisible() + await dock.collapse() + await dock.expectCollapsed(["pending", "in_progress"]) - await page.locator(sessionTodoToggleButtonSelector).click() - await expect(page.locator(sessionTodoListSelector)).toBeHidden() + await dock.expand() + await dock.expectOpen(["pending", "in_progress"]) - await page.locator(sessionTodoToggleButtonSelector).click() - await expect(page.locator(sessionTodoListSelector)).toBeVisible() - - await seedSessionTodos(sdk, { - sessionID: session.id, - todos: [ - { content: "first task", status: "completed", priority: "high" }, - { content: "second task", status: "cancelled", priority: "medium" }, - ], - }) - - await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(0) - }) + await dock.finish([ + { content: "first task", status: "completed", priority: "high" }, + { content: "second task", status: "cancelled", priority: "medium" }, + ]) + await dock.expectClosed() + } finally { + await dock.clear() + } }) }) @@ -414,8 +515,7 @@ test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSe ], }) - await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) + await expectQuestionBlocked(page) await page.locator("main").click({ position: { x: 5, y: 5 } }) await page.keyboard.type("abc") 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 2034fbead06..84f77ea4a3c 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -44,9 +44,9 @@ export function SessionComposerRegion(props: { }) { const prompt = usePrompt() const language = useLanguage() - const { sessionKey } = useSessionKey() + const route = useSessionKey() - const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt) + const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt) const previewPrompt = () => prompt @@ -62,7 +62,7 @@ export function SessionComposerRegion(props: { createEffect(() => { if (!prompt.ready()) return - setSessionHandoff(sessionKey(), { prompt: previewPrompt() }) + setSessionHandoff(route.sessionKey(), { prompt: previewPrompt() }) }) const [store, setStore] = createStore({ @@ -85,7 +85,7 @@ export function SessionComposerRegion(props: { } createEffect(() => { - sessionKey() + route.sessionKey() const ready = props.ready const delay = 140 @@ -194,6 +194,7 @@ export function SessionComposerRegion(props: { >
setStore("body", el)}> return !!permissionRequest() || !!questionRequest() }) + const [test, setTest] = createStore({ + on: false, + live: undefined as boolean | undefined, + todos: undefined as Todo[] | undefined, + }) + + const pull = () => { + const id = params.id + if (!id) { + setTest({ on: false, live: undefined, todos: undefined }) + return + } + + const next = composerDriver(id) + if (!next) { + setTest({ on: false, live: undefined, todos: undefined }) + return + } + + setTest({ + on: true, + live: next.live, + todos: next.todos?.map((todo) => ({ ...todo })), + }) + } + + onMount(() => { + if (!composerEnabled()) return + + pull() + createEffect(on(() => params.id, pull, { defer: true })) + + const onEvent = (event: Event) => { + const detail = (event as CustomEvent<{ sessionID?: string }>).detail + if (detail?.sessionID !== params.id) return + pull() + } + + window.addEventListener(composerEvent, onEvent) + onCleanup(() => window.removeEventListener(composerEvent, onEvent)) + }) + const todos = createMemo((): Todo[] => { + if (test.on && test.todos !== undefined) return test.todos const id = params.id if (!id) return [] return globalSync.data.session_todo[id] ?? [] @@ -64,7 +108,10 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => }) const busy = createMemo(() => status().type !== "idle") - const live = createMemo(() => busy() || blocked()) + const live = createMemo(() => { + if (test.on && test.live !== undefined) return test.live + return busy() || blocked() + }) const [store, setStore] = createStore({ responding: undefined as string | undefined, @@ -116,6 +163,10 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => // Keep stale turn todos from reopening if the model never clears them. const clear = () => { + if (test.on && test.todos !== undefined) { + setTest("todos", []) + return + } const id = params.id if (!id) return globalSync.todo.set(id, []) 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 c7907bb546d..5500de97a49 100644 --- a/packages/app/src/pages/session/composer/session-todo-dock.tsx +++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx @@ -8,6 +8,7 @@ import { TextReveal } from "@opencode-ai/ui/text-reveal" 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" function dot(status: Todo["status"]) { if (status !== "in_progress") return undefined @@ -35,6 +36,7 @@ function dot(status: Todo["status"]) { } export function SessionTodoDock(props: { + sessionID?: string todos: Todo[] title: string collapseLabel: string @@ -69,6 +71,8 @@ export function SessionTodoDock(props: { const off = createMemo(() => hide() > 0.98) const turn = createMemo(() => Math.max(0, Math.min(1, value()))) const full = createMemo(() => Math.max(78, store.height)) + const e2e = composerEnabled() + const probe = composerProbe(props.sessionID) let contentRef: HTMLDivElement | undefined createEffect(() => { @@ -83,6 +87,23 @@ export function SessionTodoDock(props: { onCleanup(() => observer.disconnect()) }) + createEffect(() => { + if (!e2e) return + + probe.set({ + mounted: true, + collapsed: store.collapsed, + hidden: store.collapsed || off(), + count: props.todos.length, + states: props.todos.map((todo) => todo.status), + }) + }) + + onCleanup(() => { + if (!e2e) return + probe.drop() + }) + return ( > +} + +export type ComposerProbeState = { + mounted: boolean + collapsed: boolean + hidden: boolean + count: number + states: Todo["status"][] +} + +type ComposerState = { + driver?: ComposerDriverState + probe?: ComposerProbeState +} + +export type ComposerWindow = Window & { + __opencode_e2e?: { + composer?: { + enabled?: boolean + sessions?: Record + } + } +} + +const clone = (driver: ComposerDriverState) => ({ + live: driver.live, + todos: driver.todos?.map((todo) => ({ ...todo })), +}) + +export const composerEnabled = () => { + if (typeof window === "undefined") return false + return (window as ComposerWindow).__opencode_e2e?.composer?.enabled === true +} + +const root = () => { + if (!composerEnabled()) return + const state = (window as ComposerWindow).__opencode_e2e?.composer + if (!state) return + state.sessions ??= {} + return state.sessions +} + +export const composerDriver = (sessionID?: string) => { + if (!sessionID) return + const state = root()?.[sessionID]?.driver + if (!state) return + return clone(state) +} + +export const composerProbe = (sessionID?: string) => { + const set = (next: ComposerProbeState) => { + if (!sessionID) return + const sessions = root() + if (!sessions) return + const prev = sessions[sessionID] ?? {} + sessions[sessionID] = { + ...prev, + probe: { + ...next, + states: [...next.states], + }, + } + } + + return { + set, + drop() { + set({ + mounted: false, + collapsed: false, + hidden: true, + count: 0, + states: [], + }) + }, + } +} From 092f654f63e6361a931d5bd30c1f64a063b3ec4a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 12 Mar 2026 22:22:16 -0400 Subject: [PATCH 12/48] fix(cli): hide console command from help output (#17277) --- packages/opencode/src/cli/cmd/account.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index 78aadca626b..8ad42c5eb1a 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -195,7 +195,7 @@ export const OrgsCommand = cmd({ export const ConsoleCommand = cmd({ command: "console", - describe: "manage console account", + describe: false, builder: (yargs) => yargs .command({ From 8f8c74cfb862b4fd92914f2137b64fefd544fe33 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 13 Mar 2026 02:33:45 +0000 Subject: [PATCH 13/48] chore: generate --- packages/app/e2e/session/session-composer-dock.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts index ffaede92ac7..5b2e8a8c6f1 100644 --- a/packages/app/e2e/session/session-composer-dock.spec.ts +++ b/packages/app/e2e/session/session-composer-dock.spec.ts @@ -1,5 +1,10 @@ import { test, expect } from "../fixtures" -import { composerEvent, type ComposerDriverState, type ComposerProbeState, type ComposerWindow } from "../../src/testing/session-composer" +import { + composerEvent, + type ComposerDriverState, + type ComposerProbeState, + type ComposerWindow, +} from "../../src/testing/session-composer" import { cleanupSession, clearSessionDockSeed, seedSessionQuestion } from "../actions" import { permissionDockSelector, From 4a2a046d79ab8a0f2c152b6af4fd637d33843c9b Mon Sep 17 00:00:00 2001 From: Tom Ryder Date: Thu, 12 Mar 2026 20:13:09 -0700 Subject: [PATCH 14/48] fix: filter empty content blocks for Bedrock provider (#14586) --- packages/opencode/src/provider/transform.ts | 2 +- .../opencode/test/provider/transform.test.ts | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 407c2687825..05b9f031fe6 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -51,7 +51,7 @@ export namespace ProviderTransform { ): ModelMessage[] { // Anthropic rejects messages with empty content - filter out empty string messages // and remove empty text/reasoning parts from array content - if (model.api.npm === "@ai-sdk/anthropic") { + if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/amazon-bedrock") { msgs = msgs .map((msg) => { if (typeof msg.content === "string") { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index ecb3a74804c..917d357eafa 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1096,6 +1096,38 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => expect(result[0].content[1]).toEqual({ type: "text", text: "Result" }) }) + test("filters empty content for bedrock provider", () => { + const bedrockModel = { + ...anthropicModel, + id: "amazon-bedrock/anthropic.claude-opus-4-6", + providerID: "amazon-bedrock", + api: { + id: "anthropic.claude-opus-4-6", + url: "https://bedrock-runtime.us-east-1.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + } + + const msgs = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "" }, + { + role: "assistant", + content: [ + { type: "text", text: "" }, + { type: "text", text: "Answer" }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, bedrockModel, {}) + + expect(result).toHaveLength(2) + expect(result[0].content).toBe("Hello") + expect(result[1].content).toHaveLength(1) + expect(result[1].content[0]).toEqual({ type: "text", text: "Answer" }) + }) + test("does not filter for non-anthropic providers", () => { const openaiModel = { ...anthropicModel, From 55766622001b0c1e048c93c06cb3f7a0a3e9fd94 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:21:02 -0500 Subject: [PATCH 15/48] feat(app): missing themes (#17275) --- .gitignore | 2 +- packages/ui/src/theme/default-themes.ts | 119 +++++++++++++----- packages/ui/src/theme/index.ts | 41 ++++-- .../src/theme/themes/catppuccin-frappe.json | 85 +++++++++++++ .../theme/themes/catppuccin-macchiato.json | 85 +++++++++++++ packages/ui/src/theme/themes/cobalt2.json | 87 +++++++++++++ packages/ui/src/theme/themes/cursor.json | 91 ++++++++++++++ packages/ui/src/theme/themes/everforest.json | 89 +++++++++++++ packages/ui/src/theme/themes/flexoki.json | 86 +++++++++++++ packages/ui/src/theme/themes/github.json | 85 +++++++++++++ packages/ui/src/theme/themes/kanagawa.json | 89 +++++++++++++ packages/ui/src/theme/themes/lucent-orng.json | 87 +++++++++++++ packages/ui/src/theme/themes/material.json | 87 +++++++++++++ packages/ui/src/theme/themes/matrix.json | 91 ++++++++++++++ packages/ui/src/theme/themes/mercury.json | 86 +++++++++++++ packages/ui/src/theme/themes/one-dark.json | 89 +++++++++++++ packages/ui/src/theme/themes/opencode.json | 89 +++++++++++++ packages/ui/src/theme/themes/orng.json | 87 +++++++++++++ packages/ui/src/theme/themes/osaka-jade.json | 88 +++++++++++++ packages/ui/src/theme/themes/palenight.json | 85 +++++++++++++ packages/ui/src/theme/themes/rosepine.json | 85 +++++++++++++ packages/ui/src/theme/themes/synthwave84.json | 87 +++++++++++++ packages/ui/src/theme/themes/vercel.json | 90 +++++++++++++ packages/ui/src/theme/themes/zenburn.json | 87 +++++++++++++ 24 files changed, 1960 insertions(+), 37 deletions(-) create mode 100644 packages/ui/src/theme/themes/catppuccin-frappe.json create mode 100644 packages/ui/src/theme/themes/catppuccin-macchiato.json create mode 100644 packages/ui/src/theme/themes/cobalt2.json create mode 100644 packages/ui/src/theme/themes/cursor.json create mode 100644 packages/ui/src/theme/themes/everforest.json create mode 100644 packages/ui/src/theme/themes/flexoki.json create mode 100644 packages/ui/src/theme/themes/github.json create mode 100644 packages/ui/src/theme/themes/kanagawa.json create mode 100644 packages/ui/src/theme/themes/lucent-orng.json create mode 100644 packages/ui/src/theme/themes/material.json create mode 100644 packages/ui/src/theme/themes/matrix.json create mode 100644 packages/ui/src/theme/themes/mercury.json create mode 100644 packages/ui/src/theme/themes/one-dark.json create mode 100644 packages/ui/src/theme/themes/opencode.json create mode 100644 packages/ui/src/theme/themes/orng.json create mode 100644 packages/ui/src/theme/themes/osaka-jade.json create mode 100644 packages/ui/src/theme/themes/palenight.json create mode 100644 packages/ui/src/theme/themes/rosepine.json create mode 100644 packages/ui/src/theme/themes/synthwave84.json create mode 100644 packages/ui/src/theme/themes/vercel.json create mode 100644 packages/ui/src/theme/themes/zenburn.json diff --git a/.gitignore b/.gitignore index bf78c046d4b..c287d91ac12 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ ts-dist /result refs Session.vim -opencode.json +/opencode.json a.out target .scripts diff --git a/packages/ui/src/theme/default-themes.ts b/packages/ui/src/theme/default-themes.ts index 4b5601b1c23..c1419895581 100644 --- a/packages/ui/src/theme/default-themes.ts +++ b/packages/ui/src/theme/default-themes.ts @@ -1,37 +1,79 @@ import type { DesktopTheme } from "./types" import oc2ThemeJson from "./themes/oc-2.json" -import tokyoThemeJson from "./themes/tokyonight.json" -import draculaThemeJson from "./themes/dracula.json" -import monokaiThemeJson from "./themes/monokai.json" -import solarizedThemeJson from "./themes/solarized.json" -import nordThemeJson from "./themes/nord.json" -import catppuccinThemeJson from "./themes/catppuccin.json" -import ayuThemeJson from "./themes/ayu.json" -import oneDarkProThemeJson from "./themes/onedarkpro.json" -import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json" -import nightowlThemeJson from "./themes/nightowl.json" -import vesperThemeJson from "./themes/vesper.json" -import carbonfoxThemeJson from "./themes/carbonfox.json" -import gruvboxThemeJson from "./themes/gruvbox.json" -import auraThemeJson from "./themes/aura.json" import amoledThemeJson from "./themes/amoled.json" +import auraThemeJson from "./themes/aura.json" +import ayuThemeJson from "./themes/ayu.json" +import carbonfoxThemeJson from "./themes/carbonfox.json" +import catppuccinThemeJson from "./themes/catppuccin.json" +import catppuccinFrappeThemeJson from "./themes/catppuccin-frappe.json" +import catppuccinMacchiatoThemeJson from "./themes/catppuccin-macchiato.json" +import cobalt2ThemeJson from "./themes/cobalt2.json" +import cursorThemeJson from "./themes/cursor.json" +import draculaThemeJson from "./themes/dracula.json" +import everforestThemeJson from "./themes/everforest.json" +import flexokiThemeJson from "./themes/flexoki.json" +import githubThemeJson from "./themes/github.json" +import gruvboxThemeJson from "./themes/gruvbox.json" +import kanagawaThemeJson from "./themes/kanagawa.json" +import lucentOrngThemeJson from "./themes/lucent-orng.json" +import materialThemeJson from "./themes/material.json" +import matrixThemeJson from "./themes/matrix.json" +import mercuryThemeJson from "./themes/mercury.json" +import monokaiThemeJson from "./themes/monokai.json" +import nightowlThemeJson from "./themes/nightowl.json" +import nordThemeJson from "./themes/nord.json" +import oneDarkThemeJson from "./themes/one-dark.json" +import oneDarkProThemeJson from "./themes/onedarkpro.json" +import opencodeThemeJson from "./themes/opencode.json" +import orngThemeJson from "./themes/orng.json" +import osakaJadeThemeJson from "./themes/osaka-jade.json" +import palenightThemeJson from "./themes/palenight.json" +import rosepineThemeJson from "./themes/rosepine.json" +import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json" +import solarizedThemeJson from "./themes/solarized.json" +import synthwave84ThemeJson from "./themes/synthwave84.json" +import tokyonightThemeJson from "./themes/tokyonight.json" +import vercelThemeJson from "./themes/vercel.json" +import vesperThemeJson from "./themes/vesper.json" +import zenburnThemeJson from "./themes/zenburn.json" export const oc2Theme = oc2ThemeJson as DesktopTheme -export const tokyonightTheme = tokyoThemeJson as DesktopTheme -export const draculaTheme = draculaThemeJson as DesktopTheme -export const monokaiTheme = monokaiThemeJson as DesktopTheme -export const solarizedTheme = solarizedThemeJson as DesktopTheme -export const nordTheme = nordThemeJson as DesktopTheme -export const catppuccinTheme = catppuccinThemeJson as DesktopTheme -export const ayuTheme = ayuThemeJson as DesktopTheme -export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme -export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme -export const nightowlTheme = nightowlThemeJson as DesktopTheme -export const vesperTheme = vesperThemeJson as DesktopTheme -export const carbonfoxTheme = carbonfoxThemeJson as DesktopTheme -export const gruvboxTheme = gruvboxThemeJson as DesktopTheme -export const auraTheme = auraThemeJson as DesktopTheme export const amoledTheme = amoledThemeJson as DesktopTheme +export const auraTheme = auraThemeJson as DesktopTheme +export const ayuTheme = ayuThemeJson as DesktopTheme +export const carbonfoxTheme = carbonfoxThemeJson as DesktopTheme +export const catppuccinTheme = catppuccinThemeJson as DesktopTheme +export const catppuccinFrappeTheme = catppuccinFrappeThemeJson as DesktopTheme +export const catppuccinMacchiatoTheme = catppuccinMacchiatoThemeJson as DesktopTheme +export const cobalt2Theme = cobalt2ThemeJson as DesktopTheme +export const cursorTheme = cursorThemeJson as DesktopTheme +export const draculaTheme = draculaThemeJson as DesktopTheme +export const everforestTheme = everforestThemeJson as DesktopTheme +export const flexokiTheme = flexokiThemeJson as DesktopTheme +export const githubTheme = githubThemeJson as DesktopTheme +export const gruvboxTheme = gruvboxThemeJson as DesktopTheme +export const kanagawaTheme = kanagawaThemeJson as DesktopTheme +export const lucentOrngTheme = lucentOrngThemeJson as DesktopTheme +export const materialTheme = materialThemeJson as DesktopTheme +export const matrixTheme = matrixThemeJson as DesktopTheme +export const mercuryTheme = mercuryThemeJson as DesktopTheme +export const monokaiTheme = monokaiThemeJson as DesktopTheme +export const nightowlTheme = nightowlThemeJson as DesktopTheme +export const nordTheme = nordThemeJson as DesktopTheme +export const oneDarkTheme = oneDarkThemeJson as DesktopTheme +export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme +export const opencodeTheme = opencodeThemeJson as DesktopTheme +export const orngTheme = orngThemeJson as DesktopTheme +export const osakaJadeTheme = osakaJadeThemeJson as DesktopTheme +export const palenightTheme = palenightThemeJson as DesktopTheme +export const rosepineTheme = rosepineThemeJson as DesktopTheme +export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme +export const solarizedTheme = solarizedThemeJson as DesktopTheme +export const synthwave84Theme = synthwave84ThemeJson as DesktopTheme +export const tokyonightTheme = tokyonightThemeJson as DesktopTheme +export const vercelTheme = vercelThemeJson as DesktopTheme +export const vesperTheme = vesperThemeJson as DesktopTheme +export const zenburnTheme = zenburnThemeJson as DesktopTheme export const DEFAULT_THEMES: Record = { "oc-2": oc2Theme, @@ -40,14 +82,35 @@ export const DEFAULT_THEMES: Record = { ayu: ayuTheme, carbonfox: carbonfoxTheme, catppuccin: catppuccinTheme, + "catppuccin-frappe": catppuccinFrappeTheme, + "catppuccin-macchiato": catppuccinMacchiatoTheme, + cobalt2: cobalt2Theme, + cursor: cursorTheme, dracula: draculaTheme, + everforest: everforestTheme, + flexoki: flexokiTheme, + github: githubTheme, gruvbox: gruvboxTheme, + kanagawa: kanagawaTheme, + "lucent-orng": lucentOrngTheme, + material: materialTheme, + matrix: matrixTheme, + mercury: mercuryTheme, monokai: monokaiTheme, nightowl: nightowlTheme, nord: nordTheme, + "one-dark": oneDarkTheme, onedarkpro: oneDarkProTheme, + opencode: opencodeTheme, + orng: orngTheme, + "osaka-jade": osakaJadeTheme, + palenight: palenightTheme, + rosepine: rosepineTheme, shadesofpurple: shadesOfPurpleTheme, solarized: solarizedTheme, + synthwave84: synthwave84Theme, tokyonight: tokyonightTheme, + vercel: vercelTheme, vesper: vesperTheme, + zenburn: zenburnTheme, } diff --git a/packages/ui/src/theme/index.ts b/packages/ui/src/theme/index.ts index bfd55e60b81..86d30eab813 100644 --- a/packages/ui/src/theme/index.ts +++ b/packages/ui/src/theme/index.ts @@ -36,15 +36,40 @@ export { ThemeProvider, useTheme, type ColorScheme } from "./context" export { DEFAULT_THEMES, oc2Theme, - tokyonightTheme, - draculaTheme, - monokaiTheme, - solarizedTheme, - nordTheme, - catppuccinTheme, + amoledTheme, + auraTheme, ayuTheme, - oneDarkProTheme, - shadesOfPurpleTheme, + carbonfoxTheme, + catppuccinTheme, + catppuccinFrappeTheme, + catppuccinMacchiatoTheme, + cobalt2Theme, + cursorTheme, + draculaTheme, + everforestTheme, + flexokiTheme, + githubTheme, + gruvboxTheme, + kanagawaTheme, + lucentOrngTheme, + materialTheme, + matrixTheme, + mercuryTheme, + monokaiTheme, nightowlTheme, + nordTheme, + oneDarkTheme, + oneDarkProTheme, + opencodeTheme, + orngTheme, + osakaJadeTheme, + palenightTheme, + rosepineTheme, + shadesOfPurpleTheme, + solarizedTheme, + synthwave84Theme, + tokyonightTheme, + vercelTheme, vesperTheme, + zenburnTheme, } from "./default-themes" diff --git a/packages/ui/src/theme/themes/catppuccin-frappe.json b/packages/ui/src/theme/themes/catppuccin-frappe.json new file mode 100644 index 00000000000..ea02b981de1 --- /dev/null +++ b/packages/ui/src/theme/themes/catppuccin-frappe.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Catppuccin Frappe", + "id": "catppuccin-frappe", + "light": { + "palette": { + "neutral": "#303446", + "ink": "#c6d0f5", + "primary": "#8da4e2", + "accent": "#f4b8e4", + "success": "#a6d189", + "warning": "#e5c890", + "error": "#e78284", + "info": "#81c8be" + }, + "overrides": { + "text-weak": "#b5bfe2", + "syntax-comment": "#949cb8", + "syntax-keyword": "#ca9ee6", + "syntax-string": "#a6d189", + "syntax-primitive": "#8da4e2", + "syntax-variable": "#e78284", + "syntax-property": "#99d1db", + "syntax-type": "#e5c890", + "syntax-constant": "#ef9f76", + "syntax-operator": "#99d1db", + "syntax-punctuation": "#c6d0f5", + "syntax-object": "#e78284", + "markdown-heading": "#ca9ee6", + "markdown-text": "#c6d0f5", + "markdown-link": "#8da4e2", + "markdown-link-text": "#99d1db", + "markdown-code": "#a6d189", + "markdown-block-quote": "#e5c890", + "markdown-emph": "#e5c890", + "markdown-strong": "#ef9f76", + "markdown-horizontal-rule": "#a5adce", + "markdown-list-item": "#8da4e2", + "markdown-list-enumeration": "#99d1db", + "markdown-image": "#8da4e2", + "markdown-image-text": "#99d1db", + "markdown-code-block": "#c6d0f5" + } + }, + "dark": { + "palette": { + "neutral": "#303446", + "ink": "#c6d0f5", + "primary": "#8da4e2", + "accent": "#f4b8e4", + "success": "#a6d189", + "warning": "#e5c890", + "error": "#e78284", + "info": "#81c8be" + }, + "overrides": { + "text-weak": "#b5bfe2", + "syntax-comment": "#949cb8", + "syntax-keyword": "#ca9ee6", + "syntax-string": "#a6d189", + "syntax-primitive": "#8da4e2", + "syntax-variable": "#e78284", + "syntax-property": "#99d1db", + "syntax-type": "#e5c890", + "syntax-constant": "#ef9f76", + "syntax-operator": "#99d1db", + "syntax-punctuation": "#c6d0f5", + "syntax-object": "#e78284", + "markdown-heading": "#ca9ee6", + "markdown-text": "#c6d0f5", + "markdown-link": "#8da4e2", + "markdown-link-text": "#99d1db", + "markdown-code": "#a6d189", + "markdown-block-quote": "#e5c890", + "markdown-emph": "#e5c890", + "markdown-strong": "#ef9f76", + "markdown-horizontal-rule": "#a5adce", + "markdown-list-item": "#8da4e2", + "markdown-list-enumeration": "#99d1db", + "markdown-image": "#8da4e2", + "markdown-image-text": "#99d1db", + "markdown-code-block": "#c6d0f5" + } + } +} diff --git a/packages/ui/src/theme/themes/catppuccin-macchiato.json b/packages/ui/src/theme/themes/catppuccin-macchiato.json new file mode 100644 index 00000000000..9315e154208 --- /dev/null +++ b/packages/ui/src/theme/themes/catppuccin-macchiato.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Catppuccin Macchiato", + "id": "catppuccin-macchiato", + "light": { + "palette": { + "neutral": "#24273a", + "ink": "#cad3f5", + "primary": "#8aadf4", + "accent": "#f5bde6", + "success": "#a6da95", + "warning": "#eed49f", + "error": "#ed8796", + "info": "#8bd5ca" + }, + "overrides": { + "text-weak": "#b8c0e0", + "syntax-comment": "#939ab7", + "syntax-keyword": "#c6a0f6", + "syntax-string": "#a6da95", + "syntax-primitive": "#8aadf4", + "syntax-variable": "#ed8796", + "syntax-property": "#91d7e3", + "syntax-type": "#eed49f", + "syntax-constant": "#f5a97f", + "syntax-operator": "#91d7e3", + "syntax-punctuation": "#cad3f5", + "syntax-object": "#ed8796", + "markdown-heading": "#c6a0f6", + "markdown-text": "#cad3f5", + "markdown-link": "#8aadf4", + "markdown-link-text": "#91d7e3", + "markdown-code": "#a6da95", + "markdown-block-quote": "#eed49f", + "markdown-emph": "#eed49f", + "markdown-strong": "#f5a97f", + "markdown-horizontal-rule": "#a5adcb", + "markdown-list-item": "#8aadf4", + "markdown-list-enumeration": "#91d7e3", + "markdown-image": "#8aadf4", + "markdown-image-text": "#91d7e3", + "markdown-code-block": "#cad3f5" + } + }, + "dark": { + "palette": { + "neutral": "#24273a", + "ink": "#cad3f5", + "primary": "#8aadf4", + "accent": "#f5bde6", + "success": "#a6da95", + "warning": "#eed49f", + "error": "#ed8796", + "info": "#8bd5ca" + }, + "overrides": { + "text-weak": "#b8c0e0", + "syntax-comment": "#939ab7", + "syntax-keyword": "#c6a0f6", + "syntax-string": "#a6da95", + "syntax-primitive": "#8aadf4", + "syntax-variable": "#ed8796", + "syntax-property": "#91d7e3", + "syntax-type": "#eed49f", + "syntax-constant": "#f5a97f", + "syntax-operator": "#91d7e3", + "syntax-punctuation": "#cad3f5", + "syntax-object": "#ed8796", + "markdown-heading": "#c6a0f6", + "markdown-text": "#cad3f5", + "markdown-link": "#8aadf4", + "markdown-link-text": "#91d7e3", + "markdown-code": "#a6da95", + "markdown-block-quote": "#eed49f", + "markdown-emph": "#eed49f", + "markdown-strong": "#f5a97f", + "markdown-horizontal-rule": "#a5adcb", + "markdown-list-item": "#8aadf4", + "markdown-list-enumeration": "#91d7e3", + "markdown-image": "#8aadf4", + "markdown-image-text": "#91d7e3", + "markdown-code-block": "#cad3f5" + } + } +} diff --git a/packages/ui/src/theme/themes/cobalt2.json b/packages/ui/src/theme/themes/cobalt2.json new file mode 100644 index 00000000000..56ff3b5041c --- /dev/null +++ b/packages/ui/src/theme/themes/cobalt2.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Cobalt2", + "id": "cobalt2", + "light": { + "palette": { + "neutral": "#ffffff", + "ink": "#193549", + "primary": "#0066cc", + "accent": "#00acc1", + "success": "#4caf50", + "warning": "#ff9800", + "error": "#e91e63", + "info": "#ff5722" + }, + "overrides": { + "text-weak": "#5c6b7d", + "syntax-comment": "#5c6b7d", + "syntax-keyword": "#ff5722", + "syntax-string": "#4caf50", + "syntax-primitive": "#ff9800", + "syntax-variable": "#193549", + "syntax-property": "#00acc1", + "syntax-type": "#00acc1", + "syntax-constant": "#e91e63", + "syntax-operator": "#ff5722", + "syntax-punctuation": "#193549", + "syntax-object": "#193549", + "markdown-heading": "#ff9800", + "markdown-text": "#193549", + "markdown-link": "#0066cc", + "markdown-link-text": "#00acc1", + "markdown-code": "#4caf50", + "markdown-block-quote": "#5c6b7d", + "markdown-emph": "#ff5722", + "markdown-strong": "#e91e63", + "markdown-horizontal-rule": "#d3dae3", + "markdown-list-item": "#0066cc", + "markdown-list-enumeration": "#00acc1", + "markdown-image": "#0066cc", + "markdown-image-text": "#00acc1", + "markdown-code-block": "#193549" + } + }, + "dark": { + "palette": { + "neutral": "#193549", + "ink": "#ffffff", + "primary": "#0088ff", + "accent": "#2affdf", + "success": "#9eff80", + "warning": "#ffc600", + "error": "#ff0088", + "info": "#ff9d00", + "diffAdd": "#b9ff9f", + "diffDelete": "#ff5fb3" + }, + "overrides": { + "text-weak": "#adb7c9", + "syntax-comment": "#0088ff", + "syntax-keyword": "#ff9d00", + "syntax-string": "#9eff80", + "syntax-primitive": "#ffc600", + "syntax-variable": "#ffffff", + "syntax-property": "#2affdf", + "syntax-type": "#2affdf", + "syntax-constant": "#ff628c", + "syntax-operator": "#ff9d00", + "syntax-punctuation": "#ffffff", + "syntax-object": "#ffffff", + "markdown-heading": "#ffc600", + "markdown-text": "#ffffff", + "markdown-link": "#0088ff", + "markdown-link-text": "#2affdf", + "markdown-code": "#9eff80", + "markdown-block-quote": "#adb7c9", + "markdown-emph": "#ff9d00", + "markdown-strong": "#ff628c", + "markdown-horizontal-rule": "#2d5a7b", + "markdown-list-item": "#0088ff", + "markdown-list-enumeration": "#2affdf", + "markdown-image": "#0088ff", + "markdown-image-text": "#2affdf", + "markdown-code-block": "#ffffff" + } + } +} diff --git a/packages/ui/src/theme/themes/cursor.json b/packages/ui/src/theme/themes/cursor.json new file mode 100644 index 00000000000..cbea5687075 --- /dev/null +++ b/packages/ui/src/theme/themes/cursor.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Cursor", + "id": "cursor", + "light": { + "palette": { + "neutral": "#fcfcfc", + "ink": "#141414", + "primary": "#6f9ba6", + "accent": "#6f9ba6", + "success": "#1f8a65", + "warning": "#db704b", + "error": "#cf2d56", + "info": "#3c7cab", + "interactive": "#206595", + "diffAdd": "#55a583", + "diffDelete": "#e75e78" + }, + "overrides": { + "text-weak": "#141414ad", + "syntax-comment": "#141414ad", + "syntax-keyword": "#b3003f", + "syntax-string": "#9e94d5", + "syntax-primitive": "#db704b", + "syntax-variable": "#141414", + "syntax-property": "#141414ad", + "syntax-type": "#206595", + "syntax-constant": "#b8448b", + "syntax-operator": "#141414", + "syntax-punctuation": "#141414", + "syntax-object": "#141414", + "markdown-heading": "#206595", + "markdown-text": "#141414", + "markdown-link": "#206595", + "markdown-link-text": "#141414ad", + "markdown-code": "#1f8a65", + "markdown-block-quote": "#141414ad", + "markdown-emph": "#141414", + "markdown-strong": "#141414", + "markdown-horizontal-rule": "#141414ad", + "markdown-list-item": "#141414", + "markdown-list-enumeration": "#141414ad", + "markdown-image": "#206595", + "markdown-image-text": "#141414ad", + "markdown-code-block": "#141414" + } + }, + "dark": { + "palette": { + "neutral": "#181818", + "ink": "#e4e4e4", + "primary": "#88c0d0", + "accent": "#88c0d0", + "success": "#3fa266", + "warning": "#f1b467", + "error": "#e34671", + "info": "#81a1c1", + "interactive": "#82D2CE", + "diffAdd": "#70b489", + "diffDelete": "#fc6b83" + }, + "overrides": { + "text-weak": "#e4e4e45e", + "syntax-comment": "#e4e4e45e", + "syntax-keyword": "#82D2CE", + "syntax-string": "#E394DC", + "syntax-primitive": "#EFB080", + "syntax-variable": "#e4e4e4", + "syntax-property": "#81a1c1", + "syntax-type": "#EFB080", + "syntax-constant": "#F8C762", + "syntax-operator": "#e4e4e4", + "syntax-punctuation": "#e4e4e4", + "syntax-object": "#e4e4e4", + "markdown-heading": "#AAA0FA", + "markdown-text": "#e4e4e4", + "markdown-link": "#82D2CE", + "markdown-link-text": "#81a1c1", + "markdown-code": "#E394DC", + "markdown-block-quote": "#e4e4e45e", + "markdown-emph": "#82D2CE", + "markdown-strong": "#F8C762", + "markdown-horizontal-rule": "#e4e4e45e", + "markdown-list-item": "#e4e4e4", + "markdown-list-enumeration": "#88c0d0", + "markdown-image": "#88c0d0", + "markdown-image-text": "#81a1c1", + "markdown-code-block": "#e4e4e4" + } + } +} diff --git a/packages/ui/src/theme/themes/everforest.json b/packages/ui/src/theme/themes/everforest.json new file mode 100644 index 00000000000..21c04c8ab38 --- /dev/null +++ b/packages/ui/src/theme/themes/everforest.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Everforest", + "id": "everforest", + "light": { + "palette": { + "neutral": "#fdf6e3", + "ink": "#5c6a72", + "primary": "#8da101", + "accent": "#df69ba", + "success": "#8da101", + "warning": "#f57d26", + "error": "#f85552", + "info": "#35a77c", + "diffAdd": "#4db380", + "diffDelete": "#f52a65" + }, + "overrides": { + "text-weak": "#a6b0a0", + "syntax-comment": "#a6b0a0", + "syntax-keyword": "#df69ba", + "syntax-string": "#8da101", + "syntax-primitive": "#8da101", + "syntax-variable": "#f85552", + "syntax-property": "#35a77c", + "syntax-type": "#dfa000", + "syntax-constant": "#f57d26", + "syntax-operator": "#35a77c", + "syntax-punctuation": "#5c6a72", + "syntax-object": "#f85552", + "markdown-heading": "#df69ba", + "markdown-text": "#5c6a72", + "markdown-link": "#8da101", + "markdown-link-text": "#35a77c", + "markdown-code": "#8da101", + "markdown-block-quote": "#dfa000", + "markdown-emph": "#dfa000", + "markdown-strong": "#f57d26", + "markdown-horizontal-rule": "#a6b0a0", + "markdown-list-item": "#8da101", + "markdown-list-enumeration": "#35a77c", + "markdown-image": "#8da101", + "markdown-image-text": "#35a77c", + "markdown-code-block": "#5c6a72" + } + }, + "dark": { + "palette": { + "neutral": "#2d353b", + "ink": "#d3c6aa", + "primary": "#a7c080", + "accent": "#d699b6", + "success": "#a7c080", + "warning": "#e69875", + "error": "#e67e80", + "info": "#83c092", + "diffAdd": "#b8db87", + "diffDelete": "#e26a75" + }, + "overrides": { + "text-weak": "#7a8478", + "syntax-comment": "#7a8478", + "syntax-keyword": "#d699b6", + "syntax-string": "#a7c080", + "syntax-primitive": "#a7c080", + "syntax-variable": "#e67e80", + "syntax-property": "#83c092", + "syntax-type": "#dbbc7f", + "syntax-constant": "#e69875", + "syntax-operator": "#83c092", + "syntax-punctuation": "#d3c6aa", + "syntax-object": "#e67e80", + "markdown-heading": "#d699b6", + "markdown-text": "#d3c6aa", + "markdown-link": "#a7c080", + "markdown-link-text": "#83c092", + "markdown-code": "#a7c080", + "markdown-block-quote": "#dbbc7f", + "markdown-emph": "#dbbc7f", + "markdown-strong": "#e69875", + "markdown-horizontal-rule": "#7a8478", + "markdown-list-item": "#a7c080", + "markdown-list-enumeration": "#83c092", + "markdown-image": "#a7c080", + "markdown-image-text": "#83c092", + "markdown-code-block": "#d3c6aa" + } + } +} diff --git a/packages/ui/src/theme/themes/flexoki.json b/packages/ui/src/theme/themes/flexoki.json new file mode 100644 index 00000000000..14768181830 --- /dev/null +++ b/packages/ui/src/theme/themes/flexoki.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Flexoki", + "id": "flexoki", + "light": { + "palette": { + "neutral": "#FFFCF0", + "ink": "#100F0F", + "primary": "#205EA6", + "accent": "#BC5215", + "success": "#66800B", + "warning": "#BC5215", + "error": "#AF3029", + "info": "#24837B" + }, + "overrides": { + "text-weak": "#6F6E69", + "syntax-comment": "#6F6E69", + "syntax-keyword": "#66800B", + "syntax-string": "#24837B", + "syntax-primitive": "#BC5215", + "syntax-variable": "#205EA6", + "syntax-property": "#24837B", + "syntax-type": "#AD8301", + "syntax-constant": "#5E409D", + "syntax-operator": "#6F6E69", + "syntax-punctuation": "#6F6E69", + "syntax-object": "#205EA6", + "markdown-heading": "#5E409D", + "markdown-text": "#100F0F", + "markdown-link": "#205EA6", + "markdown-link-text": "#24837B", + "markdown-code": "#24837B", + "markdown-block-quote": "#AD8301", + "markdown-emph": "#AD8301", + "markdown-strong": "#BC5215", + "markdown-horizontal-rule": "#6F6E69", + "markdown-list-item": "#BC5215", + "markdown-list-enumeration": "#24837B", + "markdown-image": "#A02F6F", + "markdown-image-text": "#24837B", + "markdown-code-block": "#100F0F" + } + }, + "dark": { + "palette": { + "neutral": "#100F0F", + "ink": "#CECDC3", + "primary": "#DA702C", + "accent": "#8B7EC8", + "success": "#879A39", + "warning": "#DA702C", + "error": "#D14D41", + "info": "#3AA99F", + "interactive": "#4385BE" + }, + "overrides": { + "text-weak": "#6F6E69", + "syntax-comment": "#6F6E69", + "syntax-keyword": "#879A39", + "syntax-string": "#3AA99F", + "syntax-primitive": "#DA702C", + "syntax-variable": "#4385BE", + "syntax-property": "#3AA99F", + "syntax-type": "#D0A215", + "syntax-constant": "#8B7EC8", + "syntax-operator": "#B7B5AC", + "syntax-punctuation": "#B7B5AC", + "syntax-object": "#4385BE", + "markdown-heading": "#8B7EC8", + "markdown-text": "#CECDC3", + "markdown-link": "#4385BE", + "markdown-link-text": "#3AA99F", + "markdown-code": "#3AA99F", + "markdown-block-quote": "#D0A215", + "markdown-emph": "#D0A215", + "markdown-strong": "#DA702C", + "markdown-horizontal-rule": "#6F6E69", + "markdown-list-item": "#DA702C", + "markdown-list-enumeration": "#3AA99F", + "markdown-image": "#CE5D97", + "markdown-image-text": "#3AA99F", + "markdown-code-block": "#CECDC3" + } + } +} diff --git a/packages/ui/src/theme/themes/github.json b/packages/ui/src/theme/themes/github.json new file mode 100644 index 00000000000..8cdf54f143e --- /dev/null +++ b/packages/ui/src/theme/themes/github.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "GitHub", + "id": "github", + "light": { + "palette": { + "neutral": "#ffffff", + "ink": "#24292f", + "primary": "#0969da", + "accent": "#1b7c83", + "success": "#1a7f37", + "warning": "#9a6700", + "error": "#cf222e", + "info": "#bc4c00" + }, + "overrides": { + "text-weak": "#57606a", + "syntax-comment": "#57606a", + "syntax-keyword": "#cf222e", + "syntax-string": "#0969da", + "syntax-primitive": "#8250df", + "syntax-variable": "#bc4c00", + "syntax-property": "#1b7c83", + "syntax-type": "#bc4c00", + "syntax-constant": "#1b7c83", + "syntax-operator": "#cf222e", + "syntax-punctuation": "#24292f", + "syntax-object": "#bc4c00", + "markdown-heading": "#0969da", + "markdown-text": "#24292f", + "markdown-link": "#0969da", + "markdown-link-text": "#1b7c83", + "markdown-code": "#bf3989", + "markdown-block-quote": "#57606a", + "markdown-emph": "#9a6700", + "markdown-strong": "#bc4c00", + "markdown-horizontal-rule": "#d0d7de", + "markdown-list-item": "#0969da", + "markdown-list-enumeration": "#1b7c83", + "markdown-image": "#0969da", + "markdown-image-text": "#1b7c83", + "markdown-code-block": "#24292f" + } + }, + "dark": { + "palette": { + "neutral": "#0d1117", + "ink": "#c9d1d9", + "primary": "#58a6ff", + "accent": "#39c5cf", + "success": "#3fb950", + "warning": "#e3b341", + "error": "#f85149", + "info": "#d29922" + }, + "overrides": { + "text-weak": "#8b949e", + "syntax-comment": "#8b949e", + "syntax-keyword": "#ff7b72", + "syntax-string": "#39c5cf", + "syntax-primitive": "#bc8cff", + "syntax-variable": "#d29922", + "syntax-property": "#39c5cf", + "syntax-type": "#d29922", + "syntax-constant": "#58a6ff", + "syntax-operator": "#ff7b72", + "syntax-punctuation": "#c9d1d9", + "syntax-object": "#d29922", + "markdown-heading": "#58a6ff", + "markdown-text": "#c9d1d9", + "markdown-link": "#58a6ff", + "markdown-link-text": "#39c5cf", + "markdown-code": "#ff7b72", + "markdown-block-quote": "#8b949e", + "markdown-emph": "#e3b341", + "markdown-strong": "#d29922", + "markdown-horizontal-rule": "#30363d", + "markdown-list-item": "#58a6ff", + "markdown-list-enumeration": "#39c5cf", + "markdown-image": "#58a6ff", + "markdown-image-text": "#39c5cf", + "markdown-code-block": "#c9d1d9" + } + } +} diff --git a/packages/ui/src/theme/themes/kanagawa.json b/packages/ui/src/theme/themes/kanagawa.json new file mode 100644 index 00000000000..e1b308c15e3 --- /dev/null +++ b/packages/ui/src/theme/themes/kanagawa.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Kanagawa", + "id": "kanagawa", + "light": { + "palette": { + "neutral": "#F2E9DE", + "ink": "#54433A", + "primary": "#2D4F67", + "accent": "#D27E99", + "success": "#98BB6C", + "warning": "#D7A657", + "error": "#E82424", + "info": "#76946A", + "diffAdd": "#89AF5B", + "diffDelete": "#D61F1F" + }, + "overrides": { + "text-weak": "#9E9389", + "syntax-comment": "#9E9389", + "syntax-keyword": "#957FB8", + "syntax-string": "#98BB6C", + "syntax-primitive": "#2D4F67", + "syntax-variable": "#54433A", + "syntax-property": "#76946A", + "syntax-type": "#C38D9D", + "syntax-constant": "#D7A657", + "syntax-operator": "#D27E99", + "syntax-punctuation": "#54433A", + "syntax-object": "#54433A", + "markdown-heading": "#957FB8", + "markdown-text": "#54433A", + "markdown-link": "#2D4F67", + "markdown-link-text": "#76946A", + "markdown-code": "#98BB6C", + "markdown-block-quote": "#9E9389", + "markdown-emph": "#C38D9D", + "markdown-strong": "#D7A657", + "markdown-horizontal-rule": "#9E9389", + "markdown-list-item": "#2D4F67", + "markdown-list-enumeration": "#76946A", + "markdown-image": "#2D4F67", + "markdown-image-text": "#76946A", + "markdown-code-block": "#54433A" + } + }, + "dark": { + "palette": { + "neutral": "#1F1F28", + "ink": "#DCD7BA", + "primary": "#7E9CD8", + "accent": "#D27E99", + "success": "#98BB6C", + "warning": "#D7A657", + "error": "#E82424", + "info": "#76946A", + "diffAdd": "#A9D977", + "diffDelete": "#F24A4A" + }, + "overrides": { + "text-weak": "#727169", + "syntax-comment": "#727169", + "syntax-keyword": "#957FB8", + "syntax-string": "#98BB6C", + "syntax-primitive": "#7E9CD8", + "syntax-variable": "#DCD7BA", + "syntax-property": "#76946A", + "syntax-type": "#C38D9D", + "syntax-constant": "#D7A657", + "syntax-operator": "#D27E99", + "syntax-punctuation": "#DCD7BA", + "syntax-object": "#DCD7BA", + "markdown-heading": "#957FB8", + "markdown-text": "#DCD7BA", + "markdown-link": "#7E9CD8", + "markdown-link-text": "#76946A", + "markdown-code": "#98BB6C", + "markdown-block-quote": "#727169", + "markdown-emph": "#C38D9D", + "markdown-strong": "#D7A657", + "markdown-horizontal-rule": "#727169", + "markdown-list-item": "#7E9CD8", + "markdown-list-enumeration": "#76946A", + "markdown-image": "#7E9CD8", + "markdown-image-text": "#76946A", + "markdown-code-block": "#DCD7BA" + } + } +} diff --git a/packages/ui/src/theme/themes/lucent-orng.json b/packages/ui/src/theme/themes/lucent-orng.json new file mode 100644 index 00000000000..b963152ec35 --- /dev/null +++ b/packages/ui/src/theme/themes/lucent-orng.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Lucent Orng", + "id": "lucent-orng", + "light": { + "palette": { + "neutral": "#fff5f0", + "ink": "#1a1a1a", + "primary": "#EC5B2B", + "accent": "#c94d24", + "success": "#0062d1", + "warning": "#EC5B2B", + "error": "#d1383d", + "info": "#318795", + "diffDelete": "#f52a65" + }, + "overrides": { + "text-weak": "#8a8a8a", + "syntax-comment": "#8a8a8a", + "syntax-keyword": "#EC5B2B", + "syntax-string": "#0062d1", + "syntax-primitive": "#c94d24", + "syntax-variable": "#d1383d", + "syntax-property": "#318795", + "syntax-type": "#b0851f", + "syntax-constant": "#EC5B2B", + "syntax-operator": "#318795", + "syntax-punctuation": "#1a1a1a", + "syntax-object": "#d1383d", + "markdown-heading": "#EC5B2B", + "markdown-text": "#1a1a1a", + "markdown-link": "#EC5B2B", + "markdown-link-text": "#318795", + "markdown-code": "#0062d1", + "markdown-block-quote": "#b0851f", + "markdown-emph": "#b0851f", + "markdown-strong": "#EC5B2B", + "markdown-horizontal-rule": "#8a8a8a", + "markdown-list-item": "#EC5B2B", + "markdown-list-enumeration": "#318795", + "markdown-image": "#EC5B2B", + "markdown-image-text": "#318795", + "markdown-code-block": "#1a1a1a" + } + }, + "dark": { + "palette": { + "neutral": "#2a1a15", + "ink": "#eeeeee", + "primary": "#EC5B2B", + "accent": "#FFF7F1", + "success": "#6ba1e6", + "warning": "#EC5B2B", + "error": "#e06c75", + "info": "#56b6c2", + "diffDelete": "#e26a75" + }, + "overrides": { + "text-weak": "#808080", + "syntax-comment": "#808080", + "syntax-keyword": "#EC5B2B", + "syntax-string": "#6ba1e6", + "syntax-primitive": "#EE7948", + "syntax-variable": "#e06c75", + "syntax-property": "#56b6c2", + "syntax-type": "#e5c07b", + "syntax-constant": "#FFF7F1", + "syntax-operator": "#56b6c2", + "syntax-punctuation": "#eeeeee", + "syntax-object": "#e06c75", + "markdown-heading": "#EC5B2B", + "markdown-text": "#eeeeee", + "markdown-link": "#EC5B2B", + "markdown-link-text": "#56b6c2", + "markdown-code": "#6ba1e6", + "markdown-block-quote": "#FFF7F1", + "markdown-emph": "#e5c07b", + "markdown-strong": "#EE7948", + "markdown-horizontal-rule": "#808080", + "markdown-list-item": "#EC5B2B", + "markdown-list-enumeration": "#56b6c2", + "markdown-image": "#EC5B2B", + "markdown-image-text": "#56b6c2", + "markdown-code-block": "#eeeeee" + } + } +} diff --git a/packages/ui/src/theme/themes/material.json b/packages/ui/src/theme/themes/material.json new file mode 100644 index 00000000000..0753bbf5290 --- /dev/null +++ b/packages/ui/src/theme/themes/material.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Material", + "id": "material", + "light": { + "palette": { + "neutral": "#fafafa", + "ink": "#263238", + "primary": "#6182b8", + "accent": "#39adb5", + "success": "#91b859", + "warning": "#ffb300", + "error": "#e53935", + "info": "#f4511e", + "interactive": "#39adb5" + }, + "overrides": { + "text-weak": "#90a4ae", + "syntax-comment": "#90a4ae", + "syntax-keyword": "#7c4dff", + "syntax-string": "#91b859", + "syntax-primitive": "#6182b8", + "syntax-variable": "#263238", + "syntax-property": "#7c4dff", + "syntax-type": "#ffb300", + "syntax-constant": "#f4511e", + "syntax-operator": "#39adb5", + "syntax-punctuation": "#263238", + "syntax-object": "#263238", + "markdown-heading": "#6182b8", + "markdown-text": "#263238", + "markdown-link": "#39adb5", + "markdown-link-text": "#7c4dff", + "markdown-code": "#91b859", + "markdown-block-quote": "#90a4ae", + "markdown-emph": "#ffb300", + "markdown-strong": "#f4511e", + "markdown-horizontal-rule": "#e0e0e0", + "markdown-list-item": "#6182b8", + "markdown-list-enumeration": "#39adb5", + "markdown-image": "#39adb5", + "markdown-image-text": "#7c4dff", + "markdown-code-block": "#263238" + } + }, + "dark": { + "palette": { + "neutral": "#263238", + "ink": "#eeffff", + "primary": "#82aaff", + "accent": "#89ddff", + "success": "#c3e88d", + "warning": "#ffcb6b", + "error": "#f07178", + "info": "#ffcb6b", + "interactive": "#89ddff" + }, + "overrides": { + "text-weak": "#546e7a", + "syntax-comment": "#546e7a", + "syntax-keyword": "#c792ea", + "syntax-string": "#c3e88d", + "syntax-primitive": "#82aaff", + "syntax-variable": "#eeffff", + "syntax-property": "#c792ea", + "syntax-type": "#ffcb6b", + "syntax-constant": "#ffcb6b", + "syntax-operator": "#89ddff", + "syntax-punctuation": "#eeffff", + "syntax-object": "#eeffff", + "markdown-heading": "#82aaff", + "markdown-text": "#eeffff", + "markdown-link": "#89ddff", + "markdown-link-text": "#c792ea", + "markdown-code": "#c3e88d", + "markdown-block-quote": "#546e7a", + "markdown-emph": "#ffcb6b", + "markdown-strong": "#ffcb6b", + "markdown-horizontal-rule": "#37474f", + "markdown-list-item": "#82aaff", + "markdown-list-enumeration": "#89ddff", + "markdown-image": "#89ddff", + "markdown-image-text": "#c792ea", + "markdown-code-block": "#eeffff" + } + } +} diff --git a/packages/ui/src/theme/themes/matrix.json b/packages/ui/src/theme/themes/matrix.json new file mode 100644 index 00000000000..adc379326ee --- /dev/null +++ b/packages/ui/src/theme/themes/matrix.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Matrix", + "id": "matrix", + "light": { + "palette": { + "neutral": "#eef3ea", + "ink": "#203022", + "primary": "#1cc24b", + "accent": "#c770ff", + "success": "#1cc24b", + "warning": "#e6ff57", + "error": "#ff4b4b", + "info": "#30b3ff", + "interactive": "#30b3ff", + "diffAdd": "#5dac7e", + "diffDelete": "#d53a3a" + }, + "overrides": { + "text-weak": "#748476", + "syntax-comment": "#748476", + "syntax-keyword": "#c770ff", + "syntax-string": "#1cc24b", + "syntax-primitive": "#30b3ff", + "syntax-variable": "#203022", + "syntax-property": "#24f6d9", + "syntax-type": "#e6ff57", + "syntax-constant": "#ffa83d", + "syntax-operator": "#24f6d9", + "syntax-punctuation": "#203022", + "syntax-object": "#203022", + "markdown-heading": "#24f6d9", + "markdown-text": "#203022", + "markdown-link": "#30b3ff", + "markdown-link-text": "#24f6d9", + "markdown-code": "#1cc24b", + "markdown-block-quote": "#748476", + "markdown-emph": "#ffa83d", + "markdown-strong": "#e6ff57", + "markdown-horizontal-rule": "#748476", + "markdown-list-item": "#30b3ff", + "markdown-list-enumeration": "#24f6d9", + "markdown-image": "#30b3ff", + "markdown-image-text": "#24f6d9", + "markdown-code-block": "#203022" + } + }, + "dark": { + "palette": { + "neutral": "#0a0e0a", + "ink": "#62ff94", + "primary": "#2eff6a", + "accent": "#c770ff", + "success": "#62ff94", + "warning": "#e6ff57", + "error": "#ff4b4b", + "info": "#30b3ff", + "interactive": "#30b3ff", + "diffAdd": "#77ffaf", + "diffDelete": "#ff7171" + }, + "overrides": { + "text-weak": "#8ca391", + "syntax-comment": "#8ca391", + "syntax-keyword": "#c770ff", + "syntax-string": "#1cc24b", + "syntax-primitive": "#30b3ff", + "syntax-variable": "#62ff94", + "syntax-property": "#24f6d9", + "syntax-type": "#e6ff57", + "syntax-constant": "#ffa83d", + "syntax-operator": "#24f6d9", + "syntax-punctuation": "#62ff94", + "syntax-object": "#62ff94", + "markdown-heading": "#00efff", + "markdown-text": "#62ff94", + "markdown-link": "#30b3ff", + "markdown-link-text": "#24f6d9", + "markdown-code": "#1cc24b", + "markdown-block-quote": "#8ca391", + "markdown-emph": "#ffa83d", + "markdown-strong": "#e6ff57", + "markdown-horizontal-rule": "#8ca391", + "markdown-list-item": "#30b3ff", + "markdown-list-enumeration": "#24f6d9", + "markdown-image": "#30b3ff", + "markdown-image-text": "#24f6d9", + "markdown-code-block": "#62ff94" + } + } +} diff --git a/packages/ui/src/theme/themes/mercury.json b/packages/ui/src/theme/themes/mercury.json new file mode 100644 index 00000000000..0d16eda1edb --- /dev/null +++ b/packages/ui/src/theme/themes/mercury.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Mercury", + "id": "mercury", + "light": { + "palette": { + "neutral": "#ffffff", + "ink": "#363644", + "primary": "#5266eb", + "accent": "#8da4f5", + "success": "#036e43", + "warning": "#a44200", + "error": "#b0175f", + "info": "#007f95", + "interactive": "#465bd1" + }, + "overrides": { + "text-weak": "#70707d", + "syntax-comment": "#70707d", + "syntax-keyword": "#465bd1", + "syntax-string": "#036e43", + "syntax-primitive": "#5266eb", + "syntax-variable": "#007f95", + "syntax-property": "#5266eb", + "syntax-type": "#007f95", + "syntax-constant": "#a44200", + "syntax-operator": "#465bd1", + "syntax-punctuation": "#363644", + "syntax-object": "#007f95", + "markdown-heading": "#1e1e2a", + "markdown-text": "#363644", + "markdown-link": "#465bd1", + "markdown-link-text": "#5266eb", + "markdown-code": "#036e43", + "markdown-block-quote": "#70707d", + "markdown-emph": "#a44200", + "markdown-strong": "#1e1e2a", + "markdown-horizontal-rule": "#7073931a", + "markdown-list-item": "#1e1e2a", + "markdown-list-enumeration": "#5266eb", + "markdown-image": "#465bd1", + "markdown-image-text": "#5266eb", + "markdown-code-block": "#363644" + } + }, + "dark": { + "palette": { + "neutral": "#171721", + "ink": "#dddde5", + "primary": "#8da4f5", + "accent": "#8da4f5", + "success": "#77c599", + "warning": "#fc9b6f", + "error": "#fc92b4", + "info": "#77becf" + }, + "overrides": { + "text-weak": "#9d9da8", + "syntax-comment": "#9d9da8", + "syntax-keyword": "#8da4f5", + "syntax-string": "#77c599", + "syntax-primitive": "#8da4f5", + "syntax-variable": "#77becf", + "syntax-property": "#a7b6f8", + "syntax-type": "#77becf", + "syntax-constant": "#fc9b6f", + "syntax-operator": "#8da4f5", + "syntax-punctuation": "#dddde5", + "syntax-object": "#77becf", + "markdown-heading": "#ffffff", + "markdown-text": "#dddde5", + "markdown-link": "#8da4f5", + "markdown-link-text": "#a7b6f8", + "markdown-code": "#77c599", + "markdown-block-quote": "#9d9da8", + "markdown-emph": "#fc9b6f", + "markdown-strong": "#f4f5f9", + "markdown-horizontal-rule": "#b4b7c81f", + "markdown-list-item": "#ffffff", + "markdown-list-enumeration": "#8da4f5", + "markdown-image": "#8da4f5", + "markdown-image-text": "#a7b6f8", + "markdown-code-block": "#dddde5" + } + } +} diff --git a/packages/ui/src/theme/themes/one-dark.json b/packages/ui/src/theme/themes/one-dark.json new file mode 100644 index 00000000000..0017bd6f512 --- /dev/null +++ b/packages/ui/src/theme/themes/one-dark.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "One Dark", + "id": "one-dark", + "light": { + "palette": { + "neutral": "#fafafa", + "ink": "#383a42", + "primary": "#4078f2", + "accent": "#0184bc", + "success": "#50a14f", + "warning": "#c18401", + "error": "#e45649", + "info": "#986801", + "diffAdd": "#489447", + "diffDelete": "#d65145" + }, + "overrides": { + "text-weak": "#a0a1a7", + "syntax-comment": "#a0a1a7", + "syntax-keyword": "#a626a4", + "syntax-string": "#50a14f", + "syntax-primitive": "#4078f2", + "syntax-variable": "#e45649", + "syntax-property": "#0184bc", + "syntax-type": "#c18401", + "syntax-constant": "#986801", + "syntax-operator": "#0184bc", + "syntax-punctuation": "#383a42", + "syntax-object": "#e45649", + "markdown-heading": "#a626a4", + "markdown-text": "#383a42", + "markdown-link": "#4078f2", + "markdown-link-text": "#0184bc", + "markdown-code": "#50a14f", + "markdown-block-quote": "#a0a1a7", + "markdown-emph": "#c18401", + "markdown-strong": "#986801", + "markdown-horizontal-rule": "#a0a1a7", + "markdown-list-item": "#4078f2", + "markdown-list-enumeration": "#0184bc", + "markdown-image": "#4078f2", + "markdown-image-text": "#0184bc", + "markdown-code-block": "#383a42" + } + }, + "dark": { + "palette": { + "neutral": "#282c34", + "ink": "#abb2bf", + "primary": "#61afef", + "accent": "#56b6c2", + "success": "#98c379", + "warning": "#e5c07b", + "error": "#e06c75", + "info": "#d19a66", + "diffAdd": "#aad482", + "diffDelete": "#e8828b" + }, + "overrides": { + "text-weak": "#5c6370", + "syntax-comment": "#5c6370", + "syntax-keyword": "#c678dd", + "syntax-string": "#98c379", + "syntax-primitive": "#61afef", + "syntax-variable": "#e06c75", + "syntax-property": "#56b6c2", + "syntax-type": "#e5c07b", + "syntax-constant": "#d19a66", + "syntax-operator": "#56b6c2", + "syntax-punctuation": "#abb2bf", + "syntax-object": "#e06c75", + "markdown-heading": "#c678dd", + "markdown-text": "#abb2bf", + "markdown-link": "#61afef", + "markdown-link-text": "#56b6c2", + "markdown-code": "#98c379", + "markdown-block-quote": "#5c6370", + "markdown-emph": "#e5c07b", + "markdown-strong": "#d19a66", + "markdown-horizontal-rule": "#5c6370", + "markdown-list-item": "#61afef", + "markdown-list-enumeration": "#56b6c2", + "markdown-image": "#61afef", + "markdown-image-text": "#56b6c2", + "markdown-code-block": "#abb2bf" + } + } +} diff --git a/packages/ui/src/theme/themes/opencode.json b/packages/ui/src/theme/themes/opencode.json new file mode 100644 index 00000000000..de7ab19e56e --- /dev/null +++ b/packages/ui/src/theme/themes/opencode.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "OpenCode", + "id": "opencode", + "light": { + "palette": { + "neutral": "#ffffff", + "ink": "#1a1a1a", + "primary": "#3b7dd8", + "accent": "#d68c27", + "success": "#3d9a57", + "warning": "#d68c27", + "error": "#d1383d", + "info": "#318795", + "diffAdd": "#4db380", + "diffDelete": "#f52a65" + }, + "overrides": { + "text-weak": "#8a8a8a", + "syntax-comment": "#8a8a8a", + "syntax-keyword": "#d68c27", + "syntax-string": "#3d9a57", + "syntax-primitive": "#3b7dd8", + "syntax-variable": "#d1383d", + "syntax-property": "#318795", + "syntax-type": "#b0851f", + "syntax-constant": "#d68c27", + "syntax-operator": "#318795", + "syntax-punctuation": "#1a1a1a", + "syntax-object": "#d1383d", + "markdown-heading": "#d68c27", + "markdown-text": "#1a1a1a", + "markdown-link": "#3b7dd8", + "markdown-link-text": "#318795", + "markdown-code": "#3d9a57", + "markdown-block-quote": "#b0851f", + "markdown-emph": "#b0851f", + "markdown-strong": "#d68c27", + "markdown-horizontal-rule": "#8a8a8a", + "markdown-list-item": "#3b7dd8", + "markdown-list-enumeration": "#318795", + "markdown-image": "#3b7dd8", + "markdown-image-text": "#318795", + "markdown-code-block": "#1a1a1a" + } + }, + "dark": { + "palette": { + "neutral": "#0a0a0a", + "ink": "#eeeeee", + "primary": "#fab283", + "accent": "#9d7cd8", + "success": "#7fd88f", + "warning": "#f5a742", + "error": "#e06c75", + "info": "#56b6c2", + "diffAdd": "#b8db87", + "diffDelete": "#e26a75" + }, + "overrides": { + "text-weak": "#808080", + "syntax-comment": "#808080", + "syntax-keyword": "#9d7cd8", + "syntax-string": "#7fd88f", + "syntax-primitive": "#fab283", + "syntax-variable": "#e06c75", + "syntax-property": "#56b6c2", + "syntax-type": "#e5c07b", + "syntax-constant": "#f5a742", + "syntax-operator": "#56b6c2", + "syntax-punctuation": "#eeeeee", + "syntax-object": "#e06c75", + "markdown-heading": "#9d7cd8", + "markdown-text": "#eeeeee", + "markdown-link": "#fab283", + "markdown-link-text": "#56b6c2", + "markdown-code": "#7fd88f", + "markdown-block-quote": "#e5c07b", + "markdown-emph": "#e5c07b", + "markdown-strong": "#f5a742", + "markdown-horizontal-rule": "#808080", + "markdown-list-item": "#fab283", + "markdown-list-enumeration": "#56b6c2", + "markdown-image": "#fab283", + "markdown-image-text": "#56b6c2", + "markdown-code-block": "#eeeeee" + } + } +} diff --git a/packages/ui/src/theme/themes/orng.json b/packages/ui/src/theme/themes/orng.json new file mode 100644 index 00000000000..95bf19e9ffc --- /dev/null +++ b/packages/ui/src/theme/themes/orng.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Orng", + "id": "orng", + "light": { + "palette": { + "neutral": "#ffffff", + "ink": "#1a1a1a", + "primary": "#EC5B2B", + "accent": "#c94d24", + "success": "#0062d1", + "warning": "#EC5B2B", + "error": "#d1383d", + "info": "#318795", + "diffDelete": "#f52a65" + }, + "overrides": { + "text-weak": "#8a8a8a", + "syntax-comment": "#8a8a8a", + "syntax-keyword": "#EC5B2B", + "syntax-string": "#0062d1", + "syntax-primitive": "#c94d24", + "syntax-variable": "#d1383d", + "syntax-property": "#318795", + "syntax-type": "#b0851f", + "syntax-constant": "#EC5B2B", + "syntax-operator": "#318795", + "syntax-punctuation": "#1a1a1a", + "syntax-object": "#d1383d", + "markdown-heading": "#EC5B2B", + "markdown-text": "#1a1a1a", + "markdown-link": "#EC5B2B", + "markdown-link-text": "#318795", + "markdown-code": "#0062d1", + "markdown-block-quote": "#b0851f", + "markdown-emph": "#b0851f", + "markdown-strong": "#EC5B2B", + "markdown-horizontal-rule": "#8a8a8a", + "markdown-list-item": "#EC5B2B", + "markdown-list-enumeration": "#318795", + "markdown-image": "#EC5B2B", + "markdown-image-text": "#318795", + "markdown-code-block": "#1a1a1a" + } + }, + "dark": { + "palette": { + "neutral": "#0a0a0a", + "ink": "#eeeeee", + "primary": "#EC5B2B", + "accent": "#FFF7F1", + "success": "#6ba1e6", + "warning": "#EC5B2B", + "error": "#e06c75", + "info": "#56b6c2", + "diffDelete": "#e26a75" + }, + "overrides": { + "text-weak": "#808080", + "syntax-comment": "#808080", + "syntax-keyword": "#EC5B2B", + "syntax-string": "#6ba1e6", + "syntax-primitive": "#EE7948", + "syntax-variable": "#e06c75", + "syntax-property": "#56b6c2", + "syntax-type": "#e5c07b", + "syntax-constant": "#FFF7F1", + "syntax-operator": "#56b6c2", + "syntax-punctuation": "#eeeeee", + "syntax-object": "#e06c75", + "markdown-heading": "#EC5B2B", + "markdown-text": "#eeeeee", + "markdown-link": "#EC5B2B", + "markdown-link-text": "#56b6c2", + "markdown-code": "#6ba1e6", + "markdown-block-quote": "#FFF7F1", + "markdown-emph": "#e5c07b", + "markdown-strong": "#EE7948", + "markdown-horizontal-rule": "#808080", + "markdown-list-item": "#EC5B2B", + "markdown-list-enumeration": "#56b6c2", + "markdown-image": "#EC5B2B", + "markdown-image-text": "#56b6c2", + "markdown-code-block": "#eeeeee" + } + } +} diff --git a/packages/ui/src/theme/themes/osaka-jade.json b/packages/ui/src/theme/themes/osaka-jade.json new file mode 100644 index 00000000000..2a0689c2861 --- /dev/null +++ b/packages/ui/src/theme/themes/osaka-jade.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Osaka Jade", + "id": "osaka-jade", + "light": { + "palette": { + "neutral": "#F6F5DD", + "ink": "#111c18", + "primary": "#1faa90", + "accent": "#3d7a52", + "success": "#3d7a52", + "warning": "#b5a020", + "error": "#c7392d", + "info": "#1faa90" + }, + "overrides": { + "text-weak": "#53685B", + "syntax-comment": "#53685B", + "syntax-keyword": "#1faa90", + "syntax-string": "#3d7a52", + "syntax-primitive": "#3d7560", + "syntax-variable": "#111c18", + "syntax-property": "#3d7a52", + "syntax-type": "#3d7a52", + "syntax-constant": "#a8527a", + "syntax-operator": "#b5a020", + "syntax-punctuation": "#111c18", + "syntax-object": "#111c18", + "markdown-heading": "#1faa90", + "markdown-text": "#111c18", + "markdown-link": "#1faa90", + "markdown-link-text": "#3d7a52", + "markdown-code": "#3d7a52", + "markdown-block-quote": "#53685B", + "markdown-emph": "#a8527a", + "markdown-strong": "#111c18", + "markdown-horizontal-rule": "#53685B", + "markdown-list-item": "#1faa90", + "markdown-list-enumeration": "#1faa90", + "markdown-image": "#1faa90", + "markdown-image-text": "#3d7a52", + "markdown-code-block": "#111c18" + } + }, + "dark": { + "palette": { + "neutral": "#111c18", + "ink": "#C1C497", + "primary": "#2DD5B7", + "accent": "#549e6a", + "success": "#549e6a", + "warning": "#E5C736", + "error": "#FF5345", + "info": "#2DD5B7", + "interactive": "#8CD3CB", + "diffAdd": "#63b07a", + "diffDelete": "#db9f9c" + }, + "overrides": { + "text-weak": "#53685B", + "syntax-comment": "#53685B", + "syntax-keyword": "#2DD5B7", + "syntax-string": "#63b07a", + "syntax-primitive": "#509475", + "syntax-variable": "#C1C497", + "syntax-property": "#549e6a", + "syntax-type": "#549e6a", + "syntax-constant": "#D2689C", + "syntax-operator": "#459451", + "syntax-punctuation": "#C1C497", + "syntax-object": "#C1C497", + "markdown-heading": "#2DD5B7", + "markdown-text": "#C1C497", + "markdown-link": "#8CD3CB", + "markdown-link-text": "#549e6a", + "markdown-code": "#63b07a", + "markdown-block-quote": "#53685B", + "markdown-emph": "#D2689C", + "markdown-strong": "#C1C497", + "markdown-horizontal-rule": "#53685B", + "markdown-list-item": "#2DD5B7", + "markdown-list-enumeration": "#8CD3CB", + "markdown-image": "#8CD3CB", + "markdown-image-text": "#549e6a", + "markdown-code-block": "#C1C497" + } + } +} diff --git a/packages/ui/src/theme/themes/palenight.json b/packages/ui/src/theme/themes/palenight.json new file mode 100644 index 00000000000..15463c9c81b --- /dev/null +++ b/packages/ui/src/theme/themes/palenight.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Palenight", + "id": "palenight", + "light": { + "palette": { + "neutral": "#fafafa", + "ink": "#292d3e", + "primary": "#4976eb", + "accent": "#00acc1", + "success": "#91b859", + "warning": "#ffb300", + "error": "#e53935", + "info": "#f4511e" + }, + "overrides": { + "text-weak": "#8796b0", + "syntax-comment": "#8796b0", + "syntax-keyword": "#a854f2", + "syntax-string": "#91b859", + "syntax-primitive": "#4976eb", + "syntax-variable": "#292d3e", + "syntax-property": "#00acc1", + "syntax-type": "#ffb300", + "syntax-constant": "#f4511e", + "syntax-operator": "#00acc1", + "syntax-punctuation": "#292d3e", + "syntax-object": "#292d3e", + "markdown-heading": "#a854f2", + "markdown-text": "#292d3e", + "markdown-link": "#4976eb", + "markdown-link-text": "#00acc1", + "markdown-code": "#91b859", + "markdown-block-quote": "#8796b0", + "markdown-emph": "#ffb300", + "markdown-strong": "#f4511e", + "markdown-horizontal-rule": "#8796b0", + "markdown-list-item": "#4976eb", + "markdown-list-enumeration": "#00acc1", + "markdown-image": "#4976eb", + "markdown-image-text": "#00acc1", + "markdown-code-block": "#292d3e" + } + }, + "dark": { + "palette": { + "neutral": "#292d3e", + "ink": "#a6accd", + "primary": "#82aaff", + "accent": "#89ddff", + "success": "#c3e88d", + "warning": "#ffcb6b", + "error": "#f07178", + "info": "#f78c6c" + }, + "overrides": { + "text-weak": "#676e95", + "syntax-comment": "#676e95", + "syntax-keyword": "#c792ea", + "syntax-string": "#c3e88d", + "syntax-primitive": "#82aaff", + "syntax-variable": "#a6accd", + "syntax-property": "#89ddff", + "syntax-type": "#ffcb6b", + "syntax-constant": "#f78c6c", + "syntax-operator": "#89ddff", + "syntax-punctuation": "#a6accd", + "syntax-object": "#a6accd", + "markdown-heading": "#c792ea", + "markdown-text": "#a6accd", + "markdown-link": "#82aaff", + "markdown-link-text": "#89ddff", + "markdown-code": "#c3e88d", + "markdown-block-quote": "#676e95", + "markdown-emph": "#ffcb6b", + "markdown-strong": "#f78c6c", + "markdown-horizontal-rule": "#676e95", + "markdown-list-item": "#82aaff", + "markdown-list-enumeration": "#89ddff", + "markdown-image": "#82aaff", + "markdown-image-text": "#89ddff", + "markdown-code-block": "#a6accd" + } + } +} diff --git a/packages/ui/src/theme/themes/rosepine.json b/packages/ui/src/theme/themes/rosepine.json new file mode 100644 index 00000000000..a71ad18ce00 --- /dev/null +++ b/packages/ui/src/theme/themes/rosepine.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Rose Pine", + "id": "rosepine", + "light": { + "palette": { + "neutral": "#faf4ed", + "ink": "#575279", + "primary": "#31748f", + "accent": "#d7827e", + "success": "#286983", + "warning": "#ea9d34", + "error": "#b4637a", + "info": "#56949f" + }, + "overrides": { + "text-weak": "#9893a5", + "syntax-comment": "#9893a5", + "syntax-keyword": "#286983", + "syntax-string": "#ea9d34", + "syntax-primitive": "#d7827e", + "syntax-variable": "#575279", + "syntax-property": "#d7827e", + "syntax-type": "#56949f", + "syntax-constant": "#907aa9", + "syntax-operator": "#797593", + "syntax-punctuation": "#797593", + "syntax-object": "#575279", + "markdown-heading": "#907aa9", + "markdown-text": "#575279", + "markdown-link": "#31748f", + "markdown-link-text": "#d7827e", + "markdown-code": "#286983", + "markdown-block-quote": "#9893a5", + "markdown-emph": "#ea9d34", + "markdown-strong": "#b4637a", + "markdown-horizontal-rule": "#dfdad9", + "markdown-list-item": "#31748f", + "markdown-list-enumeration": "#d7827e", + "markdown-image": "#31748f", + "markdown-image-text": "#d7827e", + "markdown-code-block": "#575279" + } + }, + "dark": { + "palette": { + "neutral": "#191724", + "ink": "#e0def4", + "primary": "#9ccfd8", + "accent": "#ebbcba", + "success": "#31748f", + "warning": "#f6c177", + "error": "#eb6f92", + "info": "#9ccfd8" + }, + "overrides": { + "text-weak": "#6e6a86", + "syntax-comment": "#6e6a86", + "syntax-keyword": "#31748f", + "syntax-string": "#f6c177", + "syntax-primitive": "#ebbcba", + "syntax-variable": "#e0def4", + "syntax-property": "#ebbcba", + "syntax-type": "#9ccfd8", + "syntax-constant": "#c4a7e7", + "syntax-operator": "#908caa", + "syntax-punctuation": "#908caa", + "syntax-object": "#e0def4", + "markdown-heading": "#c4a7e7", + "markdown-text": "#e0def4", + "markdown-link": "#9ccfd8", + "markdown-link-text": "#ebbcba", + "markdown-code": "#31748f", + "markdown-block-quote": "#6e6a86", + "markdown-emph": "#f6c177", + "markdown-strong": "#eb6f92", + "markdown-horizontal-rule": "#403d52", + "markdown-list-item": "#9ccfd8", + "markdown-list-enumeration": "#ebbcba", + "markdown-image": "#9ccfd8", + "markdown-image-text": "#ebbcba", + "markdown-code-block": "#e0def4" + } + } +} diff --git a/packages/ui/src/theme/themes/synthwave84.json b/packages/ui/src/theme/themes/synthwave84.json new file mode 100644 index 00000000000..ce86ce89f2d --- /dev/null +++ b/packages/ui/src/theme/themes/synthwave84.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Synthwave '84", + "id": "synthwave84", + "light": { + "palette": { + "neutral": "#fafafa", + "ink": "#262335", + "primary": "#00bcd4", + "accent": "#9c27b0", + "success": "#4caf50", + "warning": "#ff9800", + "error": "#f44336", + "info": "#ff5722" + }, + "overrides": { + "text-weak": "#5c5c8a", + "syntax-comment": "#5c5c8a", + "syntax-keyword": "#e91e63", + "syntax-string": "#ff9800", + "syntax-primitive": "#ff5722", + "syntax-variable": "#262335", + "syntax-property": "#9c27b0", + "syntax-type": "#00bcd4", + "syntax-constant": "#9c27b0", + "syntax-operator": "#e91e63", + "syntax-punctuation": "#262335", + "syntax-object": "#262335", + "markdown-heading": "#e91e63", + "markdown-text": "#262335", + "markdown-link": "#00bcd4", + "markdown-link-text": "#9c27b0", + "markdown-code": "#4caf50", + "markdown-block-quote": "#5c5c8a", + "markdown-emph": "#ff9800", + "markdown-strong": "#ff5722", + "markdown-horizontal-rule": "#e0e0e0", + "markdown-list-item": "#00bcd4", + "markdown-list-enumeration": "#9c27b0", + "markdown-image": "#00bcd4", + "markdown-image-text": "#9c27b0", + "markdown-code-block": "#262335" + } + }, + "dark": { + "palette": { + "neutral": "#262335", + "ink": "#ffffff", + "primary": "#36f9f6", + "accent": "#b084eb", + "success": "#72f1b8", + "warning": "#fede5d", + "error": "#fe4450", + "info": "#ff8b39", + "diffAdd": "#97f1d8", + "diffDelete": "#ff5e5b" + }, + "overrides": { + "text-weak": "#848bbd", + "syntax-comment": "#848bbd", + "syntax-keyword": "#ff7edb", + "syntax-string": "#fede5d", + "syntax-primitive": "#ff8b39", + "syntax-variable": "#ffffff", + "syntax-property": "#b084eb", + "syntax-type": "#36f9f6", + "syntax-constant": "#b084eb", + "syntax-operator": "#ff7edb", + "syntax-punctuation": "#ffffff", + "syntax-object": "#ffffff", + "markdown-heading": "#ff7edb", + "markdown-text": "#ffffff", + "markdown-link": "#36f9f6", + "markdown-link-text": "#b084eb", + "markdown-code": "#72f1b8", + "markdown-block-quote": "#848bbd", + "markdown-emph": "#fede5d", + "markdown-strong": "#ff8b39", + "markdown-horizontal-rule": "#495495", + "markdown-list-item": "#36f9f6", + "markdown-list-enumeration": "#b084eb", + "markdown-image": "#36f9f6", + "markdown-image-text": "#b084eb", + "markdown-code-block": "#ffffff" + } + } +} diff --git a/packages/ui/src/theme/themes/vercel.json b/packages/ui/src/theme/themes/vercel.json new file mode 100644 index 00000000000..dd0028c7061 --- /dev/null +++ b/packages/ui/src/theme/themes/vercel.json @@ -0,0 +1,90 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Vercel", + "id": "vercel", + "light": { + "palette": { + "neutral": "#FFFFFF", + "ink": "#171717", + "primary": "#0070F3", + "accent": "#8E4EC6", + "success": "#388E3C", + "warning": "#FF9500", + "error": "#DC3545", + "info": "#0070F3", + "diffAdd": "#46A758", + "diffDelete": "#E5484D" + }, + "overrides": { + "text-weak": "#666666", + "syntax-comment": "#888888", + "syntax-keyword": "#E93D82", + "syntax-string": "#46A758", + "syntax-primitive": "#8E4EC6", + "syntax-variable": "#0070F3", + "syntax-property": "#12A594", + "syntax-type": "#12A594", + "syntax-constant": "#FFB224", + "syntax-operator": "#E93D82", + "syntax-punctuation": "#171717", + "syntax-object": "#0070F3", + "markdown-heading": "#8E4EC6", + "markdown-text": "#171717", + "markdown-link": "#0070F3", + "markdown-link-text": "#12A594", + "markdown-code": "#46A758", + "markdown-block-quote": "#666666", + "markdown-emph": "#FFB224", + "markdown-strong": "#E93D82", + "markdown-horizontal-rule": "#999999", + "markdown-list-item": "#171717", + "markdown-list-enumeration": "#0070F3", + "markdown-image": "#12A594", + "markdown-image-text": "#12A594", + "markdown-code-block": "#171717" + } + }, + "dark": { + "palette": { + "neutral": "#000000", + "ink": "#EDEDED", + "primary": "#0070F3", + "accent": "#8E4EC6", + "success": "#46A758", + "warning": "#FFB224", + "error": "#E5484D", + "info": "#52A8FF", + "interactive": "#52A8FF", + "diffAdd": "#63C46D", + "diffDelete": "#FF6166" + }, + "overrides": { + "text-weak": "#878787", + "syntax-comment": "#878787", + "syntax-keyword": "#F75590", + "syntax-string": "#63C46D", + "syntax-primitive": "#BF7AF0", + "syntax-variable": "#52A8FF", + "syntax-property": "#0AC7AC", + "syntax-type": "#0AC7AC", + "syntax-constant": "#F2A700", + "syntax-operator": "#F75590", + "syntax-punctuation": "#EDEDED", + "syntax-object": "#52A8FF", + "markdown-heading": "#BF7AF0", + "markdown-text": "#EDEDED", + "markdown-link": "#52A8FF", + "markdown-link-text": "#0AC7AC", + "markdown-code": "#63C46D", + "markdown-block-quote": "#878787", + "markdown-emph": "#F2A700", + "markdown-strong": "#F75590", + "markdown-horizontal-rule": "#454545", + "markdown-list-item": "#EDEDED", + "markdown-list-enumeration": "#52A8FF", + "markdown-image": "#0AC7AC", + "markdown-image-text": "#50E3C2", + "markdown-code-block": "#EDEDED" + } + } +} diff --git a/packages/ui/src/theme/themes/zenburn.json b/packages/ui/src/theme/themes/zenburn.json new file mode 100644 index 00000000000..fc642059903 --- /dev/null +++ b/packages/ui/src/theme/themes/zenburn.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://opencode.ai/desktop-theme.json", + "name": "Zenburn", + "id": "zenburn", + "light": { + "palette": { + "neutral": "#ffffef", + "ink": "#3f3f3f", + "primary": "#5f7f8f", + "accent": "#5f8f8f", + "success": "#5f8f5f", + "warning": "#8f8f5f", + "error": "#8f5f5f", + "info": "#8f7f5f" + }, + "overrides": { + "text-weak": "#6f6f6f", + "syntax-comment": "#5f7f5f", + "syntax-keyword": "#8f8f5f", + "syntax-string": "#8f5f5f", + "syntax-primitive": "#5f7f8f", + "syntax-variable": "#3f3f3f", + "syntax-property": "#5f8f8f", + "syntax-type": "#5f8f8f", + "syntax-constant": "#5f8f5f", + "syntax-operator": "#8f8f5f", + "syntax-punctuation": "#3f3f3f", + "syntax-object": "#3f3f3f", + "markdown-heading": "#8f8f5f", + "markdown-text": "#3f3f3f", + "markdown-link": "#5f7f8f", + "markdown-link-text": "#5f8f8f", + "markdown-code": "#5f8f5f", + "markdown-block-quote": "#6f6f6f", + "markdown-emph": "#8f8f5f", + "markdown-strong": "#8f7f5f", + "markdown-horizontal-rule": "#6f6f6f", + "markdown-list-item": "#5f7f8f", + "markdown-list-enumeration": "#5f8f8f", + "markdown-image": "#5f7f8f", + "markdown-image-text": "#5f8f8f", + "markdown-code-block": "#3f3f3f" + } + }, + "dark": { + "palette": { + "neutral": "#3f3f3f", + "ink": "#dcdccc", + "primary": "#8cd0d3", + "accent": "#93e0e3", + "success": "#7f9f7f", + "warning": "#f0dfaf", + "error": "#cc9393", + "info": "#dfaf8f", + "diffAdd": "#8fb28f", + "diffDelete": "#dca3a3" + }, + "overrides": { + "text-weak": "#9f9f9f", + "syntax-comment": "#7f9f7f", + "syntax-keyword": "#f0dfaf", + "syntax-string": "#cc9393", + "syntax-primitive": "#8cd0d3", + "syntax-variable": "#dcdccc", + "syntax-property": "#93e0e3", + "syntax-type": "#93e0e3", + "syntax-constant": "#8fb28f", + "syntax-operator": "#f0dfaf", + "syntax-punctuation": "#dcdccc", + "syntax-object": "#dcdccc", + "markdown-heading": "#f0dfaf", + "markdown-text": "#dcdccc", + "markdown-link": "#8cd0d3", + "markdown-link-text": "#93e0e3", + "markdown-code": "#7f9f7f", + "markdown-block-quote": "#9f9f9f", + "markdown-emph": "#e0cf9f", + "markdown-strong": "#dfaf8f", + "markdown-horizontal-rule": "#9f9f9f", + "markdown-list-item": "#8cd0d3", + "markdown-list-enumeration": "#93e0e3", + "markdown-image": "#8cd0d3", + "markdown-image-text": "#93e0e3", + "markdown-code-block": "#dcdccc" + } + } +} From d961981e25b77e13a2f0afd4dcf3808222ed581b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:49:44 -0500 Subject: [PATCH 16/48] fix(app): list item background colors --- .../src/components/dialog-select-server.tsx | 4 +-- .../app/src/components/settings-general.tsx | 29 ++++++++++--------- .../app/src/components/settings-keybinds.tsx | 5 ++-- packages/app/src/components/settings-list.tsx | 5 ++++ .../app/src/components/settings-models.tsx | 5 ++-- .../app/src/components/settings-providers.tsx | 9 +++--- 6 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 packages/app/src/components/settings-list.tsx diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index cba401a467e..655aba0b023 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -121,7 +121,7 @@ function ServerForm(props: ServerFormProps) { return (
-
+
{(i) => { const key = ServerConnection.key(i) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 16689bfe2e1..b768bafcca0 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -12,6 +12,7 @@ import { usePlatform } from "@/context/platform" import { useSettings, monoFontFamily } from "@/context/settings" import { playSound, SOUND_OPTIONS } from "@/utils/sound" import { Link } from "./link" +import { SettingsList } from "./settings-list" let demoSoundState = { cleanup: undefined as (() => void) | undefined, @@ -177,7 +178,7 @@ export const SettingsGeneral: Component = () => { const GeneralSection = () => (
-
+ { triggerStyle={{ "min-width": "180px" }} /> -
+
) @@ -256,7 +257,7 @@ export const SettingsGeneral: Component = () => {

{language.t("settings.general.section.appearance")}

-
+ { )} -
+
) @@ -341,7 +342,7 @@ export const SettingsGeneral: Component = () => {

{language.t("settings.general.section.notifications")}

-
+ { />
-
+
) @@ -385,7 +386,7 @@ export const SettingsGeneral: Component = () => {

{language.t("settings.general.section.sounds")}

-
+ { )} /> -
+
) @@ -438,7 +439,7 @@ export const SettingsGeneral: Component = () => {

{language.t("settings.general.section.updates")}

-
+ { : language.t("settings.updates.action.checkNow")} -
+
) @@ -504,7 +505,7 @@ export const SettingsGeneral: Component = () => {

{language.t("settings.desktop.section.wsl")}

-
+ { />
-
+
) }} @@ -537,7 +538,7 @@ export const SettingsGeneral: Component = () => {

{language.t("settings.general.section.display")}

-
+ @@ -555,7 +556,7 @@ export const SettingsGeneral: Component = () => {
-
+
) }} diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index 94bc76d76a0..1e42447895d 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -9,6 +9,7 @@ import fuzzysort from "fuzzysort" import { formatKeybind, parseKeybind, useCommand } from "@/context/command" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" +import { SettingsList } from "./settings-list" const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) const PALETTE_ID = "command.palette" @@ -406,7 +407,7 @@ export const SettingsKeybinds: Component = () => { 0}>

{language.t(groupKey[group])}

-
+ {(id) => (
@@ -432,7 +433,7 @@ export const SettingsKeybinds: Component = () => {
)}
-
+
)} diff --git a/packages/app/src/components/settings-list.tsx b/packages/app/src/components/settings-list.tsx new file mode 100644 index 00000000000..bd8e4d7d1f1 --- /dev/null +++ b/packages/app/src/components/settings-list.tsx @@ -0,0 +1,5 @@ +import { type Component, type JSX } from "solid-js" + +export const SettingsList: Component<{ children: JSX.Element }> = (props) => { + return
{props.children}
+} diff --git a/packages/app/src/components/settings-models.tsx b/packages/app/src/components/settings-models.tsx index eff62cd6c92..14667338e95 100644 --- a/packages/app/src/components/settings-models.tsx +++ b/packages/app/src/components/settings-models.tsx @@ -8,6 +8,7 @@ import { type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useModels } from "@/context/models" import { popularProviders } from "@/hooks/use-providers" +import { SettingsList } from "./settings-list" type ModelItem = ReturnType["list"]>[number] @@ -100,7 +101,7 @@ export const SettingsModels: Component = () => { {group.items[0].provider.name}
-
+ {(item) => { const key = { providerID: item.provider.id, modelID: item.id } @@ -124,7 +125,7 @@ export const SettingsModels: Component = () => { ) }} -
+
)} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index a9839758b72..cc69327f80d 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -11,6 +11,7 @@ import { useGlobalSync } from "@/context/global-sync" import { DialogConnectProvider } from "./dialog-connect-provider" import { DialogSelectProvider } from "./dialog-select-provider" import { DialogCustomProvider } from "./dialog-custom-provider" +import { SettingsList } from "./settings-list" type ProviderSource = "env" | "api" | "config" | "custom" type ProviderItem = ReturnType["connected"]>[number] @@ -136,7 +137,7 @@ export const SettingsProviders: Component = () => {

{language.t("settings.providers.section.connected")}

-
+ 0} fallback={ @@ -169,12 +170,12 @@ export const SettingsProviders: Component = () => { )} -
+

{language.t("settings.providers.section.popular")}

-
+ {(item) => (
@@ -232,7 +233,7 @@ export const SettingsProviders: Component = () => { {language.t("common.connect")}
-
+
@@ -224,6 +227,9 @@ export function BillingSection() { {i18n.t("workspace.billing.alipay")} + + {i18n.t("workspace.billing.wechat")} +
-
@@ -278,7 +289,7 @@ export const Playground = { max={0.5} step={0.01} value={bounce()} - onInput={(e) => setBounce(Number(e.currentTarget.value))} + onInput={(e) => setState("bounce", Number(e.currentTarget.value))} /> {bounce().toFixed(2)}
@@ -287,8 +298,14 @@ export const Playground = { fade ease
@@ -318,7 +335,7 @@ export const Playground = { max={14} step={0.5} value={blur()} - onInput={(e) => setBlur(Number(e.currentTarget.value))} + onInput={(e) => setState("blur", Number(e.currentTarget.value))} /> {blur()}px
diff --git a/packages/ui/src/components/text-reveal.stories.tsx b/packages/ui/src/components/text-reveal.stories.tsx index df514ca38d1..af25d190c75 100644 --- a/packages/ui/src/components/text-reveal.stories.tsx +++ b/packages/ui/src/components/text-reveal.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 { TextReveal } from "./text-reveal" export default { @@ -87,33 +88,42 @@ const headingSlot = { export const Playground = { render: () => { - const [index, setIndex] = createSignal(0) - const [cycling, setCycling] = createSignal(false) - const [growOnly, setGrowOnly] = createSignal(true) - - const [duration, setDuration] = createSignal(600) - const [bounce, setBounce] = createSignal(1.0) - const [bounceSoft, setBounceSoft] = createSignal(1.0) - - const [hybridTravel, setHybridTravel] = createSignal(25) - const [hybridEdge, setHybridEdge] = createSignal(17) - - const [edge, setEdge] = createSignal(17) - const [revealTravel, setRevealTravel] = createSignal(0) + const [state, setState] = createStore({ + index: 0, + cycling: false, + growOnly: true, + duration: 600, + bounce: 1.0, + bounceSoft: 1.0, + hybridTravel: 25, + hybridEdge: 17, + edge: 17, + revealTravel: 0, + }) + const index = () => state.index + const cycling = () => state.cycling + const growOnly = () => state.growOnly + const duration = () => state.duration + const bounce = () => state.bounce + const bounceSoft = () => state.bounceSoft + const hybridTravel = () => state.hybridTravel + const hybridEdge = () => state.hybridEdge + const edge = () => state.edge + const revealTravel = () => state.revealTravel let timer: number | undefined const text = () => TEXTS[index()] - const next = () => setIndex((i) => (i + 1) % TEXTS.length) - const prev = () => setIndex((i) => (i - 1 + TEXTS.length) % TEXTS.length) + const next = () => setState("index", (value) => (value + 1) % TEXTS.length) + const prev = () => setState("index", (value) => (value - 1 + TEXTS.length) % TEXTS.length) const toggleCycle = () => { if (cycling()) { if (timer) clearTimeout(timer) timer = undefined - setCycling(false) + setState("cycling", false) return } - setCycling(true) + setState("cycling", true) const tick = () => { next() timer = window.setTimeout(tick, 700 + Math.floor(Math.random() * 600)) @@ -172,7 +182,7 @@ export const Playground = {
{TEXTS.map((t, i) => ( - ))} @@ -188,7 +198,7 @@ export const Playground = { -
@@ -204,7 +214,7 @@ export const Playground = { max="40" step="1" value={hybridEdge()} - onInput={(e) => setHybridEdge(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("hybridEdge", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {hybridEdge()}% @@ -218,7 +228,7 @@ export const Playground = { max="40" step="1" value={hybridTravel()} - onInput={(e) => setHybridTravel(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("hybridTravel", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {hybridTravel()}px @@ -234,7 +244,7 @@ export const Playground = { max="1400" step="10" value={duration()} - onInput={(e) => setDuration(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("duration", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {duration()}ms @@ -248,7 +258,7 @@ export const Playground = { max="2" step="0.01" value={bounce()} - onInput={(e) => setBounce(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("bounce", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {bounce().toFixed(2)} @@ -262,7 +272,7 @@ export const Playground = { max="1.5" step="0.01" value={bounceSoft()} - onInput={(e) => setBounceSoft(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("bounceSoft", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {bounceSoft().toFixed(2)} @@ -280,7 +290,7 @@ export const Playground = { max="40" step="1" value={edge()} - onInput={(e) => setEdge(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("edge", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {edge()}% @@ -294,7 +304,7 @@ export const Playground = { max="16" step="1" value={revealTravel()} - onInput={(e) => setRevealTravel(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("revealTravel", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {revealTravel()}px diff --git a/packages/ui/src/components/text-reveal.tsx b/packages/ui/src/components/text-reveal.tsx index c4fe1302f0e..02bf8084ce0 100644 --- a/packages/ui/src/components/text-reveal.tsx +++ b/packages/ui/src/components/text-reveal.tsx @@ -1,4 +1,5 @@ -import { createEffect, createSignal, on, onCleanup, onMount } from "solid-js" +import { createEffect, on, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" const px = (value: number | string | undefined, fallback: number) => { if (typeof value === "number") return `${value}px` @@ -30,11 +31,18 @@ export function TextReveal(props: { growOnly?: boolean truncate?: boolean }) { - const [cur, setCur] = createSignal(props.text) - const [old, setOld] = createSignal() - const [width, setWidth] = createSignal("auto") - const [ready, setReady] = createSignal(false) - const [swapping, setSwapping] = createSignal(false) + const [state, setState] = createStore({ + cur: props.text, + old: undefined as string | undefined, + width: "auto", + ready: false, + swapping: false, + }) + const cur = () => state.cur + const old = () => state.old + const width = () => state.width + const ready = () => state.ready + const swapping = () => state.swapping let inRef: HTMLSpanElement | undefined let outRef: HTMLSpanElement | undefined let rootRef: HTMLSpanElement | undefined @@ -49,7 +57,7 @@ export function TextReveal(props: { const prev = Number.parseFloat(width()) if (Number.isFinite(prev) && next <= prev) return } - setWidth(`${next}px`) + setState("width", `${next}px`) } createEffect( @@ -58,25 +66,25 @@ export function TextReveal(props: { (next, prev) => { if (next === prev) return if (typeof next === "string" && typeof prev === "string" && next.startsWith(prev)) { - setCur(next) + setState("cur", next) widen(win()) return } - setSwapping(true) - setOld(prev) - setCur(next) + setState("swapping", true) + setState("old", prev) + setState("cur", next) if (typeof requestAnimationFrame !== "function") { widen(Math.max(win(), wout())) rootRef?.offsetHeight - setSwapping(false) + setState("swapping", false) return } if (frame !== undefined && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame) frame = requestAnimationFrame(() => { widen(Math.max(win(), wout())) rootRef?.offsetHeight - setSwapping(false) + setState("swapping", false) frame = undefined }) }, @@ -87,16 +95,16 @@ export function TextReveal(props: { widen(win()) const fonts = typeof document !== "undefined" ? document.fonts : undefined if (typeof requestAnimationFrame !== "function") { - setReady(true) + setState("ready", true) return } if (!fonts) { - requestAnimationFrame(() => setReady(true)) + requestAnimationFrame(() => setState("ready", true)) return } fonts.ready.finally(() => { widen(win()) - requestAnimationFrame(() => setReady(true)) + requestAnimationFrame(() => setState("ready", true)) }) }) diff --git a/packages/ui/src/components/text-strikethrough.stories.tsx b/packages/ui/src/components/text-strikethrough.stories.tsx index b07e7455348..5e86413f93f 100644 --- a/packages/ui/src/components/text-strikethrough.stories.tsx +++ b/packages/ui/src/components/text-strikethrough.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" import { useSpring } from "./motion-spring" import { TextStrikethrough } from "./text-strikethrough" @@ -130,12 +131,16 @@ function VariantF(props: { active: boolean; text: string }) { ) let baseRef: HTMLSpanElement | undefined let containerRef: HTMLSpanElement | undefined - const [textWidth, setTextWidth] = createSignal(0) - const [containerWidth, setContainerWidth] = createSignal(0) + const [state, setState] = createStore({ + textWidth: 0, + containerWidth: 0, + }) + const textWidth = () => state.textWidth + const containerWidth = () => state.containerWidth const measure = () => { - if (baseRef) setTextWidth(baseRef.scrollWidth) - if (containerRef) setContainerWidth(containerRef.offsetWidth) + if (baseRef) setState("textWidth", baseRef.scrollWidth) + if (containerRef) setState("containerWidth", containerRef.offsetWidth) } onMount(measure) diff --git a/packages/ui/src/components/text-strikethrough.tsx b/packages/ui/src/components/text-strikethrough.tsx index 211e7d44c09..aee5e0cbd0e 100644 --- a/packages/ui/src/components/text-strikethrough.tsx +++ b/packages/ui/src/components/text-strikethrough.tsx @@ -1,5 +1,6 @@ import type { JSX } from "solid-js" -import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { createEffect, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" import { useSpring } from "./motion-spring" export function TextStrikethrough(props: { @@ -19,12 +20,16 @@ export function TextStrikethrough(props: { let baseRef: HTMLSpanElement | undefined let containerRef: HTMLSpanElement | undefined - const [textWidth, setTextWidth] = createSignal(0) - const [containerWidth, setContainerWidth] = createSignal(0) + const [state, setState] = createStore({ + textWidth: 0, + containerWidth: 0, + }) + const textWidth = () => state.textWidth + const containerWidth = () => state.containerWidth const measure = () => { - if (baseRef) setTextWidth(baseRef.scrollWidth) - if (containerRef) setContainerWidth(containerRef.offsetWidth) + if (baseRef) setState("textWidth", baseRef.scrollWidth) + if (containerRef) setState("containerWidth", containerRef.offsetWidth) } onMount(measure) diff --git a/packages/ui/src/components/thinking-heading.stories.tsx b/packages/ui/src/components/thinking-heading.stories.tsx index 90eb7ee3190..3a65619ce1c 100644 --- a/packages/ui/src/components/thinking-heading.stories.tsx +++ b/packages/ui/src/components/thinking-heading.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck -import { createSignal, createEffect, on, onMount, onCleanup } from "solid-js" +import { createEffect, on, onMount, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" import { TextShimmer } from "./text-shimmer" import { TextReveal } from "./text-reveal" @@ -375,11 +376,18 @@ input[type="range"].heading-slider::-webkit-slider-thumb { // --------------------------------------------------------------------------- function AnimatedHeading(props) { - const [current, setCurrent] = createSignal(props.text) - const [leaving, setLeaving] = createSignal(undefined) - const [width, setWidth] = createSignal("auto") - const [ready, setReady] = createSignal(false) - const [swapping, setSwapping] = createSignal(false) + const [state, setState] = createStore({ + current: props.text, + leaving: undefined, + width: "auto", + ready: false, + swapping: false, + }) + const current = () => state.current + const leaving = () => state.leaving + const width = () => state.width + const ready = () => state.ready + const swapping = () => state.swapping let enterRef let leaveRef let containerRef @@ -391,16 +399,16 @@ function AnimatedHeading(props) { if (px <= 0) return const w = Number.parseFloat(width()) if (Number.isFinite(w) && px <= w) return - setWidth(`${px}px`) + setState("width", `${px}px`) } const measure = () => { if (!current()) { - setWidth("0px") + setState("width", "0px") return } const px = measureEnter() - if (px > 0) setWidth(`${px}px`) + if (px > 0) setState("width", `${px}px`) } createEffect( @@ -408,9 +416,9 @@ function AnimatedHeading(props) { () => props.text, (next, prev) => { if (next === prev) return - setSwapping(true) - setLeaving(prev) - setCurrent(next) + setState("swapping", true) + setState("leaving", prev) + setState("current", next) if (frame) cancelAnimationFrame(frame) frame = requestAnimationFrame(() => { @@ -420,10 +428,10 @@ function AnimatedHeading(props) { const leaveW = measureLeave() widen(Math.max(enterW, leaveW)) containerRef?.offsetHeight // reflow with max width + swap positions - setSwapping(false) + setState("swapping", false) } else { containerRef?.offsetHeight - setSwapping(false) + setState("swapping", false) measure() } frame = undefined @@ -436,7 +444,7 @@ function AnimatedHeading(props) { measure() document.fonts?.ready.finally(() => { measure() - requestAnimationFrame(() => setReady(true)) + requestAnimationFrame(() => setState("ready", true)) }) }) @@ -552,47 +560,56 @@ const VARIANTS: { key: string; label: string }[] = [] export const Playground = { render: () => { - const [heading, setHeading] = createSignal(HEADINGS[0]) - const [headingIndex, setHeadingIndex] = createSignal(0) - const [active, setActive] = createSignal(true) - const [cycling, setCycling] = createSignal(false) + const [state, setState] = createStore({ + heading: HEADINGS[0], + headingIndex: 0, + active: true, + cycling: false, + duration: 550, + blur: 2, + travel: 4, + bounce: 1.35, + maskSize: 12, + maskPad: 9, + maskHeight: 0, + debug: false, + odoBlur: false, + }) + const heading = () => state.heading + const headingIndex = () => state.headingIndex + const active = () => state.active + const cycling = () => state.cycling + const duration = () => state.duration + const blur = () => state.blur + const travel = () => state.travel + const bounce = () => state.bounce + const maskSize = () => state.maskSize + const maskPad = () => state.maskPad + const maskHeight = () => state.maskHeight + const debug = () => state.debug + const odoBlur = () => state.odoBlur let cycleTimer - // tunable params - const [duration, setDuration] = createSignal(550) - const [blur, setBlur] = createSignal(2) - const [travel, setTravel] = createSignal(4) - const [bounce, setBounce] = createSignal(1.35) - const [maskSize, setMaskSize] = createSignal(12) - const [maskPad, setMaskPad] = createSignal(9) - const [maskHeight, setMaskHeight] = createSignal(0) - const [debug, setDebug] = createSignal(false) - const [odoBlur, setOdoBlur] = createSignal(false) - const nextHeading = () => { - setHeadingIndex((i) => { - const next = (i + 1) % HEADINGS.length - setHeading(HEADINGS[next]) - return next - }) + const next = (headingIndex() + 1) % HEADINGS.length + setState("headingIndex", next) + setState("heading", HEADINGS[next]) } const prevHeading = () => { - setHeadingIndex((i) => { - const prev = (i - 1 + HEADINGS.length) % HEADINGS.length - setHeading(HEADINGS[prev]) - return prev - }) + const prev = (headingIndex() - 1 + HEADINGS.length) % HEADINGS.length + setState("headingIndex", prev) + setState("heading", HEADINGS[prev]) } const toggleCycling = () => { if (cycling()) { clearTimeout(cycleTimer) cycleTimer = undefined - setCycling(false) + setState("cycling", false) return } - setCycling(true) + setState("cycling", true) const tick = () => { if (!cycling()) return nextHeading() @@ -602,11 +619,11 @@ export const Playground = { } const clearHeading = () => { - setHeading(undefined) + setState("heading", undefined) if (cycling()) { clearTimeout(cycleTimer) cycleTimer = undefined - setCycling(false) + setState("cycling", false) } } @@ -686,7 +703,7 @@ export const Playground = { max={1400} step={50} value={duration()} - onInput={(e) => setDuration(Number(e.currentTarget.value))} + onInput={(e) => setState("duration", Number(e.currentTarget.value))} /> {duration()}ms
@@ -700,7 +717,7 @@ export const Playground = { max={16} step={0.5} value={blur()} - onInput={(e) => setBlur(Number(e.currentTarget.value))} + onInput={(e) => setState("blur", Number(e.currentTarget.value))} /> {blur()}px @@ -714,7 +731,7 @@ export const Playground = { max={120} step={1} value={travel()} - onInput={(e) => setTravel(Number(e.currentTarget.value))} + onInput={(e) => setState("travel", Number(e.currentTarget.value))} /> {travel()}px @@ -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 4be3a02bbec..cf160b188bf 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 ba39ae586af..0c99924de98 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 68440b6c637..2a58e0e5bb6 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 ee608152d5e..692ab31670f 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 27/48] 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 c6fca36d59a..e370862212b 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 6fde71f3b2e..cbb24f77bc1 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 655aba0b023..eb039c14d61 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 8cb704bf1df..ae9d2800ed4 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 4f49911c127..89895874250 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 1e42447895d..7e2a48110cb 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 84090329388..ff455ebe205 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 03bd6318dab..2c6799d12be 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 99c6d2e4219..f8fec7142d8 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 c1a87b95b89..60e9fd6d542 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 1b6cdf530a7..c8409886928 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 8b1a3c48c5f..13494b7ade0 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 cec76ff87ec..eee763f16de 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 e2ada244fb3..d5904c60964 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 00000000000..3e8fa9af251 --- /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 a2807375ff6..e65c1678846 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 1b872082679..99a2d03d094 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 9e1f7f2a0e8..46ee7f114e1 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 3151c9b2213..140b838103b 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 8d9331ab68f..9b776c143e2 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 782b67262fc..5031748b46c 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 b950cab8d64..65e878b4e9b 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 a2633004cf7..2fabd6d4c8e 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 cbde19316dd..dc30a0e537a 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 914ac5cd796..1f5615c9bd1 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 f0a3f3ae6b2..a2f5e5c7c8b 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 17ba96058b5..ed75e556ea6 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 38111c8738b..2507acd9d2a 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 0b63e842273..6145b3011b3 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 94bb2c2c318..9cc3c5be1da 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 66835c1c574..373f26ad6fb 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 d6b179c5c50..819e1cd87d7 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 3796350d2c4..8c80cd3235e 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 a30d86d1809..11284b3d2d7 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 c1088622a26..bc04f9ecf27 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 57ef1853d15..d917ce4c7aa 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 84f77ea4a3c..a5263cd743e 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 5500de97a49..2cd660b39f4 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 3f009f4e0fb..0b2c1e1ce4f 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 17572923eb8..04d898134cf 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 => ( -