From e49306b86c490faf42f5ffcd7701a89329ea7f28 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 25 Jan 2026 12:44:06 -0500 Subject: [PATCH 001/232] rm log statement --- packages/opencode/src/provider/models.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index b21ab0804ac..82794f35baa 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -105,9 +105,6 @@ export namespace ModelsDev { export async function refresh() { const file = Bun.file(filepath) - log.info("refreshing", { - file, - }) const result = await fetch(`${url()}/api.json`, { headers: { "User-Agent": Installation.USER_AGENT, From fc57c074aea2838a7c36210ac853546179744a26 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 25 Jan 2026 17:45:07 +0000 Subject: [PATCH 002/232] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 0919ea16c59..7e8628de7a4 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 3832a82c8eb..e6d968ed626 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 14b00f64a7e4e834653068fe8f9f9f7cbdbde018 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 12:16:31 -0600 Subject: [PATCH 003/232] fix(app): escape should always close dialogs --- packages/ui/src/context/dialog.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx index d45fe663251..a3aafa0c7a9 100644 --- a/packages/ui/src/context/dialog.tsx +++ b/packages/ui/src/context/dialog.tsx @@ -1,8 +1,10 @@ import { createContext, + createEffect, createRoot, createSignal, getOwner, + onCleanup, type Owner, type ParentProps, runWithOwner, @@ -34,6 +36,20 @@ function init() { setActive(undefined) } + createEffect(() => { + if (!active()) return + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return + close() + event.preventDefault() + event.stopPropagation() + } + + window.addEventListener("keydown", onKeyDown, true) + onCleanup(() => window.removeEventListener("keydown", onKeyDown, true)) + }) + const show = (element: DialogElement, owner: Owner, onClose?: () => void) => { close() @@ -41,13 +57,13 @@ function init() { let dispose: (() => void) | undefined const node = runWithOwner(owner, () => - createRoot((d) => { + createRoot((d: () => void) => { dispose = d return ( { + onOpenChange={(open: boolean) => { if (open) return close() }} From d115f33b59d84bda8df8937624504dc2e09731d6 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:13:38 -0600 Subject: [PATCH 004/232] fix(app): don't allow workspaces in non-vcs projects --- packages/app/src/pages/layout.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 7895b83085f..485deae99d9 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -548,6 +548,7 @@ export default function Layout(props: ParentProps) { const workspaceSetting = createMemo(() => { const project = currentProject() if (!project) return false + if (project.vcs !== "git") return false return layout.sidebar.workspaces(project.worktree)() }) @@ -587,7 +588,7 @@ export default function Layout(props: ParentProps) { if (!expanded) continue const project = projects.find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) if (!project) continue - if (layout.sidebar.workspaces(project.worktree)()) continue + if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue setStore("workspaceExpanded", directory, false) } }) @@ -2041,7 +2042,9 @@ export default function Layout(props: ParentProps) { }) const workspaces = createMemo(() => workspaceIds(props.project).slice(0, 2)) - const workspaceEnabled = createMemo(() => layout.sidebar.workspaces(props.project.worktree)()) + const workspaceEnabled = createMemo( + () => props.project.vcs === "git" && layout.sidebar.workspaces(props.project.worktree)(), + ) const [open, setOpen] = createSignal(false) const label = (directory: string) => { @@ -2301,7 +2304,7 @@ export default function Layout(props: ParentProps) { title: language.t("workspace.new"), category: language.t("command.category.workspace"), keybind: "mod+shift+w", - disabled: !layout.sidebar.workspaces(project()?.worktree ?? "")(), + disabled: !workspaceSetting(), onSelect: createWorkspace, }, ]) @@ -2429,7 +2432,18 @@ export default function Layout(props: ParentProps) { dialog.show(() => )}> {language.t("common.edit")} - layout.sidebar.toggleWorkspaces(p.worktree)}> + { + const enabled = layout.sidebar.workspaces(p.worktree)() + if (enabled) { + layout.sidebar.toggleWorkspaces(p.worktree) + return + } + if (p.vcs !== "git") return + layout.sidebar.toggleWorkspaces(p.worktree) + }} + > {layout.sidebar.workspaces(p.worktree)() ? language.t("sidebar.workspaces.disable") @@ -2447,7 +2461,7 @@ export default function Layout(props: ParentProps) {
From 94ce289dd9103c16a7f69396a7c49d48dc036a6c Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 12:51:42 -0600 Subject: [PATCH 005/232] fix(app): run start command after reset --- packages/opencode/src/worktree/index.ts | 28 +++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 97fe2c4fc0d..dd165d56f69 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -517,8 +517,32 @@ export namespace Worktree { } const dirty = outputText(status.stdout) - if (!dirty) return true + if (dirty) { + throw new ResetFailedError({ message: `Worktree reset left local changes:\n${dirty}` }) + } + + const projectID = Instance.project.id + setTimeout(() => { + const start = async () => { + const project = await Storage.read(["project", projectID]).catch(() => undefined) + const startup = project?.commands?.start?.trim() ?? "" + if (!startup) return + + const ran = await runStartCommand(worktreePath, startup) + if (ran.exitCode === 0) return - throw new ResetFailedError({ message: `Worktree reset left local changes:\n${dirty}` }) + log.error("worktree start command failed", { + kind: "project", + directory: worktreePath, + message: errorText(ran), + }) + } + + void start().catch((error) => { + log.error("worktree start task failed", { directory: worktreePath, error }) + }) + }, 0) + + return true }) } From 407f34fed5140c4eb3b378c606a422de7e313d9a Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:19:23 -0600 Subject: [PATCH 006/232] chore: cleanup --- packages/opencode/src/worktree/index.ts | 83 +++++++++++++------------ 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index dd165d56f69..0f2e2f4a06c 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -244,6 +244,46 @@ export namespace Worktree { return $`bash -lc ${cmd}`.nothrow().cwd(directory) } + type StartKind = "project" | "worktree" + + async function runStartScript(directory: string, cmd: string, kind: StartKind) { + const text = cmd.trim() + if (!text) return true + + const ran = await runStartCommand(directory, text) + if (ran.exitCode === 0) return true + + log.error("worktree start command failed", { + kind, + directory, + message: errorText(ran), + }) + return false + } + + async function runStartScripts(directory: string, input: { projectID: string; extra?: string }) { + const project = await Storage.read(["project", input.projectID]).catch(() => undefined) + const startup = project?.commands?.start?.trim() ?? "" + const ok = await runStartScript(directory, startup, "project") + if (!ok) return false + + const extra = input.extra ?? "" + await runStartScript(directory, extra, "worktree") + return true + } + + function queueStartScripts(directory: string, input: { projectID: string; extra?: string }) { + setTimeout(() => { + const start = async () => { + await runStartScripts(directory, input) + } + + void start().catch((error) => { + log.error("worktree start task failed", { directory, error }) + }) + }, 0) + } + export const create = fn(CreateInput.optional(), async (input) => { if (Instance.project.vcs !== "git") { throw new NotGitError({ message: "Worktrees are only supported for git projects" }) @@ -318,27 +358,7 @@ export namespace Worktree { }, }) - const project = await Storage.read(["project", projectID]).catch(() => undefined) - const startup = project?.commands?.start?.trim() ?? "" - - const run = async (cmd: string, kind: "project" | "worktree") => { - const ran = await runStartCommand(info.directory, cmd) - if (ran.exitCode === 0) return true - log.error("worktree start command failed", { - kind, - directory: info.directory, - message: errorText(ran), - }) - return false - } - - if (startup) { - const ok = await run(startup, "project") - if (!ok) return - } - if (extra) { - await run(extra, "worktree") - } + await runStartScripts(info.directory, { projectID, extra }) } void start().catch((error) => { @@ -522,26 +542,7 @@ export namespace Worktree { } const projectID = Instance.project.id - setTimeout(() => { - const start = async () => { - const project = await Storage.read(["project", projectID]).catch(() => undefined) - const startup = project?.commands?.start?.trim() ?? "" - if (!startup) return - - const ran = await runStartCommand(worktreePath, startup) - if (ran.exitCode === 0) return - - log.error("worktree start command failed", { - kind: "project", - directory: worktreePath, - message: errorText(ran), - }) - } - - void start().catch((error) => { - log.error("worktree start task failed", { directory: worktreePath, error }) - }) - }, 0) + queueStartScripts(worktreePath, { projectID }) return true }) From 835b396591e7f43b38b69b0439e4937680496bea Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:45:37 -0600 Subject: [PATCH 007/232] chore: i18n for readme --- README.ar.md | 132 +++++++++++++++++++++++++++++++ README.br.md | 132 +++++++++++++++++++++++++++++++ README.da.md | 132 +++++++++++++++++++++++++++++++ README.de.md | 132 +++++++++++++++++++++++++++++++ README.es.md | 132 +++++++++++++++++++++++++++++++ README.fr.md | 132 +++++++++++++++++++++++++++++++ README.ja.md | 132 +++++++++++++++++++++++++++++++ README.ko.md | 132 +++++++++++++++++++++++++++++++ README.md | 17 ++++ README.no.md | 132 +++++++++++++++++++++++++++++++ README.pl.md | 132 +++++++++++++++++++++++++++++++ README.ru.md | 132 +++++++++++++++++++++++++++++++ README.zh-CN.md => README.zh.md | 21 ++++- README.zh-TW.md => README.zht.md | 23 ++++-- 14 files changed, 1504 insertions(+), 9 deletions(-) create mode 100644 README.ar.md create mode 100644 README.br.md create mode 100644 README.da.md create mode 100644 README.de.md create mode 100644 README.es.md create mode 100644 README.fr.md create mode 100644 README.ja.md create mode 100644 README.ko.md create mode 100644 README.no.md create mode 100644 README.pl.md create mode 100644 README.ru.md rename README.zh-CN.md => README.zh.md (88%) rename README.zh-TW.md => README.zht.md (89%) diff --git a/README.ar.md b/README.ar.md new file mode 100644 index 00000000000..3bcaf3272a7 --- /dev/null +++ b/README.ar.md @@ -0,0 +1,132 @@ +

+ + + + + شعار OpenCode + + +

+

وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### التثبيت + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# مديري الحزم +npm i -g opencode-ai@latest # او bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS و Linux (موصى به، دائما محدث) +brew install opencode # macOS و Linux (صيغة brew الرسمية، تحديث اقل) +paru -S opencode-bin # Arch Linux +mise use -g opencode # اي نظام +nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث فرع dev +``` + +> [!TIP] +> احذف الاصدارات الاقدم من 0.1.x قبل التثبيت. + +### تطبيق سطح المكتب (BETA) + +يتوفر OpenCode ايضا كتطبيق سطح مكتب. قم بالتنزيل مباشرة من [صفحة الاصدارات](https://github.com/anomalyco/opencode/releases) او من [opencode.ai/download](https://opencode.ai/download). + +| المنصة | التنزيل | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb` او `.rpm` او AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### مجلد التثبيت + +يحترم سكربت التثبيت ترتيب الاولوية التالي لمسار التثبيت: + +1. `$OPENCODE_INSTALL_DIR` - مجلد تثبيت مخصص +2. `$XDG_BIN_DIR` - مسار متوافق مع مواصفات XDG Base Directory +3. `$HOME/bin` - مجلد الثنائيات القياسي للمستخدم (ان وجد او امكن انشاؤه) +4. `$HOME/.opencode/bin` - المسار الافتراضي الاحتياطي + +```bash +# امثلة +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +يتضمن OpenCode وكيليْن (Agents) مدمجين يمكنك التبديل بينهما باستخدام زر `Tab`. + +- **build** - الافتراضي، وكيل بصلاحيات كاملة لاعمال التطوير +- **plan** - وكيل للقراءة فقط للتحليل واستكشاف الكود + - يرفض تعديل الملفات افتراضيا + - يطلب الاذن قبل تشغيل اوامر bash + - مثالي لاستكشاف قواعد كود غير مألوفة او لتخطيط التغييرات + +بالاضافة الى ذلك يوجد وكيل فرعي **general** للبحث المعقد والمهام متعددة الخطوات. +يستخدم داخليا ويمكن استدعاؤه بكتابة `@general` في الرسائل. + +تعرف على المزيد حول [agents](https://opencode.ai/docs/agents). + +### التوثيق + +لمزيد من المعلومات حول كيفية ضبط OpenCode، [**راجع التوثيق**](https://opencode.ai/docs). + +### المساهمة + +اذا كنت مهتما بالمساهمة في OpenCode، يرجى قراءة [contributing docs](./CONTRIBUTING.md) قبل ارسال pull request. + +### البناء فوق OpenCode + +اذا كنت تعمل على مشروع مرتبط بـ OpenCode ويستخدم "opencode" كجزء من اسمه (مثل "opencode-dashboard" او "opencode-mobile")، يرجى اضافة ملاحظة في README توضح انه ليس مبنيا بواسطة فريق OpenCode ولا يرتبط بنا بأي شكل. + +### FAQ + +#### ما الفرق عن Claude Code؟ + +هو مشابه جدا لـ Claude Code من حيث القدرات. هذه هي الفروقات الاساسية: + +- 100% مفتوح المصدر +- غير مقترن بمزود معين. نوصي بالنماذج التي نوفرها عبر [OpenCode Zen](https://opencode.ai/zen)؛ لكن يمكن استخدام OpenCode مع Claude او OpenAI او Google او حتى نماذج محلية. مع تطور النماذج ستتقلص الفجوات وستنخفض الاسعار، لذا من المهم ان يكون مستقلا عن المزود. +- دعم LSP جاهز للاستخدام +- تركيز على TUI. تم بناء OpenCode بواسطة مستخدمي neovim ومنشئي [terminal.shop](https://terminal.shop)؛ وسندفع حدود ما هو ممكن داخل الطرفية. +- معمارية عميل/خادم. على سبيل المثال، يمكن تشغيل OpenCode على جهازك بينما تقوده عن بعد من تطبيق جوال. هذا يعني ان واجهة TUI هي واحدة فقط من العملاء الممكنين. + +--- + +**انضم الى مجتمعنا** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.br.md b/README.br.md new file mode 100644 index 00000000000..8c6527f2023 --- /dev/null +++ b/README.br.md @@ -0,0 +1,132 @@ +

+ + + + + Logo do OpenCode + + +

+

O agente de programação com IA de código aberto.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Instalação + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Gerenciadores de pacotes +npm i -g opencode-ai@latest # ou bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS e Linux (recomendado, sempre atualizado) +brew install opencode # macOS e Linux (fórmula oficial do brew, atualiza menos) +paru -S opencode-bin # Arch Linux +mise use -g opencode # qualquer sistema +nix run nixpkgs#opencode # ou github:anomalyco/opencode para a branch dev mais recente +``` + +> [!TIP] +> Remova versões anteriores a 0.1.x antes de instalar. + +### App desktop (BETA) + +O OpenCode também está disponível como aplicativo desktop. Baixe diretamente pela [página de releases](https://github.com/anomalyco/opencode/releases) ou em [opencode.ai/download](https://opencode.ai/download). + +| Plataforma | Download | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` ou AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Diretório de instalação + +O script de instalação respeita a seguinte ordem de prioridade para o caminho de instalação: + +1. `$OPENCODE_INSTALL_DIR` - Diretório de instalação personalizado +2. `$XDG_BIN_DIR` - Caminho compatível com a especificação XDG Base Directory +3. `$HOME/bin` - Diretório binário padrão do usuário (se existir ou puder ser criado) +4. `$HOME/.opencode/bin` - Fallback padrão + +```bash +# Exemplos +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +O OpenCode inclui dois agents integrados, que você pode alternar com a tecla `Tab`. + +- **build** - Padrão, agent com acesso total para trabalho de desenvolvimento +- **plan** - Agent somente leitura para análise e exploração de código + - Nega edições de arquivos por padrão + - Pede permissão antes de executar comandos bash + - Ideal para explorar codebases desconhecidas ou planejar mudanças + +Também há um subagent **general** para buscas complexas e tarefas em várias etapas. +Ele é usado internamente e pode ser invocado com `@general` nas mensagens. + +Saiba mais sobre [agents](https://opencode.ai/docs/agents). + +### Documentação + +Para mais informações sobre como configurar o OpenCode, [**veja nossa documentação**](https://opencode.ai/docs). + +### Contribuir + +Se você tem interesse em contribuir com o OpenCode, leia os [contributing docs](./CONTRIBUTING.md) antes de enviar um pull request. + +### Construindo com OpenCode + +Se você estiver trabalhando em um projeto relacionado ao OpenCode e estiver usando "opencode" como parte do nome (por exemplo, "opencode-dashboard" ou "opencode-mobile"), adicione uma nota no README para deixar claro que não foi construído pela equipe do OpenCode e não é afiliado a nós de nenhuma forma. + +### FAQ + +#### Como isso é diferente do Claude Code? + +É muito parecido com o Claude Code em termos de capacidade. Aqui estão as principais diferenças: + +- 100% open source +- Não está acoplado a nenhum provedor. Embora recomendemos os modelos que oferecemos pelo [OpenCode Zen](https://opencode.ai/zen); o OpenCode pode ser usado com Claude, OpenAI, Google ou até modelos locais. À medida que os modelos evoluem, as diferenças diminuem e os preços caem, então ser provider-agnostic é importante. +- Suporte a LSP pronto para uso +- Foco em TUI. O OpenCode é construído por usuários de neovim e pelos criadores do [terminal.shop](https://terminal.shop); vamos levar ao limite o que é possível no terminal. +- Arquitetura cliente/servidor. Isso, por exemplo, permite executar o OpenCode no seu computador enquanto você o controla remotamente por um aplicativo mobile. Isso significa que o frontend TUI é apenas um dos possíveis clientes. + +--- + +**Junte-se à nossa comunidade** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.da.md b/README.da.md new file mode 100644 index 00000000000..7116b8bd763 --- /dev/null +++ b/README.da.md @@ -0,0 +1,132 @@ +

+ + + + + OpenCode logo + + +

+

Den open source AI-kodeagent.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installation + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Pakkehåndteringer +npm i -g opencode-ai@latest # eller bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS og Linux (anbefalet, altid up to date) +brew install opencode # macOS og Linux (officiel brew formula, opdateres sjældnere) +paru -S opencode-bin # Arch Linux +mise use -g opencode # alle OS +nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch +``` + +> [!TIP] +> Fjern versioner ældre end 0.1.x før installation. + +### Desktop-app (BETA) + +OpenCode findes også som desktop-app. Download direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download). + +| Platform | Download | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, eller AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Installationsmappe + +Installationsscriptet bruger følgende prioriteringsrækkefølge for installationsstien: + +1. `$OPENCODE_INSTALL_DIR` - Tilpasset installationsmappe +2. `$XDG_BIN_DIR` - Sti der følger XDG Base Directory Specification +3. `$HOME/bin` - Standard bruger-bin-mappe (hvis den findes eller kan oprettes) +4. `$HOME/.opencode/bin` - Standard fallback + +```bash +# Eksempler +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode har to indbyggede agents, som du kan skifte mellem med `Tab`-tasten. + +- **build** - Standard, agent med fuld adgang til udviklingsarbejde +- **plan** - Skrivebeskyttet agent til analyse og kodeudforskning + - Afviser filredigering som standard + - Spørger om tilladelse før bash-kommandoer + - Ideel til at udforske ukendte kodebaser eller planlægge ændringer + +Derudover findes der en **general**-subagent til komplekse søgninger og flertrinsopgaver. +Den bruges internt og kan kaldes via `@general` i beskeder. + +Læs mere om [agents](https://opencode.ai/docs/agents). + +### Dokumentation + +For mere info om konfiguration af OpenCode, [**se vores docs**](https://opencode.ai/docs). + +### Bidrag + +Hvis du vil bidrage til OpenCode, så læs vores [contributing docs](./CONTRIBUTING.md) før du sender en pull request. + +### Bygget på OpenCode + +Hvis du arbejder på et projekt der er relateret til OpenCode og bruger "opencode" som en del af navnet; f.eks. "opencode-dashboard" eller "opencode-mobile", så tilføj en note i din README, der tydeliggør at projektet ikke er bygget af OpenCode-teamet og ikke er tilknyttet os på nogen måde. + +### FAQ + +#### Hvordan adskiller dette sig fra Claude Code? + +Det minder meget om Claude Code i forhold til funktionalitet. Her er de vigtigste forskelle: + +- 100% open source +- Ikke låst til en udbyder. Selvom vi anbefaler modellerne via [OpenCode Zen](https://opencode.ai/zen); kan OpenCode bruges med Claude, OpenAI, Google eller endda lokale modeller. Efterhånden som modeller udvikler sig vil forskellene mindskes og priserne falde, så det er vigtigt at være provider-agnostic. +- LSP-support out of the box +- Fokus på TUI. OpenCode er bygget af neovim-brugere og skaberne af [terminal.shop](https://terminal.shop); vi vil skubbe grænserne for hvad der er muligt i terminalen. +- Klient/server-arkitektur. Det kan f.eks. lade OpenCode køre på din computer, mens du styrer den eksternt fra en mobilapp. Det betyder at TUI-frontend'en kun er en af de mulige clients. + +--- + +**Bliv en del af vores community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.de.md b/README.de.md new file mode 100644 index 00000000000..fd903d91ddc --- /dev/null +++ b/README.de.md @@ -0,0 +1,132 @@ +

+ + + + + OpenCode logo + + +

+

Der Open-Source KI-Coding-Agent.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installation + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Paketmanager +npm i -g opencode-ai@latest # oder bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS und Linux (empfohlen, immer aktuell) +brew install opencode # macOS und Linux (offizielle Brew-Formula, seltener aktualisiert) +paru -S opencode-bin # Arch Linux +mise use -g opencode # jedes Betriebssystem +nix run nixpkgs#opencode # oder github:anomalyco/opencode für den neuesten dev-Branch +``` + +> [!TIP] +> Entferne Versionen älter als 0.1.x vor der Installation. + +### Desktop-App (BETA) + +OpenCode ist auch als Desktop-Anwendung verfügbar. Lade sie direkt von der [Releases-Seite](https://github.com/anomalyco/opencode/releases) oder [opencode.ai/download](https://opencode.ai/download) herunter. + +| Plattform | Download | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` oder AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Installationsverzeichnis + +Das Installationsskript beachtet die folgende Prioritätsreihenfolge für den Installationspfad: + +1. `$OPENCODE_INSTALL_DIR` - Benutzerdefiniertes Installationsverzeichnis +2. `$XDG_BIN_DIR` - XDG Base Directory Specification-konformer Pfad +3. `$HOME/bin` - Standard-Binärverzeichnis des Users (falls vorhanden oder erstellbar) +4. `$HOME/.opencode/bin` - Standard-Fallback + +```bash +# Beispiele +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode enthält zwei eingebaute Agents, zwischen denen du mit der `Tab`-Taste wechseln kannst. + +- **build** - Standard-Agent mit vollem Zugriff für Entwicklungsarbeit +- **plan** - Nur-Lese-Agent für Analyse und Code-Exploration + - Verweigert Datei-Edits standardmäßig + - Fragt vor dem Ausführen von bash-Befehlen nach + - Ideal zum Erkunden unbekannter Codebases oder zum Planen von Änderungen + +Außerdem ist ein **general**-Subagent für komplexe Suchen und mehrstufige Aufgaben enthalten. +Dieser wird intern genutzt und kann in Nachrichten mit `@general` aufgerufen werden. + +Mehr dazu unter [Agents](https://opencode.ai/docs/agents). + +### Dokumentation + +Mehr Infos zur Konfiguration von OpenCode findest du in unseren [**Docs**](https://opencode.ai/docs). + +### Beitragen + +Wenn du zu OpenCode beitragen möchtest, lies bitte unsere [Contributing Docs](./CONTRIBUTING.md), bevor du einen Pull Request einreichst. + +### Auf OpenCode aufbauen + +Wenn du an einem Projekt arbeitest, das mit OpenCode zusammenhängt und "opencode" als Teil seines Namens verwendet (z.B. "opencode-dashboard" oder "opencode-mobile"), füge bitte einen Hinweis in deine README ein, dass es nicht vom OpenCode-Team gebaut wird und nicht in irgendeiner Weise mit uns verbunden ist. + +### FAQ + +#### Worin unterscheidet sich das von Claude Code? + +In Bezug auf die Fähigkeiten ist es Claude Code sehr ähnlich. Hier sind die wichtigsten Unterschiede: + +- 100% open source +- Nicht an einen Anbieter gekoppelt. Wir empfehlen die Modelle aus [OpenCode Zen](https://opencode.ai/zen); OpenCode kann aber auch mit Claude, OpenAI, Google oder sogar lokalen Modellen genutzt werden. Mit der Weiterentwicklung der Modelle werden die Unterschiede kleiner und die Preise sinken, deshalb ist Provider-Unabhängigkeit wichtig. +- LSP-Unterstützung direkt nach dem Start +- Fokus auf TUI. OpenCode wird von Neovim-Nutzern und den Machern von [terminal.shop](https://terminal.shop) gebaut; wir treiben die Grenzen dessen, was im Terminal möglich ist. +- Client/Server-Architektur. Das ermöglicht z.B., OpenCode auf deinem Computer laufen zu lassen, während du es von einer mobilen App aus fernsteuerst. Das TUI-Frontend ist nur einer der möglichen Clients. + +--- + +**Tritt unserer Community bei** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.es.md b/README.es.md new file mode 100644 index 00000000000..15e54c0d32f --- /dev/null +++ b/README.es.md @@ -0,0 +1,132 @@ +

+ + + + + OpenCode logo + + +

+

El agente de programación con IA de código abierto.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Instalación + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Gestores de paquetes +npm i -g opencode-ai@latest # o bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS y Linux (recomendado, siempre al día) +brew install opencode # macOS y Linux (fórmula oficial de brew, se actualiza menos) +paru -S opencode-bin # Arch Linux +mise use -g opencode # cualquier sistema +nix run nixpkgs#opencode # o github:anomalyco/opencode para la rama dev más reciente +``` + +> [!TIP] +> Elimina versiones anteriores a 0.1.x antes de instalar. + +### App de escritorio (BETA) + +OpenCode también está disponible como aplicación de escritorio. Descárgala directamente desde la [página de releases](https://github.com/anomalyco/opencode/releases) o desde [opencode.ai/download](https://opencode.ai/download). + +| Plataforma | Descarga | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, o AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Directorio de instalación + +El script de instalación respeta el siguiente orden de prioridad para la ruta de instalación: + +1. `$OPENCODE_INSTALL_DIR` - Directorio de instalación personalizado +2. `$XDG_BIN_DIR` - Ruta compatible con la especificación XDG Base Directory +3. `$HOME/bin` - Directorio binario estándar del usuario (si existe o se puede crear) +4. `$HOME/.opencode/bin` - Alternativa por defecto + +```bash +# Ejemplos +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode incluye dos agents integrados que puedes alternar con la tecla `Tab`. + +- **build** - Por defecto, agent con acceso completo para trabajo de desarrollo +- **plan** - Agent de solo lectura para análisis y exploración de código + - Niega ediciones de archivos por defecto + - Pide permiso antes de ejecutar comandos bash + - Ideal para explorar codebases desconocidas o planificar cambios + +Además, incluye un subagent **general** para búsquedas complejas y tareas de varios pasos. +Se usa internamente y se puede invocar con `@general` en los mensajes. + +Más información sobre [agents](https://opencode.ai/docs/agents). + +### Documentación + +Para más información sobre cómo configurar OpenCode, [**ve a nuestra documentación**](https://opencode.ai/docs). + +### Contribuir + +Si te interesa contribuir a OpenCode, lee nuestras [docs de contribución](./CONTRIBUTING.md) antes de enviar un pull request. + +### Construyendo sobre OpenCode + +Si estás trabajando en un proyecto relacionado con OpenCode y usas "opencode" como parte del nombre; por ejemplo, "opencode-dashboard" u "opencode-mobile", agrega una nota en tu README para aclarar que no está construido por el equipo de OpenCode y que no está afiliado con nosotros de ninguna manera. + +### FAQ + +#### ¿En qué se diferencia de Claude Code? + +Es muy similar a Claude Code en cuanto a capacidades. Estas son las diferencias clave: + +- 100% open source +- No está acoplado a ningún proveedor. Aunque recomendamos los modelos que ofrecemos a través de [OpenCode Zen](https://opencode.ai/zen); OpenCode se puede usar con Claude, OpenAI, Google o incluso modelos locales. A medida que evolucionan los modelos, las brechas se cerrarán y los precios bajarán, por lo que ser agnóstico al proveedor es importante. +- Soporte LSP listo para usar +- Un enfoque en la TUI. OpenCode está construido por usuarios de neovim y los creadores de [terminal.shop](https://terminal.shop); vamos a empujar los límites de lo que es posible en la terminal. +- Arquitectura cliente/servidor. Esto, por ejemplo, permite ejecutar OpenCode en tu computadora mientras lo controlas de forma remota desde una app móvil. Esto significa que el frontend TUI es solo uno de los posibles clientes. + +--- + +**Únete a nuestra comunidad** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 00000000000..d99d4a61e2b --- /dev/null +++ b/README.fr.md @@ -0,0 +1,132 @@ +

+ + + + + Logo OpenCode + + +

+

L'agent de codage IA open source.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installation + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Gestionnaires de paquets +npm i -g opencode-ai@latest # ou bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS et Linux (recommandé, toujours à jour) +brew install opencode # macOS et Linux (formule officielle brew, mise à jour moins fréquente) +paru -S opencode-bin # Arch Linux +mise use -g opencode # n'importe quel OS +nix run nixpkgs#opencode # ou github:anomalyco/opencode pour la branche dev la plus récente +``` + +> [!TIP] +> Supprimez les versions antérieures à 0.1.x avant d'installer. + +### Application de bureau (BETA) + +OpenCode est aussi disponible en application de bureau. Téléchargez-la directement depuis la [page des releases](https://github.com/anomalyco/opencode/releases) ou [opencode.ai/download](https://opencode.ai/download). + +| Plateforme | Téléchargement | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, ou AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Répertoire d'installation + +Le script d'installation respecte l'ordre de priorité suivant pour le chemin d'installation : + +1. `$OPENCODE_INSTALL_DIR` - Répertoire d'installation personnalisé +2. `$XDG_BIN_DIR` - Chemin conforme à la spécification XDG Base Directory +3. `$HOME/bin` - Répertoire binaire utilisateur standard (s'il existe ou peut être créé) +4. `$HOME/.opencode/bin` - Repli par défaut + +```bash +# Exemples +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode inclut deux agents intégrés que vous pouvez basculer avec la touche `Tab`. + +- **build** - Par défaut, agent avec accès complet pour le travail de développement +- **plan** - Agent en lecture seule pour l'analyse et l'exploration du code + - Refuse les modifications de fichiers par défaut + - Demande l'autorisation avant d'exécuter des commandes bash + - Idéal pour explorer une base de code inconnue ou planifier des changements + +Un sous-agent **general** est aussi inclus pour les recherches complexes et les tâches en plusieurs étapes. +Il est utilisé en interne et peut être invoqué via `@general` dans les messages. + +En savoir plus sur les [agents](https://opencode.ai/docs/agents). + +### Documentation + +Pour plus d'informations sur la configuration d'OpenCode, [**consultez notre documentation**](https://opencode.ai/docs). + +### Contribuer + +Si vous souhaitez contribuer à OpenCode, lisez nos [docs de contribution](./CONTRIBUTING.md) avant de soumettre une pull request. + +### Construire avec OpenCode + +Si vous travaillez sur un projet lié à OpenCode et que vous utilisez "opencode" dans le nom du projet (par exemple, "opencode-dashboard" ou "opencode-mobile"), ajoutez une note dans votre README pour préciser qu'il n'est pas construit par l'équipe OpenCode et qu'il n'est pas affilié à nous. + +### FAQ + +#### En quoi est-ce différent de Claude Code ? + +C'est très similaire à Claude Code en termes de capacités. Voici les principales différences : + +- 100% open source +- Pas couplé à un fournisseur. Nous recommandons les modèles proposés via [OpenCode Zen](https://opencode.ai/zen) ; OpenCode peut être utilisé avec Claude, OpenAI, Google ou même des modèles locaux. Au fur et à mesure que les modèles évoluent, les écarts se réduiront et les prix baisseront, donc être agnostique au fournisseur est important. +- Support LSP prêt à l'emploi +- Un focus sur la TUI. OpenCode est construit par des utilisateurs de neovim et les créateurs de [terminal.shop](https://terminal.shop) ; nous allons repousser les limites de ce qui est possible dans le terminal. +- Architecture client/serveur. Cela permet par exemple de faire tourner OpenCode sur votre ordinateur tout en le pilotant à distance depuis une application mobile. Cela signifie que la TUI n'est qu'un des clients possibles. + +--- + +**Rejoignez notre communauté** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 00000000000..b22e9106b98 --- /dev/null +++ b/README.ja.md @@ -0,0 +1,132 @@ +

+ + + + + OpenCode logo + + +

+

オープンソースのAIコーディングエージェント。

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### インストール + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# パッケージマネージャー +npm i -g opencode-ai@latest # bun/pnpm/yarn でもOK +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS と Linux(推奨。常に最新) +brew install opencode # macOS と Linux(公式 brew formula。更新頻度は低め) +paru -S opencode-bin # Arch Linux +mise use -g opencode # どのOSでも +nix run nixpkgs#opencode # または github:anomalyco/opencode で最新 dev ブランチ +``` + +> [!TIP] +> インストール前に 0.1.x より古いバージョンを削除してください。 + +### デスクトップアプリ (BETA) + +OpenCode はデスクトップアプリとしても利用できます。[releases page](https://github.com/anomalyco/opencode/releases) から直接ダウンロードするか、[opencode.ai/download](https://opencode.ai/download) を利用してください。 + +| プラットフォーム | ダウンロード | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`、`.rpm`、または AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### インストールディレクトリ + +インストールスクリプトは、インストール先パスを次の優先順位で決定します。 + +1. `$OPENCODE_INSTALL_DIR` - カスタムのインストールディレクトリ +2. `$XDG_BIN_DIR` - XDG Base Directory Specification に準拠したパス +3. `$HOME/bin` - 標準のユーザー用バイナリディレクトリ(存在する場合、または作成できる場合) +4. `$HOME/.opencode/bin` - デフォルトのフォールバック + +```bash +# 例 +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode には組み込みの Agent が2つあり、`Tab` キーで切り替えられます。 + +- **build** - デフォルト。開発向けのフルアクセス Agent +- **plan** - 分析とコード探索向けの読み取り専用 Agent + - デフォルトでファイル編集を拒否 + - bash コマンド実行前に確認 + - 未知のコードベース探索や変更計画に最適 + +また、複雑な検索やマルチステップのタスク向けに **general** サブ Agent も含まれています。 +内部的に使用されており、メッセージで `@general` と入力して呼び出せます。 + +[agents](https://opencode.ai/docs/agents) の詳細はこちら。 + +### ドキュメント + +OpenCode の設定については [**ドキュメント**](https://opencode.ai/docs) を参照してください。 + +### コントリビュート + +OpenCode に貢献したい場合は、Pull Request を送る前に [contributing docs](./CONTRIBUTING.md) を読んでください。 + +### OpenCode の上に構築する + +OpenCode に関連するプロジェクトで、名前に "opencode"(例: "opencode-dashboard" や "opencode-mobile")を含める場合は、そのプロジェクトが OpenCode チームによって作られたものではなく、いかなる形でも関係がないことを README に明記してください。 + +### FAQ + +#### Claude Code との違いは? + +機能面では Claude Code と非常に似ています。主な違いは次のとおりです。 + +- 100% オープンソース +- 特定のプロバイダーに依存しません。[OpenCode Zen](https://opencode.ai/zen) で提供しているモデルを推奨しますが、OpenCode は Claude、OpenAI、Google、またはローカルモデルでも利用できます。モデルが進化すると差は縮まり価格も下がるため、provider-agnostic であることが重要です。 +- そのまま使える LSP サポート +- TUI にフォーカス。OpenCode は neovim ユーザーと [terminal.shop](https://terminal.shop) の制作者によって作られており、ターミナルで可能なことの限界を押し広げます。 +- クライアント/サーバー構成。例えば OpenCode をあなたのPCで動かし、モバイルアプリからリモート操作できます。TUI フロントエンドは複数あるクライアントの1つにすぎません。 + +--- + +**コミュニティに参加** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 00000000000..3b3100a2745 --- /dev/null +++ b/README.ko.md @@ -0,0 +1,132 @@ +

+ + + + + OpenCode logo + + +

+

오픈 소스 AI 코딩 에이전트.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### 설치 + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# 패키지 매니저 +npm i -g opencode-ai@latest # bun/pnpm/yarn 도 가능 +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS 및 Linux (권장, 항상 최신) +brew install opencode # macOS 및 Linux (공식 brew formula, 업데이트 빈도 낮음) +paru -S opencode-bin # Arch Linux +mise use -g opencode # 어떤 OS든 +nix run nixpkgs#opencode # 또는 github:anomalyco/opencode 로 최신 dev 브랜치 +``` + +> [!TIP] +> 설치 전에 0.1.x 보다 오래된 버전을 제거하세요. + +### 데스크톱 앱 (BETA) + +OpenCode 는 데스크톱 앱으로도 제공됩니다. [releases page](https://github.com/anomalyco/opencode/releases) 에서 직접 다운로드하거나 [opencode.ai/download](https://opencode.ai/download) 를 이용하세요. + +| 플랫폼 | 다운로드 | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, 또는 AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### 설치 디렉터리 + +설치 스크립트는 설치 경로를 다음 우선순위로 결정합니다. + +1. `$OPENCODE_INSTALL_DIR` - 사용자 지정 설치 디렉터리 +2. `$XDG_BIN_DIR` - XDG Base Directory Specification 준수 경로 +3. `$HOME/bin` - 표준 사용자 바이너리 디렉터리 (존재하거나 생성 가능할 경우) +4. `$HOME/.opencode/bin` - 기본 폴백 + +```bash +# 예시 +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode 에는 내장 에이전트 2개가 있으며 `Tab` 키로 전환할 수 있습니다. + +- **build** - 기본값, 개발 작업을 위한 전체 권한 에이전트 +- **plan** - 분석 및 코드 탐색을 위한 읽기 전용 에이전트 + - 기본적으로 파일 편집을 거부 + - bash 명령 실행 전에 권한을 요청 + - 낯선 코드베이스를 탐색하거나 변경을 계획할 때 적합 + +또한 복잡한 검색과 여러 단계 작업을 위한 **general** 서브 에이전트가 포함되어 있습니다. +내부적으로 사용되며, 메시지에서 `@general` 로 호출할 수 있습니다. + +[agents](https://opencode.ai/docs/agents) 에 대해 더 알아보세요. + +### 문서 + +OpenCode 설정에 대한 자세한 내용은 [**문서**](https://opencode.ai/docs) 를 참고하세요. + +### 기여하기 + +OpenCode 에 기여하고 싶다면, Pull Request 를 제출하기 전에 [contributing docs](./CONTRIBUTING.md) 를 읽어주세요. + +### OpenCode 기반으로 만들기 + +OpenCode 와 관련된 프로젝트를 진행하면서 이름에 "opencode"(예: "opencode-dashboard" 또는 "opencode-mobile") 를 포함한다면, README 에 해당 프로젝트가 OpenCode 팀이 만든 것이 아니며 어떤 방식으로도 우리와 제휴되어 있지 않다는 점을 명시해 주세요. + +### FAQ + +#### Claude Code 와는 무엇이 다른가요? + +기능 면에서는 Claude Code 와 매우 유사합니다. 주요 차이점은 다음과 같습니다. + +- 100% 오픈 소스 +- 특정 제공자에 묶여 있지 않습니다. [OpenCode Zen](https://opencode.ai/zen) 을 통해 제공하는 모델을 권장하지만, OpenCode 는 Claude, OpenAI, Google 또는 로컬 모델과도 사용할 수 있습니다. 모델이 발전하면서 격차는 줄고 가격은 내려가므로 provider-agnostic 인 것이 중요합니다. +- 기본으로 제공되는 LSP 지원 +- TUI 에 집중. OpenCode 는 neovim 사용자와 [terminal.shop](https://terminal.shop) 제작자가 만들었으며, 터미널에서 가능한 것의 한계를 밀어붙입니다. +- 클라이언트/서버 아키텍처. 예를 들어 OpenCode 를 내 컴퓨터에서 실행하면서 모바일 앱으로 원격 조작할 수 있습니다. 즉, TUI 프런트엔드는 가능한 여러 클라이언트 중 하나일 뿐입니다. + +--- + +**커뮤니티에 참여하기** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.md b/README.md index 64ca1ef7a6f..a7350912a7d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,23 @@ Build status

+

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ [![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) --- diff --git a/README.no.md b/README.no.md new file mode 100644 index 00000000000..67c5d2d579a --- /dev/null +++ b/README.no.md @@ -0,0 +1,132 @@ +

+ + + + + OpenCode logo + + +

+

AI-kodeagent med åpen kildekode.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installasjon + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Pakkehåndterere +npm i -g opencode-ai@latest # eller bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS og Linux (anbefalt, alltid oppdatert) +brew install opencode # macOS og Linux (offisiell brew-formel, oppdateres sjeldnere) +paru -S opencode-bin # Arch Linux +mise use -g opencode # alle OS +nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch +``` + +> [!TIP] +> Fjern versjoner eldre enn 0.1.x før du installerer. + +### Desktop-app (BETA) + +OpenCode er også tilgjengelig som en desktop-app. Last ned direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download). + +| Plattform | Nedlasting | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` eller AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Installasjonsmappe + +Installasjonsskriptet bruker følgende prioritet for installasjonsstien: + +1. `$OPENCODE_INSTALL_DIR` - Egendefinert installasjonsmappe +2. `$XDG_BIN_DIR` - Sti som følger XDG Base Directory Specification +3. `$HOME/bin` - Standard brukerbinar-mappe (hvis den finnes eller kan opprettes) +4. `$HOME/.opencode/bin` - Standard fallback + +```bash +# Eksempler +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode har to innebygde agents du kan bytte mellom med `Tab`-tasten. + +- **build** - Standard, agent med full tilgang for utviklingsarbeid +- **plan** - Skrivebeskyttet agent for analyse og kodeutforsking + - Nekter filendringer som standard + - Spør om tillatelse før bash-kommandoer + - Ideell for å utforske ukjente kodebaser eller planlegge endringer + +Det finnes også en **general**-subagent for komplekse søk og flertrinnsoppgaver. +Den brukes internt og kan kalles via `@general` i meldinger. + +Les mer om [agents](https://opencode.ai/docs/agents). + +### Dokumentasjon + +For mer info om hvordan du konfigurerer OpenCode, [**se dokumentasjonen**](https://opencode.ai/docs). + +### Bidra + +Hvis du vil bidra til OpenCode, les [contributing docs](./CONTRIBUTING.md) før du sender en pull request. + +### Bygge på OpenCode + +Hvis du jobber med et prosjekt som er relatert til OpenCode og bruker "opencode" som en del av navnet; for eksempel "opencode-dashboard" eller "opencode-mobile", legg inn en merknad i README som presiserer at det ikke er bygget av OpenCode-teamet og ikke er tilknyttet oss på noen måte. + +### FAQ + +#### Hvordan er dette forskjellig fra Claude Code? + +Det er veldig likt Claude Code når det gjelder funksjonalitet. Her er de viktigste forskjellene: + +- 100% open source +- Ikke knyttet til en bestemt leverandør. Selv om vi anbefaler modellene vi tilbyr gjennom [OpenCode Zen](https://opencode.ai/zen); kan OpenCode brukes med Claude, OpenAI, Google eller til og med lokale modeller. Etter hvert som modellene utvikler seg vil gapene lukkes og prisene gå ned, så det er viktig å være provider-agnostic. +- LSP-støtte rett ut av boksen +- Fokus på TUI. OpenCode er bygget av neovim-brukere og skaperne av [terminal.shop](https://terminal.shop); vi kommer til å presse grensene for hva som er mulig i terminalen. +- Klient/server-arkitektur. Dette kan for eksempel la OpenCode kjøre på maskinen din, mens du styrer den eksternt fra en mobilapp. Det betyr at TUI-frontend'en bare er en av de mulige klientene. + +--- + +**Bli med i fellesskapet** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.pl.md b/README.pl.md new file mode 100644 index 00000000000..abcf1fc8941 --- /dev/null +++ b/README.pl.md @@ -0,0 +1,132 @@ +

+ + + + + OpenCode logo + + +

+

Otwartoźródłowy agent kodujący AI.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Instalacja + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Menedżery pakietów +npm i -g opencode-ai@latest # albo bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS i Linux (polecane, zawsze aktualne) +brew install opencode # macOS i Linux (oficjalna formuła brew, rzadziej aktualizowana) +paru -S opencode-bin # Arch Linux +mise use -g opencode # dowolny system +nix run nixpkgs#opencode # lub github:anomalyco/opencode dla najnowszej gałęzi dev +``` + +> [!TIP] +> Przed instalacją usuń wersje starsze niż 0.1.x. + +### Aplikacja desktopowa (BETA) + +OpenCode jest także dostępny jako aplikacja desktopowa. Pobierz ją bezpośrednio ze strony [releases](https://github.com/anomalyco/opencode/releases) lub z [opencode.ai/download](https://opencode.ai/download). + +| Platforma | Pobieranie | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` lub AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Katalog instalacji + +Skrypt instalacyjny stosuje następujący priorytet wyboru ścieżki instalacji: + +1. `$OPENCODE_INSTALL_DIR` - Własny katalog instalacji +2. `$XDG_BIN_DIR` - Ścieżka zgodna ze specyfikacją XDG Base Directory +3. `$HOME/bin` - Standardowy katalog binarny użytkownika (jeśli istnieje lub można go utworzyć) +4. `$HOME/.opencode/bin` - Domyślny fallback + +```bash +# Przykłady +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode zawiera dwóch wbudowanych agentów, między którymi możesz przełączać się klawiszem `Tab`. + +- **build** - Domyślny agent z pełnym dostępem do pracy developerskiej +- **plan** - Agent tylko do odczytu do analizy i eksploracji kodu + - Domyślnie odmawia edycji plików + - Pyta o zgodę przed uruchomieniem komend bash + - Idealny do poznawania nieznanych baz kodu lub planowania zmian + +Dodatkowo jest subagent **general** do złożonych wyszukiwań i wieloetapowych zadań. +Jest używany wewnętrznie i można go wywołać w wiadomościach przez `@general`. + +Dowiedz się więcej o [agents](https://opencode.ai/docs/agents). + +### Dokumentacja + +Więcej informacji o konfiguracji OpenCode znajdziesz w [**dokumentacji**](https://opencode.ai/docs). + +### Współtworzenie + +Jeśli chcesz współtworzyć OpenCode, przeczytaj [contributing docs](./CONTRIBUTING.md) przed wysłaniem pull requesta. + +### Budowanie na OpenCode + +Jeśli pracujesz nad projektem związanym z OpenCode i używasz "opencode" jako części nazwy (na przykład "opencode-dashboard" lub "opencode-mobile"), dodaj proszę notatkę do swojego README, aby wyjaśnić, że projekt nie jest tworzony przez zespół OpenCode i nie jest z nami w żaden sposób powiązany. + +### FAQ + +#### Czym to się różni od Claude Code? + +Jest bardzo podobne do Claude Code pod względem możliwości. Oto kluczowe różnice: + +- 100% open source +- Niezależne od dostawcy. Chociaż polecamy modele oferowane przez [OpenCode Zen](https://opencode.ai/zen); OpenCode może być używany z Claude, OpenAI, Google, a nawet z modelami lokalnymi. W miarę jak modele ewoluują, różnice będą się zmniejszać, a ceny spadać, więc ważna jest niezależność od dostawcy. +- Wbudowane wsparcie LSP +- Skupienie na TUI. OpenCode jest budowany przez użytkowników neovim i twórców [terminal.shop](https://terminal.shop); przesuwamy granice tego, co jest możliwe w terminalu. +- Architektura klient/serwer. Pozwala np. uruchomić OpenCode na twoim komputerze, a sterować nim zdalnie z aplikacji mobilnej. To znaczy, że frontend TUI jest tylko jednym z możliwych klientów. + +--- + +**Dołącz do naszej społeczności** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 00000000000..a9b0b108fb7 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,132 @@ +

+ + + + + OpenCode logo + + +

+

Открытый AI-агент для программирования.

+

+ Discord + npm + Build status +

+ +

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Установка + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Менеджеры пакетов +npm i -g opencode-ai@latest # или bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS и Linux (рекомендуем, всегда актуально) +brew install opencode # macOS и Linux (официальная формула brew, обновляется реже) +paru -S opencode-bin # Arch Linux +mise use -g opencode # любая ОС +nix run nixpkgs#opencode # или github:anomalyco/opencode для самой свежей ветки dev +``` + +> [!TIP] +> Перед установкой удалите версии старше 0.1.x. + +### Десктопное приложение (BETA) + +OpenCode также доступен как десктопное приложение. Скачайте его со [страницы релизов](https://github.com/anomalyco/opencode/releases) или с [opencode.ai/download](https://opencode.ai/download). + +| Платформа | Загрузка | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` или AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Каталог установки + +Скрипт установки выбирает путь установки в следующем порядке приоритета: + +1. `$OPENCODE_INSTALL_DIR` - Пользовательский каталог установки +2. `$XDG_BIN_DIR` - Путь, совместимый со спецификацией XDG Base Directory +3. `$HOME/bin` - Стандартный каталог пользовательских бинарников (если существует или можно создать) +4. `$HOME/.opencode/bin` - Fallback по умолчанию + +```bash +# Примеры +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +В OpenCode есть два встроенных агента, между которыми можно переключаться клавишей `Tab`. + +- **build** - По умолчанию, агент с полным доступом для разработки +- **plan** - Агент только для чтения для анализа и изучения кода + - По умолчанию запрещает редактирование файлов + - Запрашивает разрешение перед выполнением bash-команд + - Идеален для изучения незнакомых кодовых баз или планирования изменений + +Также включен сабагент **general** для сложных поисков и многошаговых задач. +Он используется внутренне и может быть вызван в сообщениях через `@general`. + +Подробнее об [agents](https://opencode.ai/docs/agents). + +### Документация + +Больше информации о том, как настроить OpenCode: [**наши docs**](https://opencode.ai/docs). + +### Вклад + +Если вы хотите внести вклад в OpenCode, прочитайте [contributing docs](./CONTRIBUTING.md) перед тем, как отправлять pull request. + +### Разработка на базе OpenCode + +Если вы делаете проект, связанный с OpenCode, и используете "opencode" как часть имени (например, "opencode-dashboard" или "opencode-mobile"), добавьте примечание в README, чтобы уточнить, что проект не создан командой OpenCode и не аффилирован с нами. + +### FAQ + +#### Чем это отличается от Claude Code? + +По возможностям это очень похоже на Claude Code. Вот ключевые отличия: + +- 100% open source +- Не привязано к одному провайдеру. Мы рекомендуем модели из [OpenCode Zen](https://opencode.ai/zen); но OpenCode можно использовать с Claude, OpenAI, Google или даже локальными моделями. По мере развития моделей разрыв будет сокращаться, а цены падать, поэтому важна независимость от провайдера. +- Поддержка LSP из коробки +- Фокус на TUI. OpenCode построен пользователями neovim и создателями [terminal.shop](https://terminal.shop); мы будем раздвигать границы того, что возможно в терминале. +- Архитектура клиент/сервер. Например, это позволяет запускать OpenCode на вашем компьютере, а управлять им удаленно из мобильного приложения. Это значит, что TUI-фронтенд - лишь один из возможных клиентов. + +--- + +**Присоединяйтесь к нашему сообществу** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.zh-CN.md b/README.zh.md similarity index 88% rename from README.zh-CN.md rename to README.zh.md index 4b56e0fb0b0..9f6b613b14f 100644 --- a/README.zh-CN.md +++ b/README.zh.md @@ -14,6 +14,23 @@ Build status

+

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ [![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) --- @@ -109,10 +126,6 @@ OpenCode 内置两种 Agent,可用 `Tab` 键快速切换: - 聚焦终端界面 (TUI)。OpenCode 由 Neovim 爱好者和 [terminal.shop](https://terminal.shop) 的创建者打造,会持续探索终端的极限。 - 客户端/服务器架构。可在本机运行,同时用移动设备远程驱动。TUI 只是众多潜在客户端之一。 -#### 另一个同名的仓库是什么? - -另一个名字相近的仓库与本项目无关。[点击这里了解背后故事](https://x.com/thdxr/status/1933561254481666466)。 - --- **加入我们的社区** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.zh-TW.md b/README.zht.md similarity index 89% rename from README.zh-TW.md rename to README.zht.md index 66664a70305..0d6fff4e55b 100644 --- a/README.zh-TW.md +++ b/README.zht.md @@ -14,6 +14,23 @@ Build status

+

+ English | + Chinese (Simplified) | + Chinese (Traditional) | + Korean | + German | + Spanish | + French | + Danish | + Japanese | + Polish | + Russian | + Arabic | + Norwegian | + Portuguese (Brazil) +

+ [![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) --- @@ -31,7 +48,7 @@ choco install opencode # Windows brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新) brew install opencode # macOS 與 Linux(官方 brew formula,更新頻率較低) paru -S opencode-bin # Arch Linux -mise use -g github:anomalyco/opencode # 任何作業系統 +mise use -g opencode # 任何作業系統 nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支 ``` @@ -109,10 +126,6 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。 - 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。 - 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。 -#### 另一個同名的 Repo 是什麼? - -另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。 - --- **加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) From a84843507fd83f4a512f9deb063435505b7a5902 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:53:50 -0600 Subject: [PATCH 008/232] chore: readme links --- README.ar.md | 28 ++++++++++++++-------------- README.br.md | 28 ++++++++++++++-------------- README.da.md | 28 ++++++++++++++-------------- README.de.md | 28 ++++++++++++++-------------- README.es.md | 28 ++++++++++++++-------------- README.fr.md | 28 ++++++++++++++-------------- README.ja.md | 28 ++++++++++++++-------------- README.ko.md | 28 ++++++++++++++-------------- README.no.md | 28 ++++++++++++++-------------- README.pl.md | 28 ++++++++++++++-------------- README.ru.md | 28 ++++++++++++++-------------- README.zh.md | 28 ++++++++++++++-------------- README.zht.md | 28 ++++++++++++++-------------- 13 files changed, 182 insertions(+), 182 deletions(-) diff --git a/README.ar.md b/README.ar.md index 3bcaf3272a7..41ea7efbd37 100644 --- a/README.ar.md +++ b/README.ar.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + الإنجليزية | + الصينية (المبسطة) | + الصينية (التقليدية) | + الكورية | + الألمانية | + الإسبانية | + الفرنسية | + الدنماركية | + اليابانية | + البولندية | + الروسية | + العربية | + النرويجية | + البرتغالية (البرازيل)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.br.md b/README.br.md index 8c6527f2023..3618ae55ede 100644 --- a/README.br.md +++ b/README.br.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + Inglês | + Chinês (Simplificado) | + Chinês (Tradicional) | + Coreano | + Alemão | + Espanhol | + Francês | + Dinamarquês | + Japonês | + Polonês | + Russo | + Árabe | + Norueguês | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.da.md b/README.da.md index 7116b8bd763..d4eaeb19751 100644 --- a/README.da.md +++ b/README.da.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + Engelsk | + Kinesisk (forenklet) | + Kinesisk (traditionelt) | + Koreansk | + Tysk | + Spansk | + Fransk | + Dansk | + Japansk | + Polsk | + Russisk | + Arabisk | + Norsk | + Portugisisk (Brasilien)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.de.md b/README.de.md index fd903d91ddc..dcea2c7ed24 100644 --- a/README.de.md +++ b/README.de.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + Englisch | + Chinesisch (vereinfacht) | + Chinesisch (traditionell) | + Koreanisch | + Deutsch | + Spanisch | + Französisch | + Dänisch | + Japanisch | + Polnisch | + Russisch | + Arabisch | + Norwegisch | + Portugiesisch (Brasilien)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.es.md b/README.es.md index 15e54c0d32f..2ea9be28b17 100644 --- a/README.es.md +++ b/README.es.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + Inglés | + Chino (simplificado) | + Chino (tradicional) | + Coreano | + Alemán | + Español | + Francés | + Danés | + Japonés | + Polaco | + Ruso | + Árabe | + Noruego | + Portugués (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.fr.md b/README.fr.md index d99d4a61e2b..1adf073bf3c 100644 --- a/README.fr.md +++ b/README.fr.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + Anglais | + Chinois (simplifié) | + Chinois (traditionnel) | + Coréen | + Allemand | + Espagnol | + Français | + Danois | + Japonais | + Polonais | + Russe | + Arabe | + Norvégien | + Portugais (Brésil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ja.md b/README.ja.md index b22e9106b98..37d2d179c87 100644 --- a/README.ja.md +++ b/README.ja.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + 英語 | + 中国語(簡体字) | + 中国語(繁体字) | + 韓国語 | + ドイツ語 | + スペイン語 | + フランス語 | + デンマーク語 | + 日本語 | + ポーランド語 | + ロシア語 | + アラビア語 | + ノルウェー語 | + ポルトガル語(ブラジル)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ko.md b/README.ko.md index 3b3100a2745..b533f7efe58 100644 --- a/README.ko.md +++ b/README.ko.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + 영어 | + 중국어(간체) | + 중국어(번체) | + 한국어 | + 독일어 | + 스페인어 | + 프랑스어 | + 덴마크어 | + 일본어 | + 폴란드어 | + 러시아어 | + 아랍어 | + 노르웨이어 | + 포르투갈어(브라질)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.no.md b/README.no.md index 67c5d2d579a..95a7a317711 100644 --- a/README.no.md +++ b/README.no.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + Engelsk | + Kinesisk (forenklet) | + Kinesisk (tradisjonell) | + Koreansk | + Tysk | + Spansk | + Fransk | + Dansk | + Japansk | + Polsk | + Russisk | + Arabisk | + Norsk | + Portugisisk (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.pl.md b/README.pl.md index abcf1fc8941..df650cac3d4 100644 --- a/README.pl.md +++ b/README.pl.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + Angielski | + Chiński (uproszczony) | + Chiński (tradycyjny) | + Koreański | + Niemiecki | + Hiszpański | + Francuski | + Duński | + Japoński | + Polski | + Rosyjski | + Arabski | + Norweski | + Portugalski (Brazylia)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ru.md b/README.ru.md index a9b0b108fb7..3fabcc9d7c4 100644 --- a/README.ru.md +++ b/README.ru.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + Английский | + Китайский (упрощенный) | + Китайский (традиционный) | + Корейский | + Немецкий | + Испанский | + Французский | + Датский | + Японский | + Польский | + Русский | + Арабский | + Норвежский | + Португальский (Бразилия)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.zh.md b/README.zh.md index 9f6b613b14f..2ce826b76f5 100644 --- a/README.zh.md +++ b/README.zh.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + 英语 | + 简体中文 | + 繁体中文 | + 韩语 | + 德语 | + 西班牙语 | + 法语 | + 丹麦语 | + 日语 | + 波兰语 | + 俄语 | + 阿拉伯语 | + 挪威语 | + 葡萄牙语(巴西)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.zht.md b/README.zht.md index 0d6fff4e55b..2fb443502c9 100644 --- a/README.zht.md +++ b/README.zht.md @@ -15,20 +15,20 @@

- English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + 英語 | + 簡體中文 | + 繁體中文 | + 韓語 | + 德語 | + 西班牙語 | + 法語 | + 丹麥語 | + 日語 | + 波蘭語 | + 俄語 | + 阿拉伯語 | + 挪威語 | + 葡萄牙語(巴西)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) From 94dd0a8dbebd8c168a13198d657ba170a87fe0f7 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 25 Jan 2026 17:27:15 -0500 Subject: [PATCH 009/232] ignore: rm spoof and bump plugin version --- packages/opencode/src/agent/agent.ts | 5 +++-- packages/opencode/src/global/index.ts | 2 +- packages/opencode/src/plugin/index.ts | 2 +- packages/opencode/src/session/llm.ts | 8 ++++++-- packages/opencode/src/session/prompt/anthropic_spoof.txt | 1 - packages/opencode/src/session/system.ts | 5 ----- packages/plugin/src/index.ts | 2 +- 7 files changed, 12 insertions(+), 13 deletions(-) delete mode 100644 packages/opencode/src/session/prompt/anthropic_spoof.txt diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 2b44308f130..19ac607cebf 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -17,6 +17,7 @@ import { PermissionNext } from "@/permission/next" import { mergeDeep, pipe, sortBy, values } from "remeda" import { Global } from "@/global" import path from "path" +import { Plugin } from "@/plugin" export namespace Agent { export const Info = z @@ -279,8 +280,8 @@ export namespace Agent { const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID) const language = await Provider.getLanguage(model) - const system = SystemPrompt.header(defaultModel.providerID) - system.push(PROMPT_GENERATE) + const system = [PROMPT_GENERATE] + await Plugin.trigger("experimental.chat.system.transform", { model }, { system }) const existing = await list() const params = { diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index d3011b41506..ade3e5d5295 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -33,7 +33,7 @@ await Promise.all([ fs.mkdir(Global.Path.bin, { recursive: true }), ]) -const CACHE_VERSION = "18" +const CACHE_VERSION = "19" const version = await Bun.file(path.join(Global.Path.cache, "version")) .text() diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 76d34b845ae..691fff4b2f8 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -15,7 +15,7 @@ import { CopilotAuthPlugin } from "./copilot" export namespace Plugin { const log = Log.create({ service: "plugin" }) - const BUILTIN = ["opencode-anthropic-auth@0.0.9", "@gitlab/opencode-gitlab-auth@1.3.2"] + const BUILTIN = ["opencode-anthropic-auth@0.0.10", "@gitlab/opencode-gitlab-auth@1.3.2"] // Built-in plugins that are directly imported (not installed from npm) const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin] diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 55c9c452473..1e409b03fe6 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -66,7 +66,7 @@ export namespace LLM { ]) const isCodex = provider.id === "openai" && auth?.type === "oauth" - const system = SystemPrompt.header(input.model.providerID) + const system = [] system.push( [ // use agent prompt otherwise provider prompt @@ -83,7 +83,11 @@ export namespace LLM { const header = system[0] const original = clone(system) - await Plugin.trigger("experimental.chat.system.transform", { sessionID: input.sessionID }, { system }) + await Plugin.trigger( + "experimental.chat.system.transform", + { sessionID: input.sessionID, model: input.model }, + { system }, + ) if (system.length === 0) { system.push(...original) } diff --git a/packages/opencode/src/session/prompt/anthropic_spoof.txt b/packages/opencode/src/session/prompt/anthropic_spoof.txt deleted file mode 100644 index aed6cc19789..00000000000 --- a/packages/opencode/src/session/prompt/anthropic_spoof.txt +++ /dev/null @@ -1 +0,0 @@ -You are Claude Code, Anthropic's official CLI for Claude. diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 8d619357a4f..b055bd10e70 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -34,11 +34,6 @@ async function resolveRelativeInstruction(instruction: string): Promise Promise "experimental.chat.system.transform"?: ( - input: { sessionID: string }, + input: { sessionID?: string; model: Model }, output: { system: string[] }, From 045c30acf34b48cfc6f12b77b61995560fbddead Mon Sep 17 00:00:00 2001 From: MartinWie <42982533+MartinWie@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:44:57 +0100 Subject: [PATCH 010/232] docs: fix permission event name (permission.asked not permission.updated) (#10588) --- packages/web/src/content/docs/plugins.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/plugins.mdx b/packages/web/src/content/docs/plugins.mdx index 66a1b3cad95..2033a9d90e3 100644 --- a/packages/web/src/content/docs/plugins.mdx +++ b/packages/web/src/content/docs/plugins.mdx @@ -170,8 +170,8 @@ Plugins can subscribe to events as seen below in the Examples section. Here is a #### Permission Events +- `permission.asked` - `permission.replied` -- `permission.updated` #### Server Events From 57532326f7b3526348b14e82bb61ecd41ac7d480 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 25 Jan 2026 17:46:14 -0500 Subject: [PATCH 011/232] zen: handle subscription payment failure --- .../console/app/src/routes/stripe/webhook.ts | 24 ++++++------------ packages/console/core/src/billing.ts | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts index c987158d32d..828eb4c711c 100644 --- a/packages/console/app/src/routes/stripe/webhook.ts +++ b/packages/console/app/src/routes/stripe/webhook.ts @@ -396,27 +396,17 @@ export async function POST(input: APIEvent) { } */ } - if (body.type === "customer.subscription.deleted") { + if (body.type === "customer.subscription.updated" && body.data.object.status === "incomplete_expired") { const subscriptionID = body.data.object.id if (!subscriptionID) throw new Error("Subscription ID not found") - const workspaceID = await Database.use((tx) => - tx - .select({ workspaceID: BillingTable.workspaceID }) - .from(BillingTable) - .where(eq(BillingTable.subscriptionID, subscriptionID)) - .then((rows) => rows[0]?.workspaceID), - ) - if (!workspaceID) throw new Error("Workspace ID not found for subscription") - - await Database.transaction(async (tx) => { - await tx - .update(BillingTable) - .set({ subscriptionID: null, subscription: null }) - .where(eq(BillingTable.workspaceID, workspaceID)) + await Billing.unsubscribe({ subscriptionID }) + } + if (body.type === "customer.subscription.deleted") { + const subscriptionID = body.data.object.id + if (!subscriptionID) throw new Error("Subscription ID not found") - await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID)) - }) + await Billing.unsubscribe({ subscriptionID }) } if (body.type === "invoice.payment_succeeded") { if ( diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index 44f12db9e3d..2c1cdb0687b 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -335,4 +335,29 @@ export namespace Billing { return subscription.id }, ) + + export const unsubscribe = fn( + z.object({ + subscriptionID: z.string(), + }), + async ({ subscriptionID }) => { + const workspaceID = await Database.use((tx) => + tx + .select({ workspaceID: BillingTable.workspaceID }) + .from(BillingTable) + .where(eq(BillingTable.subscriptionID, subscriptionID)) + .then((rows) => rows[0]?.workspaceID), + ) + if (!workspaceID) throw new Error("Workspace ID not found for subscription") + + await Database.transaction(async (tx) => { + await tx + .update(BillingTable) + .set({ subscriptionID: null, subscription: null }) + .where(eq(BillingTable.workspaceID, workspaceID)) + + await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID)) + }) + }, + ) } From f0830a74bb36d5836c4ca4e20e406ad541c262ff Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 25 Jan 2026 17:54:17 -0500 Subject: [PATCH 012/232] ignore: update AGENTS.md --- AGENTS.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6619861c2aa..c157fa32d3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ - Prefer single word variable names where possible - Use Bun APIs when possible, like `Bun.file()` -# Avoid let statements +### Avoid let statements We don't like `let` statements, especially combined with if/else statements. Prefer `const`. @@ -31,7 +31,7 @@ if (condition) foo = 1 else foo = 2 ``` -# Avoid else statements +### Avoid else statements Prefer early returns or using an `iife` to avoid else statements. @@ -53,7 +53,7 @@ function foo() { } ``` -# Prefer single word naming +### Prefer single word naming Try your best to find a single word name for your variables, functions, etc. Only use multiple words if you cannot. @@ -73,3 +73,8 @@ const fooBar = 1 const barBaz = 2 const bazFoo = 3 ``` + +## Testing + +You MUST avoid using `mocks` as much as possible. +Tests MUST test actual implementation, do not duplicate logic into a test. From 3071720ce745ce788ac2238bf498625deb9a50c4 Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:57:47 -0500 Subject: [PATCH 013/232] fix(tui): Move animations toggle to global System category (resolves #10495) (#10497) --- packages/opencode/src/cli/cmd/tui/app.tsx | 9 +++++++++ .../opencode/src/cli/cmd/tui/routes/session/index.tsx | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4b177e292cf..1bbd0cdaf3c 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -570,6 +570,15 @@ function App() { dialog.clear() }, }, + { + title: kv.get("animations_enabled", true) ? "Disable animations" : "Enable animations", + value: "app.toggle.animations", + category: "System", + onSelect: (dialog) => { + kv.set("animations_enabled", !kv.get("animations_enabled", true)) + dialog.clear() + }, + }, ]) createEffect(() => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 1294ab849e9..2a5a901a21e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -570,15 +570,6 @@ export function Session() { dialog.clear() }, }, - { - title: animationsEnabled() ? "Disable animations" : "Enable animations", - value: "session.toggle.animations", - category: "Session", - onSelect: (dialog) => { - setAnimationsEnabled((prev) => !prev) - dialog.clear() - }, - }, { title: "Page up", value: "session.page.up", From fbcf13852658750bc3e5ba4d7114f7411f61a772 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:00:01 -0600 Subject: [PATCH 014/232] chore: better i18n links --- README.ar.md | 26 +++++++++++++------------- README.br.md | 26 +++++++++++++------------- README.da.md | 24 ++++++++++++------------ README.de.md | 26 +++++++++++++------------- README.es.md | 26 +++++++++++++------------- README.fr.md | 26 +++++++++++++------------- README.ja.md | 26 +++++++++++++------------- README.ko.md | 26 +++++++++++++------------- README.md | 26 +++++++++++++------------- README.no.md | 24 ++++++++++++------------ README.pl.md | 26 +++++++++++++------------- README.ru.md | 26 +++++++++++++------------- README.zh.md | 26 +++++++++++++------------- README.zht.md | 26 +++++++++++++------------- packages/app/src/i18n/ar.ts | 26 +++++++++++++------------- packages/app/src/i18n/br.ts | 26 +++++++++++++------------- packages/app/src/i18n/da.ts | 24 ++++++++++++------------ packages/app/src/i18n/de.ts | 26 +++++++++++++------------- packages/app/src/i18n/en.ts | 26 +++++++++++++------------- packages/app/src/i18n/es.ts | 26 +++++++++++++------------- packages/app/src/i18n/fr.ts | 26 +++++++++++++------------- packages/app/src/i18n/ja.ts | 26 +++++++++++++------------- packages/app/src/i18n/ko.ts | 26 +++++++++++++------------- packages/app/src/i18n/no.ts | 24 ++++++++++++------------ packages/app/src/i18n/pl.ts | 25 +++++++++++++------------ packages/app/src/i18n/ru.ts | 24 +++++++++++++----------- packages/app/src/i18n/zh.ts | 26 +++++++++++++------------- packages/app/src/i18n/zht.ts | 20 +++++++++++++------- 28 files changed, 360 insertions(+), 351 deletions(-) diff --git a/README.ar.md b/README.ar.md index 41ea7efbd37..9ce00c67aa8 100644 --- a/README.ar.md +++ b/README.ar.md @@ -15,20 +15,20 @@

- الإنجليزية | - الصينية (المبسطة) | - الصينية (التقليدية) | - الكورية | - الألمانية | - الإسبانية | - الفرنسية | - الدنماركية | - اليابانية | - البولندية | - الروسية | + English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Dansk | + 日本語 | + Polski | + Русский | العربية | - النرويجية | - البرتغالية (البرازيل) + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.br.md b/README.br.md index 3618ae55ede..efa7a9ea743 100644 --- a/README.br.md +++ b/README.br.md @@ -15,19 +15,19 @@

- Inglês | - Chinês (Simplificado) | - Chinês (Tradicional) | - Coreano | - Alemão | - Espanhol | - Francês | - Dinamarquês | - Japonês | - Polonês | - Russo | - Árabe | - Norueguês | + English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | Português (Brasil)

diff --git a/README.da.md b/README.da.md index d4eaeb19751..a3cc7c4146d 100644 --- a/README.da.md +++ b/README.da.md @@ -15,20 +15,20 @@

- Engelsk | - Kinesisk (forenklet) | - Kinesisk (traditionelt) | - Koreansk | - Tysk | - Spansk | - Fransk | + English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | Dansk | - Japansk | - Polsk | - Russisk | - Arabisk | + 日本語 | + Polski | + Русский | + العربية | Norsk | - Portugisisk (Brasilien) + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.de.md b/README.de.md index dcea2c7ed24..189171ea3c4 100644 --- a/README.de.md +++ b/README.de.md @@ -15,20 +15,20 @@

- Englisch | - Chinesisch (vereinfacht) | - Chinesisch (traditionell) | - Koreanisch | + English | + 简体中文 | + 繁體中文 | + 한국어 | Deutsch | - Spanisch | - Französisch | - Dänisch | - Japanisch | - Polnisch | - Russisch | - Arabisch | - Norwegisch | - Portugiesisch (Brasilien) + Español | + Français | + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.es.md b/README.es.md index 2ea9be28b17..d6530b1dd08 100644 --- a/README.es.md +++ b/README.es.md @@ -15,20 +15,20 @@

- Inglés | - Chino (simplificado) | - Chino (tradicional) | - Coreano | - Alemán | + English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | Español | - Francés | - Danés | - Japonés | - Polaco | - Ruso | - Árabe | - Noruego | - Portugués (Brasil) + Français | + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.fr.md b/README.fr.md index 1adf073bf3c..520ed3a061c 100644 --- a/README.fr.md +++ b/README.fr.md @@ -15,20 +15,20 @@

- Anglais | - Chinois (simplifié) | - Chinois (traditionnel) | - Coréen | - Allemand | - Espagnol | + English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | Français | - Danois | - Japonais | - Polonais | - Russe | - Arabe | - Norvégien | - Portugais (Brésil) + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ja.md b/README.ja.md index 37d2d179c87..271bcc4e1f2 100644 --- a/README.ja.md +++ b/README.ja.md @@ -15,20 +15,20 @@

- 英語 | - 中国語(簡体字) | - 中国語(繁体字) | - 韓国語 | - ドイツ語 | - スペイン語 | - フランス語 | - デンマーク語 | + English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Dansk | 日本語 | - ポーランド語 | - ロシア語 | - アラビア語 | - ノルウェー語 | - ポルトガル語(ブラジル) + Polski | + Русский | + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ko.md b/README.ko.md index b533f7efe58..a5e299400f4 100644 --- a/README.ko.md +++ b/README.ko.md @@ -15,20 +15,20 @@

- 영어 | - 중국어(간체) | - 중국어(번체) | + English | + 简体中文 | + 繁體中文 | 한국어 | - 독일어 | - 스페인어 | - 프랑스어 | - 덴마크어 | - 일본어 | - 폴란드어 | - 러시아어 | - 아랍어 | - 노르웨이어 | - 포르투갈어(브라질) + Deutsch | + Español | + Français | + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.md b/README.md index a7350912a7d..1ee5f26975e 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,19 @@

English | - Chinese (Simplified) | - Chinese (Traditional) | - Korean | - German | - Spanish | - French | - Danish | - Japanese | - Polish | - Russian | - Arabic | - Norwegian | - Portuguese (Brazil) + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.no.md b/README.no.md index 95a7a317711..43f23b88947 100644 --- a/README.no.md +++ b/README.no.md @@ -15,20 +15,20 @@

- Engelsk | - Kinesisk (forenklet) | - Kinesisk (tradisjonell) | - Koreansk | - Tysk | - Spansk | - Fransk | + English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | Dansk | - Japansk | - Polsk | - Russisk | - Arabisk | + 日本語 | + Polski | + Русский | + العربية | Norsk | - Portugisisk (Brasil) + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.pl.md b/README.pl.md index df650cac3d4..6465879b15d 100644 --- a/README.pl.md +++ b/README.pl.md @@ -15,20 +15,20 @@

- Angielski | - Chiński (uproszczony) | - Chiński (tradycyjny) | - Koreański | - Niemiecki | - Hiszpański | - Francuski | - Duński | - Japoński | + English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Dansk | + 日本語 | Polski | - Rosyjski | - Arabski | - Norweski | - Portugalski (Brazylia) + Русский | + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ru.md b/README.ru.md index 3fabcc9d7c4..5ace29b849e 100644 --- a/README.ru.md +++ b/README.ru.md @@ -15,20 +15,20 @@

- Английский | - Китайский (упрощенный) | - Китайский (традиционный) | - Корейский | - Немецкий | - Испанский | - Французский | - Датский | - Японский | - Польский | + English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Dansk | + 日本語 | + Polski | Русский | - Арабский | - Норвежский | - Португальский (Бразилия) + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.zh.md b/README.zh.md index 2ce826b76f5..9908e8ffb18 100644 --- a/README.zh.md +++ b/README.zh.md @@ -15,20 +15,20 @@

- 英语 | + English | 简体中文 | - 繁体中文 | - 韩语 | - 德语 | - 西班牙语 | - 法语 | - 丹麦语 | - 日语 | - 波兰语 | - 俄语 | - 阿拉伯语 | - 挪威语 | - 葡萄牙语(巴西) + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.zht.md b/README.zht.md index 2fb443502c9..e06d681aa80 100644 --- a/README.zht.md +++ b/README.zht.md @@ -15,20 +15,20 @@

- 英語 | - 簡體中文 | + English | + 简体中文 | 繁體中文 | - 韓語 | - 德語 | - 西班牙語 | - 法語 | - 丹麥語 | - 日語 | - 波蘭語 | - 俄語 | - 阿拉伯語 | - 挪威語 | - 葡萄牙語(巴西) + 한국어 | + Deutsch | + Español | + Français | + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | + Português (Brasil)

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 3a1912345d2..1dfb8d4424b 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -304,20 +304,20 @@ export const dict = { "context.usage.clickToView": "انقر لعرض السياق", "context.usage.view": "عرض استخدام السياق", - "language.en": "الإنجليزية", - "language.zh": "الصينية (المبسطة)", - "language.zht": "الصينية (التقليدية)", - "language.ko": "الكورية", - "language.de": "الألمانية", - "language.es": "الإسبانية", - "language.fr": "الفرنسية", - "language.ja": "اليابانية", - "language.da": "الدانماركية", - "language.ru": "الروسية", - "language.pl": "البولندية", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", "language.ar": "العربية", - "language.no": "النرويجية", - "language.br": "البرتغالية (البرازيل)", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "لغة", "toast.language.description": "تم التبديل إلى {{language}}", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 7e1262a932c..52d4bd270db 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -298,19 +298,19 @@ export const dict = { "context.usage.clickToView": "Clique para ver o contexto", "context.usage.view": "Ver uso do contexto", - "language.en": "Inglês", - "language.zh": "Chinês (Simplificado)", - "language.zht": "Chinês (Tradicional)", - "language.ko": "Coreano", - "language.de": "Alemão", - "language.es": "Espanhol", - "language.fr": "Francês", - "language.ja": "Japonês", - "language.da": "Dinamarquês", - "language.ru": "Russo", - "language.pl": "Polonês", - "language.ar": "Árabe", - "language.no": "Norueguês", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", "language.br": "Português (Brasil)", "toast.language.title": "Idioma", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 863e7905eec..eb711dcea83 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -287,20 +287,20 @@ export const dict = { "context.usage.clickToView": "Klik for at se kontekst", "context.usage.view": "Se kontekstforbrug", - "language.en": "Engelsk", - "language.zh": "Kinesisk (forenklet)", - "language.zht": "Kinesisk (traditionelt)", - "language.ko": "Koreansk", - "language.de": "Tysk", - "language.es": "Spansk", - "language.fr": "Fransk", - "language.ja": "Japansk", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", "language.da": "Dansk", - "language.ru": "Russisk", - "language.pl": "Polsk", - "language.ar": "Arabisk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", "language.no": "Norsk", - "language.br": "Portugisisk (Brasilien)", + "language.br": "Português (Brasil)", "toast.language.title": "Sprog", "toast.language.description": "Skiftede til {{language}}", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index ca926703f9e..39ef515d195 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -292,20 +292,20 @@ export const dict = { "context.usage.clickToView": "Klicken, um Kontext anzuzeigen", "context.usage.view": "Kontextnutzung anzeigen", - "language.en": "Englisch", - "language.zh": "Chinesisch (Vereinfacht)", - "language.zht": "Chinesisch (Traditionell)", - "language.ko": "Koreanisch", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", "language.de": "Deutsch", - "language.es": "Spanisch", - "language.fr": "Französisch", - "language.ja": "Japanisch", - "language.da": "Dänisch", - "language.ru": "Russisch", - "language.pl": "Polnisch", - "language.ar": "Arabisch", - "language.no": "Norwegisch", - "language.br": "Portugiesisch (Brasilien)", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "Sprache", "toast.language.description": "Zu {{language}} gewechselt", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 5dd1ac5f342..d3e43d0896c 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -308,19 +308,19 @@ export const dict = { "context.usage.view": "View context usage", "language.en": "English", - "language.zh": "Chinese (Simplified)", - "language.zht": "Chinese (Traditional)", - "language.ko": "Korean", - "language.de": "German", - "language.es": "Spanish", - "language.fr": "French", - "language.ja": "Japanese", - "language.da": "Danish", - "language.ru": "Russian", - "language.pl": "Polish", - "language.ar": "Arabic", - "language.no": "Norwegian", - "language.br": "Portuguese (Brazil)", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "Language", "toast.language.description": "Switched to {{language}}", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 8eaa30daf0a..725213cfb78 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -287,20 +287,20 @@ export const dict = { "context.usage.clickToView": "Haz clic para ver contexto", "context.usage.view": "Ver uso del contexto", - "language.en": "Inglés", - "language.zh": "Chino (simplificado)", - "language.zht": "Chino (tradicional)", - "language.ko": "Coreano", - "language.de": "Alemán", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", "language.es": "Español", - "language.fr": "Francés", - "language.ja": "Japonés", - "language.da": "Danés", - "language.ru": "Ruso", - "language.pl": "Polaco", - "language.ar": "Árabe", - "language.no": "Noruego", - "language.br": "Portugués (Brasil)", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "Idioma", "toast.language.description": "Cambiado a {{language}}", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 16aba386b96..348aef1205e 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -287,20 +287,20 @@ export const dict = { "context.usage.clickToView": "Cliquez pour voir le contexte", "context.usage.view": "Voir l'utilisation du contexte", - "language.en": "Anglais", - "language.zh": "Chinois (simplifié)", - "language.zht": "Chinois (traditionnel)", - "language.ko": "Coréen", - "language.de": "Allemand", - "language.es": "Espagnol", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", "language.fr": "Français", - "language.ja": "Japonais", - "language.da": "Danois", - "language.ru": "Russe", - "language.pl": "Polonais", - "language.ar": "Arabe", - "language.no": "Norvégien", - "language.br": "Portugais (Brésil)", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "Langue", "toast.language.description": "Passé à {{language}}", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index d33d5c7a757..62f01c8d723 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -285,20 +285,20 @@ export const dict = { "context.usage.clickToView": "クリックしてコンテキストを表示", "context.usage.view": "コンテキスト使用量を表示", - "language.en": "英語", - "language.zh": "中国語(簡体字)", - "language.zht": "中国語(繁体字)", - "language.ko": "韓国語", - "language.de": "ドイツ語", - "language.es": "スペイン語", - "language.fr": "フランス語", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", "language.ja": "日本語", - "language.da": "デンマーク語", - "language.ru": "ロシア語", - "language.pl": "ポーランド語", - "language.ar": "アラビア語", - "language.no": "ノルウェー語", - "language.br": "ポルトガル語(ブラジル)", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "言語", "toast.language.description": "{{language}}に切り替えました", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 73d0f96873f..71ac64ae802 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -289,20 +289,20 @@ export const dict = { "context.usage.clickToView": "컨텍스트를 보려면 클릭", "context.usage.view": "컨텍스트 사용량 보기", - "language.en": "영어", - "language.zh": "중국어 (간체)", - "language.zht": "중국어 (번체)", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", "language.ko": "한국어", - "language.de": "독일어", - "language.es": "스페인어", - "language.fr": "프랑스어", - "language.ja": "일본어", - "language.da": "덴마크어", - "language.ru": "러시아어", - "language.pl": "폴란드어", - "language.ar": "아랍어", - "language.no": "노르웨이어", - "language.br": "포르투갈어 (브라질)", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "언어", "toast.language.description": "{{language}}(으)로 전환됨", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 133f60aed90..1572d391bbc 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -307,20 +307,20 @@ export const dict = { "context.usage.clickToView": "Klikk for å se kontekst", "context.usage.view": "Se kontekstforbruk", - "language.en": "Engelsk", - "language.zh": "Kinesisk (forenklet)", - "language.zht": "Kinesisk (tradisjonell)", - "language.ko": "Koreansk", - "language.de": "Tysk", - "language.es": "Spansk", - "language.fr": "Fransk", - "language.ja": "Japansk", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", "language.da": "Dansk", - "language.ru": "Russisk", - "language.pl": "Polsk", - "language.ar": "Arabisk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", "language.no": "Norsk", - "language.br": "Portugisisk (Brasil)", + "language.br": "Português (Brasil)", "toast.language.title": "Språk", "toast.language.description": "Byttet til {{language}}", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 46b783082d3..4b2a7ccb2b8 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -304,19 +304,20 @@ export const dict = { "context.usage.clickToView": "Kliknij, aby zobaczyć kontekst", "context.usage.view": "Pokaż użycie kontekstu", - "language.en": "Angielski", - "language.zh": "Chiński", - "language.ko": "Koreański", - "language.de": "Niemiecki", - "language.es": "Hiszpański", - "language.fr": "Francuski", - "language.ja": "Japoński", - "language.da": "Duński", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", "language.pl": "Polski", - "language.ru": "Rosyjski", - "language.ar": "Arabski", - "language.no": "Norweski", - "language.br": "Portugalski (Brazylia)", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "Język", "toast.language.description": "Przełączono na {{language}}", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 602ff208235..ebe8265c7d8 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -305,18 +305,20 @@ export const dict = { "context.usage.clickToView": "Нажмите для просмотра контекста", "context.usage.view": "Показать использование контекста", - "language.en": "Английский", - "language.zh": "Китайский", - "language.ko": "Корейский", - "language.de": "Немецкий", - "language.es": "Испанский", - "language.fr": "Французский", - "language.ja": "Японский", - "language.da": "Датский", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", "language.ru": "Русский", - "language.ar": "Арабский", - "language.no": "Норвежский", - "language.br": "Португальский (Бразилия)", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "Язык", "toast.language.description": "Переключено на {{language}}", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 4777ca665ca..2ab985c6846 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -285,20 +285,20 @@ export const dict = { "context.usage.clickToView": "点击查看上下文", "context.usage.view": "查看上下文用量", - "language.en": "英语", + "language.en": "English", "language.zh": "简体中文", - "language.zht": "繁体中文", - "language.ko": "韩语", - "language.de": "德语", - "language.es": "西班牙语", - "language.fr": "法语", - "language.ja": "日语", - "language.da": "丹麦语", - "language.ru": "俄语", - "language.pl": "波兰语", - "language.ar": "阿拉伯语", - "language.no": "挪威语", - "language.br": "葡萄牙语(巴西)", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "语言", "toast.language.description": "已切换到{{language}}", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 5c1b2d64516..c1d7580262a 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -287,14 +287,20 @@ export const dict = { "context.usage.clickToView": "點擊查看上下文", "context.usage.view": "檢視上下文用量", - "language.en": "英語", - "language.zh": "簡體中文", + "language.en": "English", + "language.zh": "简体中文", "language.zht": "繁體中文", - "language.ko": "韓語", - "language.ru": "俄語", - "language.ar": "阿拉伯語", - "language.no": "挪威語", - "language.br": "葡萄牙語(巴西)", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", "toast.language.title": "語言", "toast.language.description": "已切換到 {{language}}", From 5369e96ab70b88abf1492c50f191dcf9e3bb7108 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:07:19 -0600 Subject: [PATCH 015/232] fix(app): line selection colors --- packages/ui/src/components/code.tsx | 27 +++++++++++++++++++++++++ packages/ui/src/components/diff-ssr.tsx | 20 ++++++++++++++++++ packages/ui/src/components/diff.tsx | 27 +++++++++++++++++++++++++ packages/ui/src/pierre/index.ts | 25 ++++++++++++++--------- 4 files changed, 90 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx index eb0ba78262c..a7687444f66 100644 --- a/packages/ui/src/components/code.tsx +++ b/packages/ui/src/components/code.tsx @@ -91,6 +91,19 @@ export function Code(props: CodeProps) { return root } + const applyScheme = () => { + const host = container.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return + + const scheme = document.documentElement.dataset.colorScheme + if (scheme === "dark" || scheme === "light") { + host.dataset.colorScheme = scheme + return + } + + host.removeAttribute("data-color-scheme") + } + const applyCommentedLines = (ranges: SelectedLineRange[]) => { const root = getRoot() if (!root) return @@ -369,10 +382,24 @@ export function Code(props: CodeProps) { containerWrapper: container, }) + applyScheme() + setRendered((value) => value + 1) notifyRendered() }) + createEffect(() => { + if (typeof document === "undefined") return + if (typeof MutationObserver === "undefined") return + + const root = document.documentElement + const monitor = new MutationObserver(() => applyScheme()) + monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] }) + applyScheme() + + onCleanup(() => monitor.disconnect()) + }) + createEffect(() => { rendered() const ranges = local.commentedLines ?? [] diff --git a/packages/ui/src/components/diff-ssr.tsx b/packages/ui/src/components/diff-ssr.tsx index 9456e4a3c32..4ab63200821 100644 --- a/packages/ui/src/components/diff-ssr.tsx +++ b/packages/ui/src/components/diff-ssr.tsx @@ -28,6 +28,16 @@ export function Diff(props: SSRDiffProps) { const getRoot = () => fileDiffRef?.shadowRoot ?? undefined + const applyScheme = () => { + const scheme = document.documentElement.dataset.colorScheme + if (scheme === "dark" || scheme === "light") { + fileDiffRef.dataset.colorScheme = scheme + return + } + + fileDiffRef.removeAttribute("data-color-scheme") + } + const findSide = (element: HTMLElement): "additions" | "deletions" => { const line = element.closest("[data-line], [data-alt-line]") if (line instanceof HTMLElement) { @@ -121,6 +131,16 @@ export function Diff(props: SSRDiffProps) { onMount(() => { if (isServer || !props.preloadedDiff) return + + applyScheme() + + if (typeof MutationObserver !== "undefined") { + const root = document.documentElement + const monitor = new MutationObserver(() => applyScheme()) + monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] }) + onCleanup(() => monitor.disconnect()) + } + fileDiffInstance = new FileDiff( { ...createDefaultOptions(props.diffStyle), diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx index 2fb2ea3bc61..c97744a9f51 100644 --- a/packages/ui/src/components/diff.tsx +++ b/packages/ui/src/components/diff.tsx @@ -102,6 +102,19 @@ export function Diff(props: DiffProps) { return root } + const applyScheme = () => { + const host = container.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return + + const scheme = document.documentElement.dataset.colorScheme + if (scheme === "dark" || scheme === "light") { + host.dataset.colorScheme = scheme + return + } + + host.removeAttribute("data-color-scheme") + } + const notifyRendered = () => { if (!local.onRendered) return @@ -488,10 +501,24 @@ export function Diff(props: DiffProps) { containerWrapper: container, }) + applyScheme() + setRendered((value) => value + 1) notifyRendered() }) + createEffect(() => { + if (typeof document === "undefined") return + if (typeof MutationObserver === "undefined") return + + const root = document.documentElement + const monitor = new MutationObserver(() => applyScheme()) + monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] }) + applyScheme() + + onCleanup(() => monitor.disconnect()) + }) + createEffect(() => { rendered() const ranges = local.commentedLines ?? [] diff --git a/packages/ui/src/pierre/index.ts b/packages/ui/src/pierre/index.ts index a83af98906f..f0da5197938 100644 --- a/packages/ui/src/pierre/index.ts +++ b/packages/ui/src/pierre/index.ts @@ -35,9 +35,22 @@ const unsafeCSS = ` --diffs-selection-base: var(--surface-warning-strong); --diffs-selection-border: var(--border-warning-base); --diffs-selection-number-fg: #1c1917; - --diffs-bg-selection: var(--diffs-bg-selection-override, color-mix(in oklch, var(--surface-warning-base) 65%, transparent)); - --diffs-bg-selection-number: var(--diffs-bg-selection-number-override, color-mix(in oklch, var(--surface-warning-base) 85%, transparent)); - --diffs-bg-selection-text: color-mix(in oklch, var(--surface-warning-strong) 20%, transparent); + /* Use explicit alpha instead of color-mix(..., transparent) to avoid Safari's non-premultiplied interpolation bugs. */ + --diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--surface-warning-base) r g b / 0.65)); + --diffs-bg-selection-number: var( + --diffs-bg-selection-number-override, + rgb(from var(--surface-warning-base) r g b / 0.85) + ); + --diffs-bg-selection-text: rgb(from var(--surface-warning-strong) r g b / 0.2); +} + +:host([data-color-scheme='dark']) [data-diffs] { + --diffs-selection-number-fg: #fdfbfb; + --diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--solaris-dark-6) r g b / 0.65)); + --diffs-bg-selection-number: var( + --diffs-bg-selection-number-override, + rgb(from var(--solaris-dark-6) r g b / 0.85) + ); } [data-diffs] ::selection { @@ -78,12 +91,6 @@ const unsafeCSS = ` ); } -:host-context([data-color-scheme='dark']) [data-diffs] { - --diffs-selection-number-fg: #fdfbfb; - --diffs-bg-selection: var(--diffs-bg-selection-override, color-mix(in oklch, var(--solaris-dark-6) 65%, transparent)); - --diffs-bg-selection-number: var(--diffs-bg-selection-number-override, color-mix(in oklch, var(--solaris-dark-6) 85%, transparent)); -} - [data-diffs-header], [data-diffs] { [data-separator-wrapper] { From 578361de642c9787bdc0654bc6e82e3b4ccf2527 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 25 Jan 2026 20:27:24 -0500 Subject: [PATCH 016/232] fix: remove broken app.tsx command option --- packages/opencode/src/cli/cmd/tui/app.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 1bbd0cdaf3c..340b972ac6c 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -488,15 +488,6 @@ function App() { }, category: "System", }, - { - title: "Open WebUI", - value: "webui.open", - onSelect: () => { - open(sdk.url).catch(() => {}) - dialog.clear() - }, - category: "System", - }, { title: "Exit the app", value: "app.exit", From 3d23d2df716bf44c8a26b445ee188cbee94c023b Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:39:05 -0600 Subject: [PATCH 017/232] fix(app): missing translations for status --- .../app/src/components/status-popover.tsx | 41 +++++++++++++------ packages/app/src/i18n/ar.ts | 8 ++++ packages/app/src/i18n/br.ts | 8 ++++ packages/app/src/i18n/da.ts | 8 ++++ packages/app/src/i18n/de.ts | 8 ++++ packages/app/src/i18n/en.ts | 8 ++++ packages/app/src/i18n/es.ts | 8 ++++ packages/app/src/i18n/fr.ts | 8 ++++ packages/app/src/i18n/ja.ts | 8 ++++ packages/app/src/i18n/ko.ts | 8 ++++ packages/app/src/i18n/no.ts | 8 ++++ packages/app/src/i18n/pl.ts | 8 ++++ packages/app/src/i18n/ru.ts | 8 ++++ packages/app/src/i18n/zh.ts | 8 ++++ packages/app/src/i18n/zht.ts | 8 ++++ 15 files changed, 141 insertions(+), 12 deletions(-) diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index a3e76517f5a..3963a54f3cf 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -153,7 +153,7 @@ export function StatusPopover() { "bg-border-weak-base": server.healthy() === undefined, }} /> - Status + {language.t("status.popover.trigger")}
} class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl" @@ -166,7 +166,7 @@ export function StatusPopover() { style={{ "box-shadow": "var(--shadow-lg-border-base)" }} > - {serverCount() > 0 ? `${serverCount()} ` : ""}Servers + {serverCount() > 0 ? `${serverCount()} ` : ""} + {language.t("status.popover.tab.servers")} - {mcpConnected() > 0 ? `${mcpConnected()} ` : ""}MCP + {mcpConnected() > 0 ? `${mcpConnected()} ` : ""} + {language.t("status.popover.tab.mcp")} - {lspCount() > 0 ? `${lspCount()} ` : ""}LSP + {lspCount() > 0 ? `${lspCount()} ` : ""} + {language.t("status.popover.tab.lsp")} - {pluginCount() > 0 ? `${pluginCount()} ` : ""}Plugins + {pluginCount() > 0 ? `${pluginCount()} ` : ""} + {language.t("status.popover.tab.plugins")} @@ -274,7 +278,7 @@ export function StatusPopover() {
- Default + {language.t("common.default")}
@@ -292,7 +296,7 @@ export function StatusPopover() { class="mt-3 self-start h-8 px-3 py-1.5" onClick={() => dialog.show(() => )} > - Manage servers + {language.t("status.popover.action.manageServers")}
@@ -304,7 +308,9 @@ export function StatusPopover() { 0} fallback={ -
No MCP servers configured
+
+ {language.t("dialog.mcp.empty")} +
} > @@ -351,7 +357,7 @@ export function StatusPopover() { when={lspItems().length > 0} fallback={
- LSPs auto-detected from file types + {language.t("dialog.lsp.empty")}
} > @@ -381,8 +387,19 @@ export function StatusPopover() { when={plugins().length > 0} fallback={
- Plugins configured in{" "} - opencode.json + {(() => { + const value = language.t("dialog.plugins.empty") + const file = "opencode.json" + const parts = value.split(file) + if (parts.length === 1) return value + return ( + <> + {parts[0]} + {file} + {parts.slice(1).join(file)} + + ) + })()}
} > diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 1dfb8d4424b..99e516a0a49 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -426,6 +426,14 @@ export const dict = { "session.header.search.placeholder": "بحث {{project}}", "session.header.searchFiles": "بحث عن الملفات", + "status.popover.trigger": "الحالة", + "status.popover.ariaLabel": "إعدادات الخوادم", + "status.popover.tab.servers": "الخوادم", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "الإضافات", + "status.popover.action.manageServers": "إدارة الخوادم", + "session.share.popover.title": "نشر على الويب", "session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.", "session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 52d4bd270db..93dc2f1febe 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -422,6 +422,14 @@ export const dict = { "session.header.search.placeholder": "Buscar {{project}}", "session.header.searchFiles": "Buscar arquivos", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Configurações de servidores", + "status.popover.tab.servers": "Servidores", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugins", + "status.popover.action.manageServers": "Gerenciar servidores", + "session.share.popover.title": "Publicar na web", "session.share.popover.description.shared": "Esta sessão é pública na web. Está acessível para qualquer pessoa com o link.", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index eb711dcea83..b2f0a9afe74 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -409,6 +409,14 @@ export const dict = { "session.header.search.placeholder": "Søg {{project}}", "session.header.searchFiles": "Søg efter filer", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Serverkonfigurationer", + "status.popover.tab.servers": "Servere", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugins", + "status.popover.action.manageServers": "Administrer servere", + "session.share.popover.title": "Udgiv på nettet", "session.share.popover.description.shared": "Denne session er offentlig på nettet. Den er tilgængelig for alle med linket.", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 39ef515d195..42f628d5ed6 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -416,6 +416,14 @@ export const dict = { "session.header.search.placeholder": "{{project}} durchsuchen", "session.header.searchFiles": "Dateien suchen", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Serverkonfigurationen", + "status.popover.tab.servers": "Server", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugins", + "status.popover.action.manageServers": "Server verwalten", + "session.share.popover.title": "Im Web veröffentlichen", "session.share.popover.description.shared": "Diese Sitzung ist öffentlich im Web. Sie ist für jeden mit dem Link zugänglich.", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index d3e43d0896c..b32f0348551 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -430,6 +430,14 @@ export const dict = { "session.header.search.placeholder": "Search {{project}}", "session.header.searchFiles": "Search files", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Server configurations", + "status.popover.tab.servers": "Servers", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugins", + "status.popover.action.manageServers": "Manage servers", + "session.share.popover.title": "Publish on web", "session.share.popover.description.shared": "This session is public on the web. It is accessible to anyone with the link.", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 725213cfb78..1039a2d3a4c 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -410,6 +410,14 @@ export const dict = { "session.header.search.placeholder": "Buscar {{project}}", "session.header.searchFiles": "Buscar archivos", + "status.popover.trigger": "Estado", + "status.popover.ariaLabel": "Configuraciones del servidor", + "status.popover.tab.servers": "Servidores", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugins", + "status.popover.action.manageServers": "Administrar servidores", + "session.share.popover.title": "Publicar en web", "session.share.popover.description.shared": "Esta sesión es pública en la web. Es accesible para cualquiera con el enlace.", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 348aef1205e..09eeea44c46 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -415,6 +415,14 @@ export const dict = { "session.header.search.placeholder": "Rechercher {{project}}", "session.header.searchFiles": "Rechercher des fichiers", + "status.popover.trigger": "Statut", + "status.popover.ariaLabel": "Configurations des serveurs", + "status.popover.tab.servers": "Serveurs", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugins", + "status.popover.action.manageServers": "Gérer les serveurs", + "session.share.popover.title": "Publier sur le web", "session.share.popover.description.shared": "Cette session est publique sur le web. Elle est accessible à toute personne disposant du lien.", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 62f01c8d723..821c6ccdb12 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -407,6 +407,14 @@ export const dict = { "session.header.search.placeholder": "{{project}}を検索", "session.header.searchFiles": "ファイルを検索", + "status.popover.trigger": "ステータス", + "status.popover.ariaLabel": "サーバー設定", + "status.popover.tab.servers": "サーバー", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "プラグイン", + "status.popover.action.manageServers": "サーバーを管理", + "session.share.popover.title": "ウェブで公開", "session.share.popover.description.shared": "このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 71ac64ae802..ddd00e763d8 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -410,6 +410,14 @@ export const dict = { "session.header.search.placeholder": "{{project}} 검색", "session.header.searchFiles": "파일 검색", + "status.popover.trigger": "상태", + "status.popover.ariaLabel": "서버 구성", + "status.popover.tab.servers": "서버", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "플러그인", + "status.popover.action.manageServers": "서버 관리", + "session.share.popover.title": "웹에 게시", "session.share.popover.description.shared": "이 세션은 웹에 공개되었습니다. 링크가 있는 누구나 액세스할 수 있습니다.", "session.share.popover.description.unshared": diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 1572d391bbc..3262d3e04c4 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -430,6 +430,14 @@ export const dict = { "session.header.search.placeholder": "Søk i {{project}}", "session.header.searchFiles": "Søk etter filer", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Serverkonfigurasjoner", + "status.popover.tab.servers": "Servere", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugins", + "status.popover.action.manageServers": "Administrer servere", + "session.share.popover.title": "Publiser på nett", "session.share.popover.description.shared": "Denne sesjonen er offentlig på nettet. Den er tilgjengelig for alle med lenken.", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 4b2a7ccb2b8..7af9d217985 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -428,6 +428,14 @@ export const dict = { "session.header.search.placeholder": "Szukaj {{project}}", "session.header.searchFiles": "Szukaj plików", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Konfiguracje serwerów", + "status.popover.tab.servers": "Serwery", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Wtyczki", + "status.popover.action.manageServers": "Zarządzaj serwerami", + "session.share.popover.title": "Opublikuj w sieci", "session.share.popover.description.shared": "Ta sesja jest publiczna w sieci. Jest dostępna dla każdego, kto posiada link.", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index ebe8265c7d8..d7fa135fa0f 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -430,6 +430,14 @@ export const dict = { "session.header.search.placeholder": "Поиск {{project}}", "session.header.searchFiles": "Поиск файлов", + "status.popover.trigger": "Статус", + "status.popover.ariaLabel": "Настройки серверов", + "status.popover.tab.servers": "Серверы", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Плагины", + "status.popover.action.manageServers": "Управлять серверами", + "session.share.popover.title": "Опубликовать в интернете", "session.share.popover.description.shared": "Эта сессия общедоступна. Доступ к ней может получить любой, у кого есть ссылка.", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 2ab985c6846..e2b7df0d10e 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -405,6 +405,14 @@ export const dict = { "session.header.search.placeholder": "搜索 {{project}}", "session.header.searchFiles": "搜索文件", + "status.popover.trigger": "状态", + "status.popover.ariaLabel": "服务器配置", + "status.popover.tab.servers": "服务器", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "插件", + "status.popover.action.manageServers": "管理服务器", + "session.share.popover.title": "发布到网页", "session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。", "session.share.popover.description.unshared": "在网页上公开分享此会话。任何拥有链接的人都可以访问。", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index c1d7580262a..9973b443b34 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -407,6 +407,14 @@ export const dict = { "session.header.search.placeholder": "搜尋 {{project}}", "session.header.searchFiles": "搜尋檔案", + "status.popover.trigger": "狀態", + "status.popover.ariaLabel": "伺服器設定", + "status.popover.tab.servers": "伺服器", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "外掛程式", + "status.popover.action.manageServers": "管理伺服器", + "session.share.popover.title": "發佈到網頁", "session.share.popover.description.shared": "此工作階段已在網頁上公開。任何擁有連結的人都可以存取。", "session.share.popover.description.unshared": "在網頁上公開分享此工作階段。任何擁有連結的人都可以存取。", From ab3268896d2387a1dba36df2881a10a2ebf8dce6 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Sun, 25 Jan 2026 20:56:37 -0500 Subject: [PATCH 018/232] Add highlight tag parsing for changelog with video support --- .../console/app/src/routes/changelog.json.ts | 110 ++++++++++++++++++ .../app/src/routes/changelog/index.css | 35 ++++++ .../app/src/routes/changelog/index.tsx | 83 ++++++++++++- 3 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 packages/console/app/src/routes/changelog.json.ts diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts new file mode 100644 index 00000000000..b668229f813 --- /dev/null +++ b/packages/console/app/src/routes/changelog.json.ts @@ -0,0 +1,110 @@ +type Release = { + tag_name: string + name: string + body: string + published_at: string + html_url: string +} + +type Highlight = { + source: string + title: string + description: string + shortDescription?: string + image?: { + src: string + width: string + height: string + } + video?: string +} + +function parseHighlights(body: string): Highlight[] { + const highlights: Highlight[] = [] + const regex = /([\s\S]*?)<\/highlight>/g + let match + + while ((match = regex.exec(body)) !== null) { + const source = match[1] + const content = match[2] + + const titleMatch = content.match(/

([^<]+)<\/h2>/) + const pMatch = content.match(/([^<]+)<\/p>/) + const imgMatch = content.match(/ { + const parsed = parseMarkdown(release.body || "") + return { + tag: release.tag_name, + name: release.name, + date: release.published_at, + url: release.html_url, + highlights: parsed.highlights, + sections: parsed.sections, + } + }), + } +} diff --git a/packages/console/app/src/routes/changelog/index.css b/packages/console/app/src/routes/changelog/index.css index a445c74474a..a06fb00554c 100644 --- a/packages/console/app/src/routes/changelog/index.css +++ b/packages/console/app/src/routes/changelog/index.css @@ -465,6 +465,41 @@ } } } + + [data-component="highlights"] { + display: flex; + flex-direction: column; + gap: 2rem; + margin-bottom: 1.5rem; + } + + [data-component="highlight"] { + h4 { + font-size: 14px; + font-weight: 600; + color: var(--color-text-strong); + margin-bottom: 8px; + } + + p[data-slot="title"] { + font-weight: 500; + color: var(--color-text-strong); + margin-bottom: 4px; + } + + p { + color: var(--color-text); + line-height: 1.5; + margin-bottom: 12px; + } + + img, + video { + max-width: 100%; + height: auto; + border-radius: 4px; + } + } } a { diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index c1b931fe3e0..34fd5f83b73 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -40,6 +40,59 @@ function formatDate(dateString: string) { }) } +type Highlight = { + source: string + title: string + description: string + shortDescription?: string + image?: { + src: string + width: string + height: string + } + video?: string +} + +function parseHighlights(body: string): Highlight[] { + const highlights: Highlight[] = [] + const regex = /([\s\S]*?)<\/highlight>/g + let match + + while ((match = regex.exec(body)) !== null) { + const source = match[1] + const content = match[2] + + const titleMatch = content.match(/

([^<]+)<\/h2>/) + const pMatch = content.match(/([^<]+)<\/p>/) + const imgMatch = content.match(/ +

{props.highlight.source}

+

{props.highlight.title}

+

{props.highlight.description}

+ + + + {props.highlight.title} + + + ) +} + export default function Changelog() { const releases = createAsync(() => getReleases()) @@ -120,6 +196,11 @@ export default function Changelog() {
+ 0}> +
+ {(highlight) => } +
+
{(section) => (
From eaad75b1765745865c7d9d0b4826e3b30463882e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 25 Jan 2026 21:05:18 -0500 Subject: [PATCH 019/232] tweak: adjust tui syncing logic to help prevent case where agents would be undefined / missing --- .../opencode/src/cli/cmd/tui/context/sync.tsx | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 392cfb7f121..eb8ed2d9bba 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -333,32 +333,57 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const start = Date.now() - 30 * 24 * 60 * 60 * 1000 const sessionListPromise = sdk.client.session .list({ start: start }) - .then((x) => setStore("session", reconcile((x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))))) + .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) // blocking - include session.list when continuing a session + const providersPromise = sdk.client.config.providers({}, { throwOnError: true }) + const providerListPromise = sdk.client.provider.list({}, { throwOnError: true }) + const agentsPromise = sdk.client.app.agents({}, { throwOnError: true }) + const configPromise = sdk.client.config.get({}, { throwOnError: true }) const blockingRequests: Promise[] = [ - sdk.client.config.providers({}, { throwOnError: true }).then((x) => { - batch(() => { - setStore("provider", reconcile(x.data!.providers)) - setStore("provider_default", reconcile(x.data!.default)) - }) - }), - sdk.client.provider.list({}, { throwOnError: true }).then((x) => { - batch(() => { - setStore("provider_next", reconcile(x.data!)) - }) - }), - sdk.client.app.agents({}, { throwOnError: true }).then((x) => setStore("agent", reconcile(x.data ?? []))), - sdk.client.config.get({}, { throwOnError: true }).then((x) => setStore("config", reconcile(x.data!))), + providersPromise, + providerListPromise, + agentsPromise, + configPromise, ...(args.continue ? [sessionListPromise] : []), ] await Promise.all(blockingRequests) + .then(() => { + const providersResponse = providersPromise.then((x) => x.data!) + const providerListResponse = providerListPromise.then((x) => x.data!) + const agentsResponse = agentsPromise.then((x) => x.data ?? []) + const configResponse = configPromise.then((x) => x.data!) + const sessionListResponse = args.continue ? sessionListPromise : undefined + + return Promise.all([ + providersResponse, + providerListResponse, + agentsResponse, + configResponse, + ...(sessionListResponse ? [sessionListResponse] : []), + ]).then((responses) => { + const providers = responses[0] + const providerList = responses[1] + const agents = responses[2] + const config = responses[3] + const sessions = responses[4] + + batch(() => { + setStore("provider", reconcile(providers.providers)) + setStore("provider_default", reconcile(providers.default)) + setStore("provider_next", reconcile(providerList)) + setStore("agent", reconcile(agents)) + setStore("config", reconcile(config)) + if (sessions !== undefined) setStore("session", reconcile(sessions)) + }) + }) + }) .then(() => { if (store.status !== "complete") setStore("status", "partial") // non-blocking Promise.all([ - ...(args.continue ? [] : [sessionListPromise]), + ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]), sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))), sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))), sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))), From cc0085676b193ea77fd18bff08fcb4987a32af79 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Sun, 25 Jan 2026 21:18:26 -0500 Subject: [PATCH 020/232] Add collapsible sections, sticky version header, and style refinements for changelog highlights --- .../console/app/src/routes/changelog.json.ts | 54 ++++--- .../app/src/routes/changelog/index.css | 114 ++++++++++++-- .../app/src/routes/changelog/index.tsx | 148 +++++++++++------- 3 files changed, 223 insertions(+), 93 deletions(-) diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts index b668229f813..a23f2050324 100644 --- a/packages/console/app/src/routes/changelog.json.ts +++ b/packages/console/app/src/routes/changelog.json.ts @@ -6,21 +6,22 @@ type Release = { html_url: string } -type Highlight = { - source: string +type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } + +type HighlightItem = { title: string description: string shortDescription?: string - image?: { - src: string - width: string - height: string - } - video?: string + media: HighlightMedia +} + +type HighlightGroup = { + source: string + items: HighlightItem[] } -function parseHighlights(body: string): Highlight[] { - const highlights: Highlight[] = [] +function parseHighlights(body: string): HighlightGroup[] { + const groups = new Map() const regex = /([\s\S]*?)<\/highlight>/g let match @@ -30,29 +31,32 @@ function parseHighlights(body: string): Highlight[] { const titleMatch = content.match(/

([^<]+)<\/h2>/) const pMatch = content.match(/([^<]+)<\/p>/) - const imgMatch = content.match(/ ({ source, items })) } function parseMarkdown(body: string) { diff --git a/packages/console/app/src/routes/changelog/index.css b/packages/console/app/src/routes/changelog/index.css index a06fb00554c..233d85cc0e6 100644 --- a/packages/console/app/src/routes/changelog/index.css +++ b/packages/console/app/src/routes/changelog/index.css @@ -367,11 +367,18 @@ display: flex; flex-direction: column; gap: 4px; + position: sticky; + top: 80px; + align-self: start; + background: var(--color-background); + padding: 8px 0; @media (max-width: 50rem) { + position: static; flex-direction: row; align-items: center; gap: 12px; + padding: 0; } [data-slot="version"] { @@ -402,24 +409,26 @@ [data-component="section"] { h3 { - font-size: 14px; + font-size: 13px; font-weight: 600; color: var(--color-text-strong); - margin-bottom: 8px; + margin-bottom: 6px; } ul { list-style: none; padding: 0; margin: 0; + padding-left: 16px; display: flex; flex-direction: column; - gap: 6px; + gap: 4px; li { color: var(--color-text); + font-size: 13px; line-height: 1.5; - padding-left: 16px; + padding-left: 12px; position: relative; &::before { @@ -431,7 +440,7 @@ [data-slot="author"] { color: var(--color-text-weak); - font-size: 13px; + font-size: 12px; margin-left: 4px; text-decoration: none; @@ -473,6 +482,72 @@ margin-bottom: 1.5rem; } + [data-component="collapsible-sections"] { + display: flex; + flex-direction: column; + gap: 0; + } + + [data-component="collapsible-section"] { + [data-slot="toggle"] { + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + padding: 6px 0; + cursor: pointer; + font-family: inherit; + font-size: 13px; + font-weight: 600; + color: var(--color-text-weak); + + &:hover { + color: var(--color-text); + } + + [data-slot="icon"] { + font-size: 10px; + } + } + + ul { + list-style: none; + padding: 0; + margin: 0; + padding-left: 16px; + padding-bottom: 8px; + + li { + color: var(--color-text); + font-size: 13px; + line-height: 1.5; + padding-left: 12px; + position: relative; + + &::before { + content: "-"; + position: absolute; + left: 0; + color: var(--color-text-weak); + } + + [data-slot="author"] { + color: var(--color-text-weak); + font-size: 12px; + margin-left: 4px; + text-decoration: none; + + &:hover { + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + } + } + } + } + } + [data-component="highlight"] { h4 { font-size: 14px; @@ -481,16 +556,29 @@ margin-bottom: 8px; } - p[data-slot="title"] { - font-weight: 500; - color: var(--color-text-strong); - margin-bottom: 4px; + hr { + border: none; + border-top: 1px solid var(--color-border-weak); + margin-bottom: 16px; } - p { - color: var(--color-text); - line-height: 1.5; - margin-bottom: 12px; + [data-slot="highlight-item"] { + margin-bottom: 24px; + + &:last-child { + margin-bottom: 0; + } + + p[data-slot="title"] { + font-weight: 600; + font-size: 16px; + margin-bottom: 4px; + } + + p { + font-size: 14px; + margin-bottom: 12px; + } } img, diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index 34fd5f83b73..87e021ec88c 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -5,7 +5,7 @@ import { Header } from "~/component/header" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" import { config } from "~/config" -import { For, Show } from "solid-js" +import { For, Show, createSignal } from "solid-js" type Release = { tag_name: string @@ -40,21 +40,22 @@ function formatDate(dateString: string) { }) } -type Highlight = { - source: string +type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } + +type HighlightItem = { title: string description: string shortDescription?: string - image?: { - src: string - width: string - height: string - } - video?: string + media: HighlightMedia } -function parseHighlights(body: string): Highlight[] { - const highlights: Highlight[] = [] +type HighlightGroup = { + source: string + items: HighlightItem[] +} + +function parseHighlights(body: string): HighlightGroup[] { + const groups = new Map() const regex = /([\s\S]*?)<\/highlight>/g let match @@ -64,33 +65,32 @@ function parseHighlights(body: string): Highlight[] { const titleMatch = content.match(/

([^<]+)<\/h2>/) const pMatch = content.match(/([^<]+)<\/p>/) - const imgMatch = content.match(/ ({ source, items })) } function parseMarkdown(body: string) { @@ -142,27 +142,60 @@ function ReleaseItem(props: { item: string }) { ) } -function HighlightCard(props: { highlight: Highlight }) { +function HighlightSection(props: { group: HighlightGroup }) { return (
-

{props.highlight.source}

-

{props.highlight.title}

-

{props.highlight.description}

- - - - {props.highlight.title} +

{props.group.source}

+
+ + {(item) => ( +
+

{item.title}

+

{item.description}

+ + + + {item.title} + +
+ )} +
+
+ ) +} + +function CollapsibleSection(props: { section: { title: string; items: string[] } }) { + const [open, setOpen] = createSignal(false) + + return ( +
+ + +
    + {(item) => } +
) } +function CollapsibleSections(props: { sections: { title: string; items: string[] }[] }) { + return ( +
+ {(section) => } +
+ ) +} + export default function Changelog() { const releases = createAsync(() => getReleases()) @@ -198,19 +231,24 @@ export default function Changelog() {
0}>
- {(highlight) => } + {(group) => }
- - {(section) => ( -
-

{section.title}

-
    - {(item) => } -
-
- )} -
+ 0 && parsed().sections.length > 0}> + + + + + {(section) => ( +
+

{section.title}

+
    + {(item) => } +
+
+ )} +
+
) From a5b72a7d994618555467b4269f48b0c000e2db84 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 20:32:56 -0600 Subject: [PATCH 021/232] fix(ui): tab click hit area --- packages/ui/src/components/tabs.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index 3f5e710b6c3..cebbd3b4faa 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -239,6 +239,7 @@ background-color: transparent; [data-slot="tabs-trigger"] { + height: 100%; padding: 0 8px; gap: 8px; justify-content: flex-start; @@ -333,6 +334,7 @@ gap: 12px; justify-content: flex-start; width: 100%; + height: 100%; } [data-component="icon"] { From 03d884797c19e7cbe92d7ef237c4b28d51b58f18 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:59:56 -0600 Subject: [PATCH 022/232] wip(app): provider settings --- .../app/src/components/dialog-settings.tsx | 68 ++++---- .../app/src/components/settings-providers.tsx | 149 +++++++++++++++++- packages/app/src/i18n/en.ts | 10 ++ packages/opencode/src/server/server.ts | 30 ++++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 32 ++++ packages/sdk/js/src/v2/gen/types.gen.ts | 29 ++++ packages/sdk/openapi.json | 50 ++++++ 7 files changed, 322 insertions(+), 46 deletions(-) diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index dbbc8fa7adc..0b8d108d0ac 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -39,16 +39,30 @@ export const DialogSettings: Component = () => { "padding-top": "12px", }} > - {language.t("settings.section.desktop")} -
- - - {language.t("settings.tab.general")} - - - - {language.t("settings.tab.shortcuts")} - +
+
+ {language.t("settings.section.desktop")} +
+ + + {language.t("settings.tab.general")} + + + + {language.t("settings.tab.shortcuts")} + +
+
+ +
+ {language.t("settings.section.server")} +
+ + + {language.t("settings.providers.title")} + +
+
@@ -56,31 +70,6 @@ export const DialogSettings: Component = () => { v{platform.version}

- {/* Server */} - {/* */} - {/* */} - {/* Permissions */} - {/* */} - {/* */} - {/* */} - {/* Providers */} - {/* */} - {/* */} - {/* */} - {/* Models */} - {/* */} - {/* */} - {/* */} - {/* Agents */} - {/* */} - {/* */} - {/* */} - {/* Commands */} - {/* */} - {/* */} - {/* */} - {/* MCP */} - {/* */} @@ -88,12 +77,9 @@ export const DialogSettings: Component = () => { - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} + + + {/* */} {/* */} {/* */} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 7b6ca193924..b175a570b0e 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -1,14 +1,153 @@ -import { Component } from "solid-js" +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { Tag } from "@opencode-ai/ui/tag" +import { showToast } from "@opencode-ai/ui/toast" +import type { IconName } from "@opencode-ai/ui/icons/provider" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" +import { useGlobalSDK } from "@/context/global-sdk" +import { DialogConnectProvider } from "./dialog-connect-provider" +import { DialogSelectProvider } from "./dialog-select-provider" + +type ProviderSource = "env" | "api" | "config" | "custom" +type ProviderMeta = { source?: ProviderSource } export const SettingsProviders: Component = () => { + const dialog = useDialog() const language = useLanguage() + const globalSDK = useGlobalSDK() + const providers = useProviders() + + const connected = createMemo(() => providers.connected()) + const popular = createMemo(() => { + const items = providers.popular().slice() + items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)) + return items + }) + + const source = (item: unknown) => (item as ProviderMeta).source + + const disconnect = async (providerID: string, name: string) => { + await globalSDK.client.auth + .remove({ providerID }) + .then(async () => { + await globalSDK.client.global.dispose() + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }), + description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }), + }) + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + } return ( -
-
-

{language.t("settings.providers.title")}

-

{language.t("settings.providers.description")}

+
+
+
+
+

{language.t("settings.providers.title")}

+
+
+
+ +
+
+

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

+
+ 0} + fallback={ +
+ {language.t("settings.providers.connected.empty")} +
+ } + > + + {(item) => ( +
+
+ + {item.name} + + {language.t("settings.providers.tag.environment")} + + + {language.t("provider.connect.method.apiKey")} + +
+ + + +
+ )} +
+
+
+
+ +
+

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

+
+ + {(item) => ( +
+
+ + {item.name} + + {language.t("dialog.provider.tag.recommended")} + + +
{language.t("dialog.provider.anthropic.note")}
+
+ +
{language.t("dialog.provider.openai.note")}
+
+ +
{language.t("dialog.provider.copilot.note")}
+
+
+ +
+ )} +
+
+ + +
) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index b32f0348551..a34c8ef2184 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -137,6 +137,9 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} connected", "provider.connect.toast.connected.description": "{{provider}} models are now available to use.", + "provider.disconnect.toast.disconnected.title": "{{provider}} disconnected", + "provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.", + "model.tag.free": "Free", "model.tag.latest": "Latest", "model.provider.anthropic": "Anthropic", @@ -159,6 +162,8 @@ export const dict = { "common.loading": "Loading", "common.loading.ellipsis": "...", "common.cancel": "Cancel", + "common.connect": "Connect", + "common.disconnect": "Disconnect", "common.submit": "Submit", "common.save": "Save", "common.saving": "Saving...", @@ -491,6 +496,7 @@ export const dict = { "sidebar.project.viewAllSessions": "View all sessions", "settings.section.desktop": "Desktop", + "settings.section.server": "Server", "settings.tab.general": "General", "settings.tab.shortcuts": "Shortcuts", @@ -599,6 +605,10 @@ export const dict = { "settings.providers.title": "Providers", "settings.providers.description": "Provider settings will be configurable here.", + "settings.providers.section.connected": "Connected providers", + "settings.providers.connected.empty": "No connected providers", + "settings.providers.section.popular": "Popular providers", + "settings.providers.tag.environment": "Environment", "settings.models.title": "Models", "settings.models.description": "Model settings will be configurable here.", "settings.agents.title": "Agents", diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index fa646f21ea8..302c5376d29 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -441,6 +441,36 @@ export namespace Server { return c.json(true) }, ) + .delete( + "/auth/:providerID", + describeRoute({ + summary: "Remove auth credentials", + description: "Remove authentication credentials", + operationId: "auth.remove", + responses: { + 200: { + description: "Successfully removed authentication credentials", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "param", + z.object({ + providerID: z.string(), + }), + ), + async (c) => { + const providerID = c.req.valid("param").providerID + await Auth.remove(providerID) + return c.json(true) + }, + ) .get( "/event", describeRoute({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 67e7ac80cb9..d39dd2b3485 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -9,6 +9,8 @@ import type { AppLogResponses, AppSkillsResponses, Auth as Auth3, + AuthRemoveErrors, + AuthRemoveResponses, AuthSetErrors, AuthSetResponses, CommandListResponses, @@ -3054,6 +3056,36 @@ export class Formatter extends HeyApiClient { } export class Auth2 extends HeyApiClient { + /** + * Remove auth credentials + * + * Remove authentication credentials + */ + public remove( + parameters: { + providerID: string + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "directory" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/auth/{providerID}", + ...options, + ...params, + }) + } + /** * Set auth credentials * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 38a52b325ad..9258bc0cde6 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4867,6 +4867,35 @@ export type FormatterStatusResponses = { export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] +export type AuthRemoveData = { + body?: never + path: { + providerID: string + } + query?: { + directory?: string + } + url: "/auth/{providerID}" +} + +export type AuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] + +export type AuthRemoveResponses = { + /** + * Successfully removed authentication credentials + */ + 200: boolean +} + +export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] + export type AuthSetData = { body?: Auth path: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index a029d0ef0ed..8808bcf7d8c 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -5709,6 +5709,56 @@ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})" } ] + }, + "delete": { + "operationId": "auth.remove", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "providerID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Remove auth credentials", + "description": "Remove authentication credentials", + "responses": { + "200": { + "description": "Successfully removed authentication credentials", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})" + } + ] } }, "/event": { From c323d96deb3f1d98bc62160f4e954aad1a807253 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:05:24 -0600 Subject: [PATCH 023/232] wip(app): provider settings --- .../app/src/components/settings-providers.tsx | 36 ++++++++++--------- packages/app/src/context/global-sync.tsx | 1 + packages/app/src/i18n/en.ts | 3 ++ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index b175a570b0e..4d842037fa6 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -22,13 +22,28 @@ export const SettingsProviders: Component = () => { const connected = createMemo(() => providers.connected()) const popular = createMemo(() => { - const items = providers.popular().slice() + const connectedIDs = new Set(connected().map((p) => p.id)) + const items = providers + .popular() + .filter((p) => !connectedIDs.has(p.id)) + .slice() items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)) return items }) const source = (item: unknown) => (item as ProviderMeta).source + const type = (item: unknown) => { + const current = source(item) + if (current === "env") return language.t("settings.providers.tag.environment") + if (current === "api") return language.t("provider.connect.method.apiKey") + if (current === "config") return language.t("settings.providers.tag.config") + if (current === "custom") return language.t("settings.providers.tag.custom") + return language.t("settings.providers.tag.other") + } + + const canDisconnect = (item: unknown) => source(item) !== "env" + const disconnect = async (providerID: string, name: string) => { await globalSDK.client.auth .remove({ providerID }) @@ -48,14 +63,8 @@ export const SettingsProviders: Component = () => { } return ( -
-
+
+

{language.t("settings.providers.title")}

@@ -81,14 +90,9 @@ export const SettingsProviders: Component = () => {
{item.name} - - {language.t("settings.providers.tag.environment")} - - - {language.t("provider.connect.method.apiKey")} - + {type(item)}
- + diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 37d4d68912b..0da7cafa5f0 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -392,6 +392,7 @@ function createGlobalSync() { project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), provider: () => sdk.provider.list().then((x) => { + console.log("provider", x) setStore("provider", normalizeProviderList(x.data!)) }), agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index a34c8ef2184..44bd40d0956 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -609,6 +609,9 @@ export const dict = { "settings.providers.connected.empty": "No connected providers", "settings.providers.section.popular": "Popular providers", "settings.providers.tag.environment": "Environment", + "settings.providers.tag.config": "Config", + "settings.providers.tag.custom": "Custom", + "settings.providers.tag.other": "Other", "settings.models.title": "Models", "settings.models.description": "Model settings will be configurable here.", "settings.agents.title": "Agents", From 5993a098b4b1ec245c0795f4f79ab2fef1213487 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:40:28 -0600 Subject: [PATCH 024/232] fix(core): don't override source in custom provider loaders --- packages/opencode/src/provider/provider.ts | 23 +++++++++---------- .../opencode/test/provider/provider.test.ts | 7 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index fdd4ccdfb61..f898d3be430 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -854,10 +854,9 @@ export namespace Provider { // Load for the main provider if auth exists if (auth) { const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider]) - mergeProvider(plugin.auth.provider, { - source: "custom", - options: options, - }) + const opts = options ?? {} + const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } + mergeProvider(providerID, patch) } // If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists @@ -870,10 +869,11 @@ export namespace Provider { () => Auth.get(enterpriseProviderID) as any, database[enterpriseProviderID], ) - mergeProvider(enterpriseProviderID, { - source: "custom", - options: enterpriseOptions, - }) + const opts = enterpriseOptions ?? {} + const patch: Partial = providers[enterpriseProviderID] + ? { options: opts } + : { source: "custom", options: opts } + mergeProvider(enterpriseProviderID, patch) } } } @@ -889,10 +889,9 @@ export namespace Provider { const result = await fn(data) if (result && (result.autoload || providers[providerID])) { if (result.getModel) modelLoaders[providerID] = result.getModel - mergeProvider(providerID, { - source: "custom", - options: result.options, - }) + const opts = result.options ?? {} + const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } + mergeProvider(providerID, patch) } } diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 8a2009646e0..482587d8ac5 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -46,9 +46,10 @@ test("provider loaded from env variable", async () => { fn: async () => { const providers = await Provider.list() expect(providers["anthropic"]).toBeDefined() - // Note: source becomes "custom" because CUSTOM_LOADERS run after env loading - // and anthropic has a custom loader that merges additional options - expect(providers["anthropic"].source).toBe("custom") + // Provider should retain its connection source even if custom loaders + // merge additional options. + expect(providers["anthropic"].source).toBe("env") + expect(providers["anthropic"].options.headers["anthropic-beta"]).toBeDefined() }, }) }) From 00d960d0807b9d1c0e620cd03a763227af178bd3 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 20:17:53 -0600 Subject: [PATCH 025/232] chore: cleanup --- packages/app/src/components/settings-providers.tsx | 13 +++++-------- packages/app/src/i18n/ar.ts | 2 +- packages/app/src/i18n/br.ts | 2 +- packages/app/src/i18n/da.ts | 2 +- packages/app/src/i18n/de.ts | 2 +- packages/app/src/i18n/en.ts | 2 +- packages/app/src/i18n/es.ts | 2 +- packages/app/src/i18n/fr.ts | 2 +- packages/app/src/i18n/ja.ts | 2 +- packages/app/src/i18n/ko.ts | 2 +- packages/app/src/i18n/no.ts | 2 +- packages/app/src/i18n/pl.ts | 2 +- packages/app/src/i18n/ru.ts | 2 +- packages/app/src/i18n/zh.ts | 2 +- packages/app/src/i18n/zht.ts | 2 +- 15 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 4d842037fa6..b10ac5d3905 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -63,12 +63,10 @@ export const SettingsProviders: Component = () => { } return ( -
+
-
-
-

{language.t("settings.providers.title")}

-
+
+

{language.t("settings.providers.title")}

@@ -127,7 +125,7 @@ export const SettingsProviders: Component = () => {
    - (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) - .slice(0, 5)} - > + {(project) => ( diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 89056b2c845..b97f70fea74 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -41,11 +41,11 @@ export type Platform = { /** Fetch override */ fetch?: typeof fetch - /** Get the configured default server URL (desktop only) */ - getDefaultServerUrl?(): Promise + /** Get the configured default server URL (platform-specific) */ + getDefaultServerUrl?(): Promise | string | null - /** Set the default server URL to use on app startup (desktop only) */ - setDefaultServerUrl?(url: string | null): Promise + /** Set the default server URL to use on app startup (platform-specific) */ + setDefaultServerUrl?(url: string | null): Promise | void /** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */ parseMarkdown?(markdown: string): Promise diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index df8547636b4..7fe03bb6af2 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -6,6 +6,8 @@ import { dict as en } from "@/i18n/en" import { dict as zh } from "@/i18n/zh" import pkg from "../package.json" +const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" + const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { const locale = (() => { @@ -62,6 +64,26 @@ const platform: Platform = { }) .catch(() => undefined) }, + getDefaultServerUrl: () => { + if (typeof localStorage === "undefined") return null + try { + return localStorage.getItem(DEFAULT_SERVER_URL_KEY) + } catch { + return null + } + }, + setDefaultServerUrl: (url) => { + if (typeof localStorage === "undefined") return + try { + if (url) { + localStorage.setItem(DEFAULT_SERVER_URL_KEY, url) + return + } + localStorage.removeItem(DEFAULT_SERVER_URL_KEY) + } catch { + return + } + }, } render( From 3296b90372a86bc969a894b036eeafc2ef62adf9 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:00:12 -0600 Subject: [PATCH 040/232] fix(app): handle non-tool call permissions --- packages/app/src/pages/session.tsx | 201 ++++++++++++++++++++--------- 1 file changed, 143 insertions(+), 58 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 5f743dcbafe..47b9d76c539 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -17,6 +17,7 @@ import { Tabs } from "@opencode-ai/ui/tabs" import { useCodeComponent } from "@opencode-ai/ui/context/code" import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment" import { SessionTurn } from "@opencode-ai/ui/session-turn" +import { BasicTool } from "@opencode-ai/ui/basic-tool" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { SessionReview } from "@opencode-ai/ui/session-review" import { Mark } from "@opencode-ai/ui/logo" @@ -184,6 +185,40 @@ export default function Page() { const prompt = usePrompt() const comments = useComments() const permission = usePermission() + + const request = createMemo(() => { + const sessionID = params.id + if (!sessionID) return + const next = sync.data.permission[sessionID]?.[0] + if (!next) return + if (next.tool) return + return next + }) + + const [responding, setResponding] = createSignal(false) + + createEffect( + on( + () => request()?.id, + () => setResponding(false), + { defer: true }, + ), + ) + + const decide = (response: "once" | "always" | "reject") => { + const perm = request() + if (!perm) return + if (responding()) return + + setResponding(true) + sdk.client.permission + .respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + .finally(() => setResponding(false)) + } const [pendingMessage, setPendingMessage] = createSignal(undefined) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) @@ -730,7 +765,7 @@ export default function Page() { const sessionID = params.id if (!sessionID) return if (status()?.type !== "idle") { - await sdk.client.session.abort({ sessionID }).catch(() => {}) + await sdk.client.session.abort({ sessionID }).catch(() => { }) } const revert = info()?.revert?.messageID // Find the last user message that's not already reverted @@ -813,69 +848,69 @@ export default function Page() { }, ...(sync.data.config.share !== "disabled" ? [ - { - id: "session.share", - title: "Share session", - description: "Share this session and copy the URL to clipboard", - category: "Session", - slash: "share", - disabled: !params.id || !!info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .share({ sessionID: params.id }) - .then((res) => { - navigator.clipboard.writeText(res.data!.share!.url).catch(() => - showToast({ - title: "Failed to copy URL to clipboard", - variant: "error", - }), - ) - }) - .then(() => + { + id: "session.share", + title: "Share session", + description: "Share this session and copy the URL to clipboard", + category: "Session", + slash: "share", + disabled: !params.id || !!info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .share({ sessionID: params.id }) + .then((res) => { + navigator.clipboard.writeText(res.data!.share!.url).catch(() => showToast({ - title: "Session shared", - description: "Share URL copied to clipboard!", - variant: "success", - }), - ) - .catch(() => - showToast({ - title: "Failed to share session", - description: "An error occurred while sharing the session", + title: "Failed to copy URL to clipboard", variant: "error", }), ) - }, + }) + .then(() => + showToast({ + title: "Session shared", + description: "Share URL copied to clipboard!", + variant: "success", + }), + ) + .catch(() => + showToast({ + title: "Failed to share session", + description: "An error occurred while sharing the session", + variant: "error", + }), + ) }, - { - id: "session.unshare", - title: "Unshare session", - description: "Stop sharing this session", - category: "Session", - slash: "unshare", - disabled: !params.id || !info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .unshare({ sessionID: params.id }) - .then(() => - showToast({ - title: "Session unshared", - description: "Session unshared successfully!", - variant: "success", - }), - ) - .catch(() => - showToast({ - title: "Failed to unshare session", - description: "An error occurred while unsharing the session", - variant: "error", - }), - ) - }, + }, + { + id: "session.unshare", + title: "Unshare session", + description: "Stop sharing this session", + category: "Session", + slash: "unshare", + disabled: !params.id || !info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .unshare({ sessionID: params.id }) + .then(() => + showToast({ + title: "Session unshared", + description: "Session unshared successfully!", + variant: "success", + }), + ) + .catch(() => + showToast({ + title: "Failed to unshare session", + description: "An error occurred while unsharing the session", + variant: "error", + }), + ) }, - ] + }, + ] : []), ]) @@ -1690,6 +1725,56 @@ export default function Page() { "md:max-w-200": !showTabs(), }} > + + {(perm) => ( +
    + + 0}> +
    + + {(pattern) => {pattern}} + +
    +
    + +
    + {language.t("settings.permissions.tool.doom_loop.description")} +
    +
    +
    +
    +
    + + + +
    +
    +
    + )} +
    + Date: Mon, 26 Jan 2026 07:11:58 -0600 Subject: [PATCH 041/232] fix(app): line selection waits on ready --- packages/ui/src/components/code.tsx | 53 ++++++++++-- packages/ui/src/components/diff-ssr.tsx | 75 ++++++++++++++++- packages/ui/src/components/diff.tsx | 107 ++++++++++++++++-------- 3 files changed, 190 insertions(+), 45 deletions(-) diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx index a7687444f66..dbf942dbb6c 100644 --- a/packages/ui/src/components/code.tsx +++ b/packages/ui/src/components/code.tsx @@ -128,20 +128,56 @@ export function Code(props: CodeProps) { } } - const notifyRendered = () => { - if (!local.onRendered) return + const lineCount = () => { + const text = local.file.contents + const total = text.split("\n").length - (text.endsWith("\n") ? 1 : 0) + return Math.max(1, total) + } + + const applySelection = (range: SelectedLineRange | null) => { + const root = getRoot() + if (!root) return false + + const lines = lineCount() + if (root.querySelectorAll("[data-line]").length < lines) return false + + if (!range) { + file().setSelectedLines(null) + return true + } + + const start = Math.min(range.start, range.end) + const end = Math.max(range.start, range.end) + + if (start < 1 || end > lines) { + file().setSelectedLines(null) + return true + } + + if (!root.querySelector(`[data-line="${start}"]`) || !root.querySelector(`[data-line="${end}"]`)) { + file().setSelectedLines(null) + return true + } + + const normalized = (() => { + if (range.endSide != null) return { start: range.start, end: range.end } + if (range.side !== "deletions") return range + if (root.querySelector("[data-deletions]") != null) return range + return { start: range.start, end: range.end } + })() + file().setSelectedLines(normalized) + return true + } + + const notifyRendered = () => { observer?.disconnect() observer = undefined renderToken++ const token = renderToken - const lines = (() => { - const text = local.file.contents - const total = text.split("\n").length - (text.endsWith("\n") ? 1 : 0) - return Math.max(1, total) - })() + const lines = lineCount() const isReady = (root: ShadowRoot) => root.querySelectorAll("[data-line]").length >= lines @@ -152,6 +188,7 @@ export function Code(props: CodeProps) { observer = undefined requestAnimationFrame(() => { if (token !== renderToken) return + applySelection(lastSelection) local.onRendered?.() }) } @@ -241,7 +278,7 @@ export function Code(props: CodeProps) { const setSelectedLines = (range: SelectedLineRange | null) => { lastSelection = range - file().setSelectedLines(range) + applySelection(range) } const scheduleSelectionUpdate = () => { diff --git a/packages/ui/src/components/diff-ssr.tsx b/packages/ui/src/components/diff-ssr.tsx index 4ab63200821..602e59a2f57 100644 --- a/packages/ui/src/components/diff-ssr.tsx +++ b/packages/ui/src/components/diff-ssr.tsx @@ -38,6 +38,77 @@ export function Diff(props: SSRDiffProps) { fileDiffRef.removeAttribute("data-color-scheme") } + const lineIndex = (split: boolean, element: HTMLElement) => { + const raw = element.dataset.lineIndex + if (!raw) return + const values = raw + .split(",") + .map((value) => parseInt(value, 10)) + .filter((value) => !Number.isNaN(value)) + if (values.length === 0) return + if (!split) return values[0] + if (values.length === 2) return values[1] + return values[0] + } + + const rowIndex = (root: ShadowRoot, split: boolean, line: number, side: "additions" | "deletions" | undefined) => { + const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter( + (node): node is HTMLElement => node instanceof HTMLElement, + ) + if (nodes.length === 0) return + + const targetSide = side ?? "additions" + + for (const node of nodes) { + if (findSide(node) === targetSide) return lineIndex(split, node) + if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(split, node) + } + } + + const fixSelection = (range: SelectedLineRange | null) => { + if (!range) return range + const root = getRoot() + if (!root) return + + const diffs = root.querySelector("[data-diffs]") + if (!(diffs instanceof HTMLElement)) return + + const split = diffs.dataset.type === "split" + + const start = rowIndex(root, split, range.start, range.side) + const end = rowIndex(root, split, range.end, range.endSide ?? range.side) + + if (start === undefined || end === undefined) { + if (root.querySelector("[data-line], [data-alt-line]") == null) return + return null + } + if (start <= end) return range + + const side = range.endSide ?? range.side + const swapped: SelectedLineRange = { + start: range.end, + end: range.start, + } + if (side) swapped.side = side + if (range.endSide && range.side) swapped.endSide = range.side + + return swapped + } + + const setSelectedLines = (range: SelectedLineRange | null, attempt = 0) => { + const diff = fileDiffInstance + if (!diff) return + + const fixed = fixSelection(range) + if (fixed === undefined) { + if (attempt >= 120) return + requestAnimationFrame(() => setSelectedLines(range, attempt + 1)) + return + } + + diff.setSelectedLines(fixed) + } + const findSide = (element: HTMLElement): "additions" | "deletions" => { const line = element.closest("[data-line], [data-alt-line]") if (line instanceof HTMLElement) { @@ -159,14 +230,14 @@ export function Diff(props: SSRDiffProps) { containerWrapper: container, }) - fileDiffInstance.setSelectedLines(local.selectedLines ?? null) + setSelectedLines(local.selectedLines ?? null) createEffect(() => { fileDiffInstance?.setLineAnnotations(local.annotations ?? []) }) createEffect(() => { - fileDiffInstance?.setSelectedLines(local.selectedLines ?? null) + setSelectedLines(local.selectedLines ?? null) }) createEffect(() => { diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx index c97744a9f51..21dada53503 100644 --- a/packages/ui/src/components/diff.tsx +++ b/packages/ui/src/components/diff.tsx @@ -115,9 +115,64 @@ export function Diff(props: DiffProps) { host.removeAttribute("data-color-scheme") } - const notifyRendered = () => { - if (!local.onRendered) return + const lineIndex = (split: boolean, element: HTMLElement) => { + const raw = element.dataset.lineIndex + if (!raw) return + const values = raw + .split(",") + .map((value) => parseInt(value, 10)) + .filter((value) => !Number.isNaN(value)) + if (values.length === 0) return + if (!split) return values[0] + if (values.length === 2) return values[1] + return values[0] + } + + const rowIndex = (root: ShadowRoot, split: boolean, line: number, side: SelectionSide | undefined) => { + const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter( + (node): node is HTMLElement => node instanceof HTMLElement, + ) + if (nodes.length === 0) return + + const targetSide = side ?? "additions" + + for (const node of nodes) { + if (findSide(node) === targetSide) return lineIndex(split, node) + if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(split, node) + } + } + + const fixSelection = (range: SelectedLineRange | null) => { + if (!range) return range + const root = getRoot() + if (!root) return + + const diffs = root.querySelector("[data-diffs]") + if (!(diffs instanceof HTMLElement)) return + + const split = diffs.dataset.type === "split" + + const start = rowIndex(root, split, range.start, range.side) + const end = rowIndex(root, split, range.end, range.endSide ?? range.side) + if (start === undefined || end === undefined) { + if (root.querySelector("[data-line], [data-alt-line]") == null) return + return null + } + if (start <= end) return range + + const side = range.endSide ?? range.side + const swapped: SelectedLineRange = { + start: range.end, + end: range.start, + } + if (side) swapped.side = side + if (range.endSide && range.side) swapped.endSide = range.side + + return swapped + } + + const notifyRendered = () => { observer?.disconnect() observer = undefined renderToken++ @@ -134,6 +189,7 @@ export function Diff(props: DiffProps) { observer = undefined requestAnimationFrame(() => { if (token !== renderToken) return + setSelectedLines(lastSelection) local.onRendered?.() }) } @@ -173,7 +229,8 @@ export function Diff(props: DiffProps) { const root = getRoot() if (typeof MutationObserver === "undefined") { if (!root || !isReady(root)) return - local.onRendered() + setSelectedLines(lastSelection) + local.onRendered?.() return } @@ -214,41 +271,14 @@ export function Diff(props: DiffProps) { ) if (code.length === 0) return - const lineIndex = (element: HTMLElement) => { - const raw = element.dataset.lineIndex - if (!raw) return - const values = raw - .split(",") - .map((value) => parseInt(value, 10)) - .filter((value) => !Number.isNaN(value)) - if (values.length === 0) return - if (!split) return values[0] - if (values.length === 2) return values[1] - return values[0] - } - - const rowIndex = (line: number, side: SelectionSide | undefined) => { - const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter( - (node): node is HTMLElement => node instanceof HTMLElement, - ) - if (nodes.length === 0) return - - const targetSide = side ?? "additions" - - for (const node of nodes) { - if (findSide(node) === targetSide) return lineIndex(node) - if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(node) - } - } - for (const range of ranges) { - const start = rowIndex(range.start, range.side) + const start = rowIndex(root, split, range.start, range.side) if (start === undefined) continue const end = (() => { const same = range.end === range.start && (range.endSide == null || range.endSide === range.side) if (same) return start - return rowIndex(range.end, range.endSide ?? range.side) + return rowIndex(root, split, range.end, range.endSide ?? range.side) })() if (end === undefined) continue @@ -258,7 +288,7 @@ export function Diff(props: DiffProps) { for (const block of code) { for (const element of Array.from(block.children)) { if (!(element instanceof HTMLElement)) continue - const idx = lineIndex(element) + const idx = lineIndex(split, element) if (idx === undefined) continue if (idx > last) break if (idx < first) continue @@ -275,8 +305,15 @@ export function Diff(props: DiffProps) { const setSelectedLines = (range: SelectedLineRange | null) => { const active = current() if (!active) return - lastSelection = range - active.setSelectedLines(range) + + const fixed = fixSelection(range) + if (fixed === undefined) { + lastSelection = range + return + } + + lastSelection = fixed + active.setSelectedLines(fixed) } const updateSelection = () => { From 1934ee13d8439ed38b9bcebb4e26e4d2d01f6f08 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:23:01 -0600 Subject: [PATCH 042/232] wip(app): model settings --- .../app/src/components/dialog-settings.tsx | 14 +- .../app/src/components/settings-models.tsx | 131 +++++++++++++++++- 2 files changed, 133 insertions(+), 12 deletions(-) diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index 9dd6efd6881..5efee5a3c69 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -6,12 +6,8 @@ import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { SettingsGeneral } from "./settings-general" import { SettingsKeybinds } from "./settings-keybinds" -import { SettingsPermissions } from "./settings-permissions" import { SettingsProviders } from "./settings-providers" import { SettingsModels } from "./settings-models" -import { SettingsAgents } from "./settings-agents" -import { SettingsCommands } from "./settings-commands" -import { SettingsMcp } from "./settings-mcp" export const DialogSettings: Component = () => { const language = useLanguage() @@ -45,6 +41,10 @@ export const DialogSettings: Component = () => { {language.t("settings.providers.title")} + + + {language.t("settings.models.title")} +
@@ -64,9 +64,9 @@ export const DialogSettings: Component = () => { - {/* */} - {/* */} - {/* */} + + + {/* */} {/* */} {/* */} diff --git a/packages/app/src/components/settings-models.tsx b/packages/app/src/components/settings-models.tsx index 6a636879d08..a3ba45f6116 100644 --- a/packages/app/src/components/settings-models.tsx +++ b/packages/app/src/components/settings-models.tsx @@ -1,14 +1,135 @@ -import { Component } from "solid-js" +import { useFilteredList } from "@opencode-ai/ui/hooks" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { Switch } from "@opencode-ai/ui/switch" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { TextField } from "@opencode-ai/ui/text-field" +import type { IconName } from "@opencode-ai/ui/icons/provider" +import { type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" +import { type ModelKey, useLocal } from "@/context/local" +import { popularProviders } from "@/hooks/use-providers" + +type ModelItem = ReturnType["model"]["list"]>[number] export const SettingsModels: Component = () => { + const local = useLocal() const language = useLanguage() + const list = useFilteredList({ + items: (_filter) => local.model.list(), + key: (x) => `${x.provider.id}:${x.id}`, + filterKeys: ["provider.name", "name", "id"], + sortBy: (a, b) => a.name.localeCompare(b.name), + groupBy: (x) => x.provider.id, + sortGroupsBy: (a, b) => { + const aIndex = popularProviders.indexOf(a.category) + const bIndex = popularProviders.indexOf(b.category) + const aPopular = aIndex >= 0 + const bPopular = bIndex >= 0 + + if (aPopular && !bPopular) return -1 + if (!aPopular && bPopular) return 1 + if (aPopular && bPopular) return aIndex - bIndex + + const aName = a.items[0].provider.name + const bName = b.items[0].provider.name + return aName.localeCompare(bName) + }, + }) + return ( -
-
-

{language.t("settings.models.title")}

-

{language.t("settings.models.description")}

+
+
+
+

{language.t("settings.models.title")}

+
+ + + + + +
+
+
+ +
+ + + {language.t("common.loading")} + {language.t("common.loading.ellipsis")} + +
+ } + > + 0} + fallback={ +
+ {language.t("dialog.model.empty")} + + "{list.filter()}" + +
+ } + > + + {(group) => ( +
+
+ + {group.items[0].provider.name} +
+
+ + {(item) => { + const key: ModelKey = { providerID: item.provider.id, modelID: item.id } + return ( +
+
+ {item.name} +
+
+ { + local.model.setVisibility(key, checked) + }} + hideLabel + > + {item.name} + +
+
+ ) + }} +
+
+
+ )} +
+
+
) From 84b12a8fb74c8b0ba3b3d8c07516826df0793e89 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:14:33 -0600 Subject: [PATCH 043/232] feat(app): model settings --- packages/app/src/app.tsx | 9 +- .../app/src/components/settings-models.tsx | 14 +- packages/app/src/context/local.tsx | 117 ++------------- packages/app/src/context/models.tsx | 140 ++++++++++++++++++ 4 files changed, 166 insertions(+), 114 deletions(-) create mode 100644 packages/app/src/context/models.tsx diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 6b9f887fa4e..0980bedb933 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -21,6 +21,7 @@ import { PromptProvider } from "@/context/prompt" import { FileProvider } from "@/context/file" import { CommentsProvider } from "@/context/comments" import { NotificationProvider } from "@/context/notification" +import { ModelsProvider } from "@/context/models" import { DialogProvider } from "@opencode-ai/ui/context/dialog" import { CommandProvider } from "@/context/command" import { LanguageProvider, useLanguage } from "@/context/language" @@ -116,9 +117,11 @@ export function AppInterface(props: { defaultUrl?: string }) { - - {props.children} - + + + {props.children} + + diff --git a/packages/app/src/components/settings-models.tsx b/packages/app/src/components/settings-models.tsx index a3ba45f6116..3a73a42de7a 100644 --- a/packages/app/src/components/settings-models.tsx +++ b/packages/app/src/components/settings-models.tsx @@ -7,17 +7,17 @@ import { TextField } from "@opencode-ai/ui/text-field" import type { IconName } from "@opencode-ai/ui/icons/provider" import { type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" -import { type ModelKey, useLocal } from "@/context/local" +import { useModels } from "@/context/models" import { popularProviders } from "@/hooks/use-providers" -type ModelItem = ReturnType["model"]["list"]>[number] +type ModelItem = ReturnType["list"]>[number] export const SettingsModels: Component = () => { - const local = useLocal() const language = useLanguage() + const models = useModels() const list = useFilteredList({ - items: (_filter) => local.model.list(), + items: (_filter) => models.list(), key: (x) => `${x.provider.id}:${x.id}`, filterKeys: ["provider.name", "name", "id"], sortBy: (a, b) => a.name.localeCompare(b.name), @@ -103,7 +103,7 @@ export const SettingsModels: Component = () => {
{(item) => { - const key: ModelKey = { providerID: item.provider.id, modelID: item.id } + const key = { providerID: item.provider.id, modelID: item.id } return (
@@ -111,9 +111,9 @@ export const SettingsModels: Component = () => {
{ - local.model.setVisibility(key, checked) + models.setVisibility(key, checked) }} hideLabel > diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 43d47e1b1bc..4b27e6d37ab 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,14 +1,12 @@ import { createStore, produce, reconcile } from "solid-js/store" import { batch, createEffect, createMemo, onCleanup } from "solid-js" -import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "@opencode-ai/ui/context" import { useSDK } from "./sdk" import { useSync } from "./sync" import { base64Encode } from "@opencode-ai/util/encode" import { useProviders } from "@/hooks/use-providers" -import { DateTime } from "luxon" -import { Persist, persisted } from "@/utils/persist" +import { useModels } from "@/context/models" import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" @@ -112,18 +110,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ })() const model = (() => { - const [store, setStore, _, modelReady] = persisted( - Persist.global("model", ["model.v1"]), - createStore<{ - user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[] - recent: ModelKey[] - variant?: Record - }>({ - user: [], - recent: [], - variant: {}, - }), - ) + const models = useModels() const [ephemeral, setEphemeral] = createStore<{ model: Record @@ -131,57 +118,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ model: {}, }) - const available = createMemo(() => - providers.connected().flatMap((p) => - Object.values(p.models).map((m) => ({ - ...m, - provider: p, - })), - ), - ) - - const latest = createMemo(() => - pipe( - available(), - filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6), - groupBy((x) => x.provider.id), - mapValues((models) => - pipe( - models, - groupBy((x) => x.family), - values(), - (groups) => - groups.flatMap((g) => { - const first = firstBy(g, [(x) => x.release_date, "desc"]) - return first ? [{ modelID: first.id, providerID: first.provider.id }] : [] - }), - ), - ), - values(), - flat(), - ), - ) - - const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`))) - - const userVisibilityMap = createMemo(() => { - const map = new Map() - for (const item of store.user) { - map.set(`${item.providerID}:${item.modelID}`, item.visibility) - } - return map - }) - - const list = createMemo(() => - available().map((m) => ({ - ...m, - name: m.name.replace("(latest)", "").trim(), - latest: m.name.includes("(latest)"), - })), - ) - - const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID) - const fallbackModel = createMemo(() => { if (sync.data.config.model) { const [providerID, modelID] = sync.data.config.model.split("/") @@ -193,7 +129,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } } - for (const item of store.recent) { + for (const item of models.recent.list()) { if (isModelValid(item)) { return item } @@ -225,10 +161,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ fallbackModel, ) if (!key) return undefined - return find(key) + return models.find(key) }) - const recent = createMemo(() => store.recent.map(find).filter(Boolean)) + const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean)) const cycle = (direction: 1 | -1) => { const recentList = recent() @@ -253,54 +189,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) } - function updateVisibility(model: ModelKey, visibility: "show" | "hide") { - const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID) - if (index >= 0) { - setStore("user", index, { visibility }) - } else { - setStore("user", store.user.length, { ...model, visibility }) - } - } - return { - ready: modelReady, + ready: models.ready, current, recent, - list, + list: models.list, cycle, set(model: ModelKey | undefined, options?: { recent?: boolean }) { batch(() => { const currentAgent = agent.current() const next = model ?? fallbackModel() if (currentAgent) setEphemeral("model", currentAgent.name, next) - if (model) updateVisibility(model, "show") - if (options?.recent && model) { - const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID) - if (uniq.length > 5) uniq.pop() - setStore("recent", uniq) - } + if (model) models.setVisibility(model, true) + if (options?.recent && model) models.recent.push(model) }) }, visible(model: ModelKey) { - const key = `${model.providerID}:${model.modelID}` - const visibility = userVisibilityMap().get(key) - if (visibility === "hide") return false - if (visibility === "show") return true - if (latestSet().has(key)) return true - // For models without valid release_date (e.g. custom models), show by default - const m = find(model) - if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true - return false + return models.visible(model) }, setVisibility(model: ModelKey, visible: boolean) { - updateVisibility(model, visible ? "show" : "hide") + models.setVisibility(model, visible) }, variant: { current() { const m = current() if (!m) return undefined - const key = `${m.provider.id}/${m.id}` - return store.variant?.[key] + return models.variant.get({ providerID: m.provider.id, modelID: m.id }) }, list() { const m = current() @@ -311,12 +225,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ set(value: string | undefined) { const m = current() if (!m) return - const key = `${m.provider.id}/${m.id}` - if (!store.variant) { - setStore("variant", { [key]: value }) - } else { - setStore("variant", key, value) - } + models.variant.set({ providerID: m.provider.id, modelID: m.id }, value) }, cycle() { const variants = this.list() diff --git a/packages/app/src/context/models.tsx b/packages/app/src/context/models.tsx new file mode 100644 index 00000000000..fee3c10c6dc --- /dev/null +++ b/packages/app/src/context/models.tsx @@ -0,0 +1,140 @@ +import { createMemo } from "solid-js" +import { createStore } from "solid-js/store" +import { DateTime } from "luxon" +import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { useProviders } from "@/hooks/use-providers" +import { Persist, persisted } from "@/utils/persist" + +export type ModelKey = { providerID: string; modelID: string } + +type Visibility = "show" | "hide" +type User = ModelKey & { visibility: Visibility; favorite?: boolean } +type Store = { + user: User[] + recent: ModelKey[] + variant?: Record +} + +export const { use: useModels, provider: ModelsProvider } = createSimpleContext({ + name: "Models", + init: () => { + const providers = useProviders() + + const [store, setStore, _, ready] = persisted( + Persist.global("model", ["model.v1"]), + createStore({ + user: [], + recent: [], + variant: {}, + }), + ) + + const available = createMemo(() => + providers.connected().flatMap((p) => + Object.values(p.models).map((m) => ({ + ...m, + provider: p, + })), + ), + ) + + const latest = createMemo(() => + pipe( + available(), + filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6), + groupBy((x) => x.provider.id), + mapValues((models) => + pipe( + models, + groupBy((x) => x.family), + values(), + (groups) => + groups.flatMap((g) => { + const first = firstBy(g, [(x) => x.release_date, "desc"]) + return first ? [{ modelID: first.id, providerID: first.provider.id }] : [] + }), + ), + ), + values(), + flat(), + ), + ) + + const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`))) + + const visibility = createMemo(() => { + const map = new Map() + for (const item of store.user) map.set(`${item.providerID}:${item.modelID}`, item.visibility) + return map + }) + + const list = createMemo(() => + available().map((m) => ({ + ...m, + name: m.name.replace("(latest)", "").trim(), + latest: m.name.includes("(latest)"), + })), + ) + + const find = (key: ModelKey) => list().find((m) => m.id === key.modelID && m.provider.id === key.providerID) + + function update(model: ModelKey, state: Visibility) { + const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID) + if (index >= 0) { + setStore("user", index, { visibility: state }) + return + } + setStore("user", store.user.length, { ...model, visibility: state }) + } + + const visible = (model: ModelKey) => { + const key = `${model.providerID}:${model.modelID}` + const state = visibility().get(key) + if (state === "hide") return false + if (state === "show") return true + if (latestSet().has(key)) return true + const m = find(model) + if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true + return false + } + + const setVisibility = (model: ModelKey, state: boolean) => { + update(model, state ? "show" : "hide") + } + + const push = (model: ModelKey) => { + const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID) + if (uniq.length > 5) uniq.pop() + setStore("recent", uniq) + } + + const variantKey = (model: ModelKey) => `${model.providerID}/${model.modelID}` + const getVariant = (model: ModelKey) => store.variant?.[variantKey(model)] + + const setVariant = (model: ModelKey, value: string | undefined) => { + const key = variantKey(model) + if (!store.variant) { + setStore("variant", { [key]: value }) + return + } + setStore("variant", key, value) + } + + return { + ready, + list, + find, + visible, + setVisibility, + recent: { + list: createMemo(() => store.recent), + push, + }, + variant: { + get: getVariant, + set: setVariant, + }, + } + }, +}) From 7f75f71f6b970c714b9e74f5715073e07ca8e431 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 26 Jan 2026 14:15:46 +0000 Subject: [PATCH 044/232] chore: generate --- packages/app/src/pages/session.tsx | 116 ++++++++++++++--------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 47b9d76c539..bc89b2a48f8 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -765,7 +765,7 @@ export default function Page() { const sessionID = params.id if (!sessionID) return if (status()?.type !== "idle") { - await sdk.client.session.abort({ sessionID }).catch(() => { }) + await sdk.client.session.abort({ sessionID }).catch(() => {}) } const revert = info()?.revert?.messageID // Find the last user message that's not already reverted @@ -848,69 +848,69 @@ export default function Page() { }, ...(sync.data.config.share !== "disabled" ? [ - { - id: "session.share", - title: "Share session", - description: "Share this session and copy the URL to clipboard", - category: "Session", - slash: "share", - disabled: !params.id || !!info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .share({ sessionID: params.id }) - .then((res) => { - navigator.clipboard.writeText(res.data!.share!.url).catch(() => + { + id: "session.share", + title: "Share session", + description: "Share this session and copy the URL to clipboard", + category: "Session", + slash: "share", + disabled: !params.id || !!info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .share({ sessionID: params.id }) + .then((res) => { + navigator.clipboard.writeText(res.data!.share!.url).catch(() => + showToast({ + title: "Failed to copy URL to clipboard", + variant: "error", + }), + ) + }) + .then(() => showToast({ - title: "Failed to copy URL to clipboard", + title: "Session shared", + description: "Share URL copied to clipboard!", + variant: "success", + }), + ) + .catch(() => + showToast({ + title: "Failed to share session", + description: "An error occurred while sharing the session", variant: "error", }), ) - }) - .then(() => - showToast({ - title: "Session shared", - description: "Share URL copied to clipboard!", - variant: "success", - }), - ) - .catch(() => - showToast({ - title: "Failed to share session", - description: "An error occurred while sharing the session", - variant: "error", - }), - ) + }, }, - }, - { - id: "session.unshare", - title: "Unshare session", - description: "Stop sharing this session", - category: "Session", - slash: "unshare", - disabled: !params.id || !info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .unshare({ sessionID: params.id }) - .then(() => - showToast({ - title: "Session unshared", - description: "Session unshared successfully!", - variant: "success", - }), - ) - .catch(() => - showToast({ - title: "Failed to unshare session", - description: "An error occurred while unsharing the session", - variant: "error", - }), - ) + { + id: "session.unshare", + title: "Unshare session", + description: "Stop sharing this session", + category: "Session", + slash: "unshare", + disabled: !params.id || !info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .unshare({ sessionID: params.id }) + .then(() => + showToast({ + title: "Session unshared", + description: "Session unshared successfully!", + variant: "success", + }), + ) + .catch(() => + showToast({ + title: "Failed to unshare session", + description: "An error occurred while unsharing the session", + variant: "error", + }), + ) + }, }, - }, - ] + ] : []), ]) From af3d8c383e5fe2feba79ace5bb2e1082195a459d Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 06:28:25 -0600 Subject: [PATCH 045/232] wip(app): sidebar hover full --- packages/app/src/pages/layout.tsx | 768 ++++++++++++++++-------------- 1 file changed, 414 insertions(+), 354 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2702f119ba7..a14b4a8e50d 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -135,6 +135,21 @@ export default function Layout(props: ParentProps) { const editorRef = { current: undefined as HTMLInputElement | undefined } const [hoverSession, setHoverSession] = createSignal() + const [hoverProject, setHoverProject] = createSignal() + + const sidebarHovering = createMemo(() => !layout.sidebar.opened() && hoverProject() !== undefined) + const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering()) + + const hoverProjectData = createMemo(() => { + const id = hoverProject() + if (!id) return + return layout.projects.list().find((project) => project.worktree === id) + }) + + createEffect(() => { + if (!layout.sidebar.opened()) return + setHoverProject(undefined) + }) const autoselecting = createMemo(() => { if (params.dir) return false @@ -1119,15 +1134,13 @@ export default function Layout(props: ParentProps) { return language.t("common.requestFailed") } - const deleteWorkspace = async (directory: string) => { - const current = currentProject() - if (!current) return - if (directory === current.worktree) return + const deleteWorkspace = async (root: string, directory: string) => { + if (directory === root) return setBusy(directory, true) const result = await globalSDK.client.worktree - .remove({ directory: current.worktree, worktreeRemoveInput: { directory } }) + .remove({ directory: root, worktreeRemoveInput: { directory } }) .then((x) => x.data) .catch((err) => { showToast({ @@ -1142,17 +1155,15 @@ export default function Layout(props: ParentProps) { if (!result) return layout.projects.close(directory) - layout.projects.open(current.worktree) + layout.projects.open(root) if (params.dir && base64Decode(params.dir) === directory) { - navigateToProject(current.worktree) + navigateToProject(root) } } - const resetWorkspace = async (directory: string) => { - const current = currentProject() - if (!current) return - if (directory === current.worktree) return + const resetWorkspace = async (root: string, directory: string) => { + if (directory === root) return setBusy(directory, true) const progress = showToast({ @@ -1168,7 +1179,7 @@ export default function Layout(props: ParentProps) { .catch(() => []) const result = await globalSDK.client.worktree - .reset({ directory: current.worktree, worktreeResetInput: { directory } }) + .reset({ directory: root, worktreeResetInput: { directory } }) .then((x) => x.data) .catch((err) => { showToast({ @@ -1251,7 +1262,7 @@ export default function Layout(props: ParentProps) { ) } - function DialogDeleteWorkspace(props: { directory: string }) { + function DialogDeleteWorkspace(props: { root: string; directory: string }) { const name = createMemo(() => getFilename(props.directory)) const [data, setData] = createStore({ status: "loading" as "loading" | "ready" | "error", @@ -1259,12 +1270,6 @@ export default function Layout(props: ParentProps) { }) onMount(() => { - const current = currentProject() - if (!current) { - setData({ status: "error", dirty: false }) - return - } - globalSDK.client.file .status({ directory: props.directory }) .then((x) => { @@ -1279,7 +1284,7 @@ export default function Layout(props: ParentProps) { const handleDelete = () => { dialog.close() - void deleteWorkspace(props.directory) + void deleteWorkspace(props.root, props.directory) } const description = () => { @@ -1311,7 +1316,7 @@ export default function Layout(props: ParentProps) { ) } - function DialogResetWorkspace(props: { directory: string }) { + function DialogResetWorkspace(props: { root: string; directory: string }) { const name = createMemo(() => getFilename(props.directory)) const [state, setState] = createStore({ status: "loading" as "loading" | "ready" | "error", @@ -1329,12 +1334,6 @@ export default function Layout(props: ParentProps) { } onMount(() => { - const current = currentProject() - if (!current) { - setState({ status: "error", dirty: false }) - return - } - globalSDK.client.file .status({ directory: props.directory }) .then((x) => { @@ -1350,7 +1349,7 @@ export default function Layout(props: ParentProps) { const handleReset = () => { dialog.close() - void resetWorkspace(props.directory) + void resetWorkspace(props.root, props.directory) } const archivedCount = () => state.sessions.length @@ -1444,6 +1443,7 @@ export default function Layout(props: ParentProps) { function handleDragStart(event: unknown) { const id = getDraggableId(event) if (!id) return + setHoverProject(undefined) setStore("activeProject", id) } @@ -1483,6 +1483,13 @@ export default function Layout(props: ParentProps) { return [...merged, extra] } + const sidebarProject = createMemo(() => { + if (layout.sidebar.opened()) return currentProject() + const hovered = hoverProjectData() + if (hovered) return hovered + return currentProject() + }) + function handleWorkspaceDragStart(event: unknown) { const id = getDraggableId(event) if (!id) return @@ -1493,7 +1500,7 @@ export default function Layout(props: ParentProps) { const { draggable, droppable } = event if (!draggable || !droppable) return - const project = currentProject() + const project = sidebarProject() if (!project) return const ids = workspaceIds(project) @@ -1593,7 +1600,7 @@ export default function Layout(props: ParentProps) { sessionStore.message[props.session.id]?.filter((message) => message.role === "user"), ) const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined) - const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened()) + const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded()) const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const isActive = createMemo(() => props.session.id === params.id) const [menuOpen, setMenuOpen] = createSignal(false) @@ -1611,7 +1618,11 @@ export default function Layout(props: ParentProps) { class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menuOpen() ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`} onMouseEnter={() => prefetchSession(props.session, "high")} onFocus={() => prefetchSession(props.session, "high")} - onClick={() => setHoverSession(undefined)} + onClick={() => { + setHoverSession(undefined) + if (layout.sidebar.opened()) return + queueMicrotask(() => setHoverProject(undefined)) + }} >
{ const label = language.t("command.session.new") - const tooltip = () => props.mobile || !layout.sidebar.opened() + const tooltip = () => props.mobile || !sidebarExpanded() const item = ( setHoverSession(undefined)} + onClick={() => { + setHoverSession(undefined) + if (layout.sidebar.opened()) return + queueMicrotask(() => setHoverProject(undefined)) + }} >
@@ -1814,7 +1829,7 @@ export default function Layout(props: ParentProps) { const WorkspaceDragOverlay = (): JSX.Element => { const label = createMemo(() => { - const project = currentProject() + const project = sidebarProject() if (!project) return const directory = store.activeWorkspace if (!directory) return @@ -1985,13 +2000,21 @@ export default function Layout(props: ParentProps) { dialog.show(() => )} + onSelect={() => + dialog.show(() => ( + + )) + } > {language.t("common.reset")} dialog.show(() => )} + onSelect={() => + dialog.show(() => ( + + )) + } > {language.t("common.delete")} @@ -2005,9 +2028,7 @@ export default function Layout(props: ParentProps) {
+ diff --git a/packages/ui/src/components/resize-handle.css b/packages/ui/src/components/resize-handle.css index 6aac4c2fd04..c309ff838bc 100644 --- a/packages/ui/src/components/resize-handle.css +++ b/packages/ui/src/components/resize-handle.css @@ -21,6 +21,12 @@ transform: translateX(50%); cursor: col-resize; + &[data-edge="start"] { + inset-inline-start: 0; + inset-inline-end: auto; + transform: translateX(-50%); + } + &::after { width: 3px; inset-block: 0; @@ -36,6 +42,12 @@ transform: translateY(-50%); cursor: row-resize; + &[data-edge="end"] { + inset-block-start: auto; + inset-block-end: 0; + transform: translateY(50%); + } + &::after { height: 3px; inset-inline: 0; diff --git a/packages/ui/src/components/resize-handle.tsx b/packages/ui/src/components/resize-handle.tsx index 3ad01e27f99..e2eed1bb7c8 100644 --- a/packages/ui/src/components/resize-handle.tsx +++ b/packages/ui/src/components/resize-handle.tsx @@ -2,6 +2,7 @@ import { splitProps, type JSX } from "solid-js" export interface ResizeHandleProps extends Omit, "onResize"> { direction: "horizontal" | "vertical" + edge?: "start" | "end" size: number min: number max: number @@ -13,6 +14,7 @@ export interface ResizeHandleProps extends Omit { e.preventDefault() + const edge = local.edge ?? (local.direction === "vertical" ? "start" : "end") const start = local.direction === "horizontal" ? e.clientX : e.clientY const startSize = local.size let current = startSize @@ -34,7 +37,14 @@ export function ResizeHandle(props: ResizeHandleProps) { const onMouseMove = (moveEvent: MouseEvent) => { const pos = local.direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY - const delta = local.direction === "vertical" ? start - pos : pos - start + const delta = + local.direction === "vertical" + ? edge === "end" + ? pos - start + : start - pos + : edge === "start" + ? start - pos + : pos - start current = startSize + delta const clamped = Math.min(local.max, Math.max(local.min, current)) local.onResize(clamped) @@ -61,6 +71,7 @@ export function ResizeHandle(props: ResizeHandleProps) { {...rest} data-component="resize-handle" data-direction={local.direction} + data-edge={local.edge ?? (local.direction === "vertical" ? "start" : "end")} classList={{ ...(local.classList ?? {}), [local.class ?? ""]: !!local.class, diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 60ac0d51604..9a337c4538a 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -9,6 +9,7 @@ import { StickyAccordionHeader } from "./sticky-accordion-header" import { useDiffComponent } from "../context/diff" import { useI18n } from "../context/i18n" import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { checksum } from "@opencode-ai/util/encode" import { createEffect, createMemo, createSignal, For, Match, Show, Switch, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { type FileContent, type FileDiff } from "@opencode-ai/sdk/v2" @@ -118,6 +119,12 @@ function dataUrlFromValue(value: unknown): string | undefined { return `data:${mime};base64,${content}` } +function diffId(file: string): string | undefined { + const sum = checksum(file) + if (!sum) return + return `session-review-diff-${sum}` +} + type SessionReviewSelection = { file: string range: SelectedLineRange @@ -489,7 +496,12 @@ export const SessionReview = (props: SessionReviewProps) => { } return ( - +
From 801eb5d2cb868d0a14c056439e3898b110f4cc21 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:39:25 -0600 Subject: [PATCH 064/232] wip(app): file tree mode --- packages/app/src/components/file-tree.tsx | 85 ++++++++++++++++------- packages/app/src/pages/session.tsx | 65 +++++++++++++---- packages/ui/src/components/tabs.css | 53 ++++++++++++++ packages/ui/src/components/tabs.tsx | 2 +- 4 files changed, 165 insertions(+), 40 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index ac435095d45..a48f0039f0a 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -9,6 +9,7 @@ import { Match, splitProps, Switch, + untrack, type ComponentProps, type ParentProps, } from "solid-js" @@ -21,10 +22,14 @@ export default function FileTree(props: { nodeClass?: string level?: number allowed?: readonly string[] + draggable?: boolean + tooltip?: boolean onFileClick?: (file: FileNode) => void }) { const file = useFile() const level = props.level ?? 0 + const draggable = () => props.draggable ?? true + const tooltip = () => props.tooltip ?? true const filter = createMemo(() => { const allowed = props.allowed @@ -45,6 +50,18 @@ export default function FileTree(props: { return { files, dirs } }) + createEffect(() => { + const current = filter() + if (!current) return + if (level !== 0) return + + for (const dir of current.dirs) { + const expanded = untrack(() => file.tree.state(dir)?.expanded) ?? false + if (expanded) continue + file.tree.expand(dir) + } + }) + createEffect(() => { void file.tree.list(props.path) }) @@ -78,8 +95,9 @@ export default function FileTree(props: { [props.nodeClass ?? ""]: !!props.nodeClass, }} style={`padding-left: ${8 + level * 12}px`} - draggable={true} + draggable={draggable()} onDragStart={(e: DragEvent) => { + if (!draggable()) return e.dataTransfer?.setData("text/plain", `file:${local.node.path}`) e.dataTransfer?.setData("text/uri-list", `file://${local.node.path}`) if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy" @@ -123,41 +141,54 @@ export default function FileTree(props: { {(node) => { const expanded = () => file.tree.state(node.path)?.expanded ?? false + const Wrapper = (p: ParentProps) => { + if (!tooltip()) return p.children + return ( + + {p.children} + + ) + } + return ( - - - - (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))} - > - + + + (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))} + > + + - - - - - - - + + + + + + + + + props.onFileClick?.(node)}>
- - - + + + ) }} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 5160c528870..5ce03f40342 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1037,7 +1037,7 @@ export default function Page() { return `session-review-diff-${sum}` } - const scrollToReviewDiff = (path: string, behavior: ScrollBehavior) => { + const reviewDiffTop = (path: string) => { const root = reviewScroll() if (!root) return @@ -1050,15 +1050,25 @@ export default function Page() { const a = el.getBoundingClientRect() const b = root.getBoundingClientRect() - const top = a.top - b.top + root.scrollTop - root.scrollTo({ top, behavior }) + return a.top - b.top + root.scrollTop + } + + const scrollToReviewDiff = (path: string) => { + const root = reviewScroll() + if (!root) return false + + const top = reviewDiffTop(path) + if (top === undefined) return false + + view().setScroll("review", { x: root.scrollLeft, y: top }) + root.scrollTo({ top, behavior: "auto" }) + return true } const focusReviewDiff = (path: string) => { const current = view().review.open() ?? [] if (!current.includes(path)) view().review.setOpen([...current, path]) setPendingDiff(path) - requestAnimationFrame(() => scrollToReviewDiff(path, "smooth")) } createEffect(() => { @@ -1067,10 +1077,39 @@ export default function Page() { if (!reviewScroll()) return if (!diffsReady()) return - requestAnimationFrame(() => { - scrollToReviewDiff(pending, "smooth") - setPendingDiff(undefined) - }) + const attempt = (count: number) => { + if (pendingDiff() !== pending) return + if (count > 60) { + setPendingDiff(undefined) + return + } + + const root = reviewScroll() + if (!root) { + requestAnimationFrame(() => attempt(count + 1)) + return + } + + if (!scrollToReviewDiff(pending)) { + requestAnimationFrame(() => attempt(count + 1)) + return + } + + const top = reviewDiffTop(pending) + if (top === undefined) { + requestAnimationFrame(() => attempt(count + 1)) + return + } + + if (Math.abs(root.scrollTop - top) <= 1) { + setPendingDiff(undefined) + return + } + + requestAnimationFrame(() => attempt(count + 1)) + } + + requestAnimationFrame(() => attempt(0)) }) const activeTab = createMemo(() => { @@ -2605,12 +2644,12 @@ export default function Page() {
- - - + + + Changes - + All files @@ -2624,6 +2663,8 @@ export default function Page() { d.file)} + draggable={false} + tooltip={false} onFileClick={(node) => focusReviewDiff(node.path)} /> diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index cebbd3b4faa..2f3c914e146 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -212,6 +212,59 @@ /* } */ } + &[data-variant="pill"][data-orientation="horizontal"] { + background-color: transparent; + + [data-slot="tabs-list"] { + height: auto; + padding: 6px; + gap: 4px; + border-bottom: 1px solid var(--border-weak-base); + background-color: var(--background-base); + + &::after { + display: none; + } + } + + [data-slot="tabs-trigger-wrapper"] { + height: 32px; + border: none; + border-radius: 999px; + background-color: transparent; + gap: 0; + + /* text-13-medium */ + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + + [data-slot="tabs-trigger"] { + height: 100%; + width: 100%; + padding: 0 12px; + background-color: transparent; + } + + &:hover:not(:disabled) { + background-color: var(--surface-raised-base-hover); + color: var(--text-strong); + } + + &:has([data-selected]) { + background-color: var(--surface-raised-base-active); + color: var(--text-strong); + + &:hover:not(:disabled) { + background-color: var(--surface-raised-base-active); + } + } + } + } + &[data-orientation="vertical"] { flex-direction: row; diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index 825bfa85949..ddd22ec51eb 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -3,7 +3,7 @@ import { Show, splitProps, type JSX } from "solid-js" import type { ComponentProps, ParentProps, Component } from "solid-js" export interface TabsProps extends ComponentProps { - variant?: "normal" | "alt" | "settings" + variant?: "normal" | "alt" | "pill" | "settings" orientation?: "horizontal" | "vertical" } export interface TabsListProps extends ComponentProps {} From b8e8d82323f87080de377b4ac227356bac3e9726 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:55:37 -0600 Subject: [PATCH 065/232] chore: cleanup --- packages/app/src/components/file-tree.tsx | 15 +- packages/app/src/pages/session.tsx | 1226 ++++++++++----------- packages/ui/src/components/tabs.css | 3 +- 3 files changed, 596 insertions(+), 648 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index a48f0039f0a..c27ccbe6dd5 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -22,6 +22,7 @@ export default function FileTree(props: { nodeClass?: string level?: number allowed?: readonly string[] + modified?: readonly string[] draggable?: boolean tooltip?: boolean onFileClick?: (file: FileNode) => void @@ -50,6 +51,12 @@ export default function FileTree(props: { return { files, dirs } }) + const marks = createMemo(() => { + const modified = props.modified + if (!modified || modified.length === 0) return + return new Set(modified) + }) + createEffect(() => { const current = filter() if (!current) return @@ -89,7 +96,7 @@ export default function FileTree(props: { {local.node.name} + {local.node.type === "file" && marks()?.has(local.node.path) ? ( +
+ ) : null} ) } @@ -173,6 +183,7 @@ export default function FileTree(props: { path={node.path} level={level + 1} allowed={props.allowed} + modified={props.modified} draggable={props.draggable} tooltip={props.tooltip} onFileClick={props.onFileClick} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 5ce03f40342..458decfc414 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -425,6 +425,8 @@ export default function Page() { } const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) + const emptyDiffFiles: string[] = [] + const diffFiles = createMemo(() => diffs().map((d) => d.file), emptyDiffFiles, { equals: same }) const diffsReady = createMemo(() => { const id = params.id if (!id) return true @@ -1934,710 +1936,646 @@ export default function Page() { class="relative flex-1 min-w-0 h-full border-l border-border-weak-base flex" >
- -
-
- - - {language.t("session.review.loadingChanges")}
- } - > - addCommentToContext({ ...comment, origin: "review" })} - comments={comments.all()} - focusedComment={comments.focus()} - onFocusedCommentChange={comments.setFocus} - onViewFile={(path) => { - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - /> - - - -
- -
No changes in this session yet
-
-
- -
-
- - - - - - - -
- - - -
- - - -
-
{language.t("session.tab.review")}
- -
- {info()?.summary?.files ?? 0} -
+ + + + +
+ + + +
+ + -
-
- -
- - - tabs().close("context")} - aria-label={language.t("common.closeTab")} - /> - - } - hideCloseButton - onMiddleClick={() => tabs().close("context")} - > -
- -
{language.t("session.tab.context")}
-
-
-
- - {(tab) => } - -
- - dialog.show(() => )} - aria-label={language.t("command.file.open")} - /> - -
- -
- - - -
- - - - {language.t("session.review.loadingChanges")} +
+
{language.t("session.tab.review")}
+ +
+ {info()?.summary?.files ?? 0}
- } - > - addCommentToContext({ ...comment, origin: "review" })} - comments={comments.all()} - focusedComment={comments.focus()} - onFocusedCommentChange={comments.setFocus} - onViewFile={(path) => { - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - /> -
- - -
- -
- No changes in this session yet -
+
-
- +
+ +
+ + + tabs().close("context")} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close("context")} + > +
+ +
{language.t("session.tab.context")}
+
+
+
+ + + {(tab) => } + + +
+ + dialog.show(() => )} + aria-label={language.t("command.file.open")} + /> +
- - - + +
+ + + +
+ + + + {language.t("session.review.loadingChanges")} +
+ } + > + addCommentToContext({ ...comment, origin: "review" })} + comments={comments.all()} + focusedComment={comments.focus()} + onFocusedCommentChange={comments.setFocus} + onViewFile={(path) => { + const value = file.tab(path) + tabs().open(value) + file.load(path) + }} + /> +
+ + +
+ +
+ No changes in this session yet +
+
+
+ +
+
+ + - - -
- -
Select a file to open
-
-
-
- - - - -
- + + +
+ +
Select a file to open
-
- - - - {(tab) => { - let scroll: HTMLDivElement | undefined - let scrollFrame: number | undefined - let pending: { x: number; y: number } | undefined - let codeScroll: HTMLElement[] = [] - let focusToken = 0 - - const path = createMemo(() => file.pathFromTab(tab)) - const state = createMemo(() => { - const p = path() - if (!p) return - return file.get(p) - }) - const contents = createMemo(() => state()?.content?.content ?? "") - const cacheKey = createMemo(() => checksum(contents())) - const isImage = createMemo(() => { - const c = state()?.content - return ( - c?.encoding === "base64" && - c?.mimeType?.startsWith("image/") && - c?.mimeType !== "image/svg+xml" - ) - }) - const isSvg = createMemo(() => { - const c = state()?.content - return c?.mimeType === "image/svg+xml" - }) - const svgContent = createMemo(() => { - if (!isSvg()) return - const c = state()?.content - if (!c) return - if (c.encoding === "base64") return base64Decode(c.content) - return c.content - }) - const svgPreviewUrl = createMemo(() => { - if (!isSvg()) return - const c = state()?.content - if (!c) return - if (c.encoding === "base64") return `data:image/svg+xml;base64,${c.content}` - return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(c.content)}` - }) - const imageDataUrl = createMemo(() => { - if (!isImage()) return - const c = state()?.content - return `data:${c?.mimeType};base64,${c?.content}` - }) - const selectedLines = createMemo(() => { - const p = path() - if (!p) return null - if (file.ready()) return file.selectedLines(p) ?? null - return handoff.files[p] ?? null - }) - - let wrap: HTMLDivElement | undefined - - const fileComments = createMemo(() => { - const p = path() - if (!p) return [] - return comments.list(p) - }) - - const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection)) - - const [openedComment, setOpenedComment] = createSignal(null) - const [commenting, setCommenting] = createSignal(null) - const [draft, setDraft] = createSignal("") - const [positions, setPositions] = createSignal>({}) - const [draftTop, setDraftTop] = createSignal(undefined) - - const empty = {} as Record - - const commentLabel = (range: SelectedLineRange) => { - const start = Math.min(range.start, range.end) - const end = Math.max(range.start, range.end) - if (start === end) return `line ${start}` - return `lines ${start}-${end}` - } - - const getRoot = () => { - const el = wrap - if (!el) return - - const host = el.querySelector("diffs-container") - if (!(host instanceof HTMLElement)) return - - const root = host.shadowRoot - if (!root) return - - return root - } - - const findMarker = (root: ShadowRoot, range: SelectedLineRange) => { - const line = Math.max(range.start, range.end) - const node = root.querySelector(`[data-line="${line}"]`) - if (!(node instanceof HTMLElement)) return - return node - } - - const markerTop = (wrapper: HTMLElement, marker: HTMLElement) => { - const wrapperRect = wrapper.getBoundingClientRect() - const rect = marker.getBoundingClientRect() - return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2) - } - - const equal = (a: Record, b: Record) => { - const aKeys = Object.keys(a) - const bKeys = Object.keys(b) - if (aKeys.length !== bKeys.length) return false - for (const key of aKeys) { - if (a[key] !== b[key]) return false - } - return true - } - - const updateComments = () => { - const el = wrap - const root = getRoot() - if (!el || !root) { - setPositions((prev) => (Object.keys(prev).length === 0 ? prev : empty)) - setDraftTop((prev) => (prev === undefined ? prev : undefined)) - return - } + + - const next: Record = {} - for (const comment of fileComments()) { - const marker = findMarker(root, comment.selection) - if (!marker) continue - next[comment.id] = markerTop(el, marker) + + + +
+ +
+
+
+
+ + + {(tab) => { + let scroll: HTMLDivElement | undefined + let scrollFrame: number | undefined + let pending: { x: number; y: number } | undefined + let codeScroll: HTMLElement[] = [] + + const path = createMemo(() => file.pathFromTab(tab)) + const state = createMemo(() => { + const p = path() + if (!p) return + return file.get(p) + }) + const contents = createMemo(() => state()?.content?.content ?? "") + const cacheKey = createMemo(() => checksum(contents())) + const isImage = createMemo(() => { + const c = state()?.content + return ( + c?.encoding === "base64" && + c?.mimeType?.startsWith("image/") && + c?.mimeType !== "image/svg+xml" + ) + }) + const isSvg = createMemo(() => { + const c = state()?.content + return c?.mimeType === "image/svg+xml" + }) + const svgContent = createMemo(() => { + if (!isSvg()) return + const c = state()?.content + if (!c) return + if (c.encoding === "base64") return base64Decode(c.content) + return c.content + }) + const svgPreviewUrl = createMemo(() => { + if (!isSvg()) return + const c = state()?.content + if (!c) return + if (c.encoding === "base64") return `data:image/svg+xml;base64,${c.content}` + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(c.content)}` + }) + const imageDataUrl = createMemo(() => { + if (!isImage()) return + const c = state()?.content + return `data:${c?.mimeType};base64,${c?.content}` + }) + const selectedLines = createMemo(() => { + const p = path() + if (!p) return null + if (file.ready()) return file.selectedLines(p) ?? null + return handoff.files[p] ?? null + }) + + let wrap: HTMLDivElement | undefined + + const fileComments = createMemo(() => { + const p = path() + if (!p) return [] + return comments.list(p) + }) + + const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection)) + + const [openedComment, setOpenedComment] = createSignal(null) + const [commenting, setCommenting] = createSignal(null) + const [draft, setDraft] = createSignal("") + const [positions, setPositions] = createSignal>({}) + const [draftTop, setDraftTop] = createSignal(undefined) + + const commentLabel = (range: SelectedLineRange) => { + const start = Math.min(range.start, range.end) + const end = Math.max(range.start, range.end) + if (start === end) return `line ${start}` + return `lines ${start}-${end}` } - setPositions((prev) => (equal(prev, next) ? prev : next)) + const getRoot = () => { + const el = wrap + if (!el) return + + const host = el.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return - const range = commenting() - if (!range) { - setDraftTop(undefined) - return + const root = host.shadowRoot + if (!root) return + + return root } - const marker = findMarker(root, range) - if (!marker) { - setDraftTop(undefined) - return + const findMarker = (root: ShadowRoot, range: SelectedLineRange) => { + const line = Math.max(range.start, range.end) + const node = root.querySelector(`[data-line="${line}"]`) + if (!(node instanceof HTMLElement)) return + return node } - const nextTop = markerTop(el, marker) - setDraftTop((prev) => (prev === nextTop ? prev : nextTop)) - } + const markerTop = (wrapper: HTMLElement, marker: HTMLElement) => { + const wrapperRect = wrapper.getBoundingClientRect() + const rect = marker.getBoundingClientRect() + return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2) + } - let commentFrame: number | undefined + const updateComments = () => { + const el = wrap + const root = getRoot() + if (!el || !root) { + setPositions({}) + setDraftTop(undefined) + return + } - const scheduleComments = () => { - if (commentFrame !== undefined) return - commentFrame = requestAnimationFrame(() => { - commentFrame = undefined - updateComments() - }) - } - - createEffect(() => { - fileComments() - scheduleComments() - }) - - createEffect(() => { - commenting() - scheduleComments() - }) - - createEffect(() => { - const range = commenting() - if (!range) return - setDraft("") - }) - - createEffect(() => { - const focus = comments.focus() - const p = path() - if (!focus || !p) return - if (focus.file !== p) return - if (activeTab() !== tab) return - - const target = fileComments().find((comment) => comment.id === focus.id) - if (!target) return - - focusToken++ - const token = focusToken - - setOpenedComment(target.id) - setCommenting(null) - file.setSelectedLines(p, target.selection) - - const scrollTo = (attempt: number) => { - if (token !== focusToken) return - - const root = scroll - if (!root) { - if (attempt >= 120) return - requestAnimationFrame(() => scrollTo(attempt + 1)) + const next: Record = {} + for (const comment of fileComments()) { + const marker = findMarker(root, comment.selection) + if (!marker) continue + next[comment.id] = markerTop(el, marker) + } + + setPositions(next) + + const range = commenting() + if (!range) { + setDraftTop(undefined) return } - const anchor = root.querySelector(`[data-comment-id="${target.id}"]`) - const ready = - anchor instanceof HTMLElement && - anchor.style.pointerEvents !== "none" && - anchor.style.opacity !== "0" - - const shadow = getRoot() - const marker = shadow ? findMarker(shadow, target.selection) : undefined - const node = (ready ? anchor : (marker ?? wrap)) as HTMLElement | undefined - if (!node) { - if (attempt >= 120) return - requestAnimationFrame(() => scrollTo(attempt + 1)) + const marker = findMarker(root, range) + if (!marker) { + setDraftTop(undefined) return } - const rootRect = root.getBoundingClientRect() - const targetRect = node.getBoundingClientRect() - const offset = targetRect.top - rootRect.top - const next = root.scrollTop + offset - rootRect.height / 2 + targetRect.height / 2 - root.scrollTop = Math.max(0, next) + setDraftTop(markerTop(el, marker)) + } - if (ready || marker) return - if (attempt >= 120) return - requestAnimationFrame(() => scrollTo(attempt + 1)) + const scheduleComments = () => { + requestAnimationFrame(updateComments) } - requestAnimationFrame(() => scrollTo(0)) - requestAnimationFrame(() => comments.clearFocus()) - }) + createEffect(() => { + fileComments() + scheduleComments() + }) - const renderCode = (source: string, wrapperClass: string) => ( -
{ - wrap = el - scheduleComments() - }} - class={`relative overflow-hidden ${wrapperClass}`} - > - { - requestAnimationFrame(restoreScroll) - requestAnimationFrame(scheduleComments) - }} - onLineSelected={(range: SelectedLineRange | null) => { - const p = path() - if (!p) return - file.setSelectedLines(p, range) - if (!range) setCommenting(null) - }} - onLineSelectionEnd={(range: SelectedLineRange | null) => { - if (!range) { - setCommenting(null) - return - } - - setOpenedComment(null) - setCommenting(range) + createEffect(() => { + const range = commenting() + scheduleComments() + if (!range) return + setDraft("") + }) + + createEffect(() => { + const focus = comments.focus() + const p = path() + if (!focus || !p) return + if (focus.file !== p) return + if (activeTab() !== tab) return + + const target = fileComments().find((comment) => comment.id === focus.id) + if (!target) return + + setOpenedComment(target.id) + setCommenting(null) + file.setSelectedLines(p, target.selection) + requestAnimationFrame(() => comments.clearFocus()) + }) + + const renderCode = (source: string, wrapperClass: string) => ( +
{ + wrap = el + scheduleComments() }} - overflow="scroll" - class="select-text" - /> - - {(comment) => ( - { - const p = path() - if (!p) return - file.setSelectedLines(p, comment.selection) - }} - onClick={() => { - const p = path() - if (!p) return + class={`relative overflow-hidden ${wrapperClass}`} + > + { + requestAnimationFrame(restoreScroll) + requestAnimationFrame(scheduleComments) + }} + onLineSelected={(range: SelectedLineRange | null) => { + const p = path() + if (!p) return + file.setSelectedLines(p, range) + if (!range) setCommenting(null) + }} + onLineSelectionEnd={(range: SelectedLineRange | null) => { + if (!range) { setCommenting(null) - setOpenedComment((current) => (current === comment.id ? null : comment.id)) - file.setSelectedLines(p, comment.selection) - }} - comment={comment.comment} - selection={commentLabel(comment.selection)} - /> - )} - - - {(range) => ( - - setCommenting(null)} - onSubmit={(comment) => { + return + } + + setOpenedComment(null) + setCommenting(range) + }} + overflow="scroll" + class="select-text" + /> + + {(comment) => ( + { const p = path() if (!p) return - addCommentToContext({ - file: p, - selection: range(), - comment, - origin: "file", - }) - setCommenting(null) + file.setSelectedLines(p, comment.selection) }} - onPopoverFocusOut={(e) => { - const target = e.relatedTarget as Node | null - if (target && e.currentTarget.contains(target)) return - // Delay to allow click handlers to fire first - setTimeout(() => { - if ( - !document.activeElement || - !e.currentTarget.contains(document.activeElement) - ) { - setCommenting(null) - } - }, 0) + onClick={() => { + const p = path() + if (!p) return + setCommenting(null) + setOpenedComment((current) => (current === comment.id ? null : comment.id)) + file.setSelectedLines(p, comment.selection) }} /> - - )} - -
- ) + )} + + + {(range) => ( + + setDraft(value)} + onCancel={() => setCommenting(null)} + onSubmit={(value) => { + const p = path() + if (!p) return + addCommentToContext({ + file: p, + selection: range(), + comment: value, + origin: "file", + }) + setCommenting(null) + }} + onPopoverFocusOut={(e: FocusEvent) => { + const current = e.currentTarget as HTMLDivElement + const target = e.relatedTarget + if (target instanceof Node && current.contains(target)) return + + setTimeout(() => { + if (!document.activeElement || !current.contains(document.activeElement)) { + setCommenting(null) + } + }, 0) + }} + /> + + )} + +
+ ) - const getCodeScroll = () => { - const el = scroll - if (!el) return [] + const getCodeScroll = () => { + const el = scroll + if (!el) return [] - const host = el.querySelector("diffs-container") - if (!(host instanceof HTMLElement)) return [] + const host = el.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return [] - const root = host.shadowRoot - if (!root) return [] + const root = host.shadowRoot + if (!root) return [] - return Array.from(root.querySelectorAll("[data-code]")).filter( - (node): node is HTMLElement => node instanceof HTMLElement && node.clientWidth > 0, - ) - } + return Array.from(root.querySelectorAll("[data-code]")).filter( + (node): node is HTMLElement => node instanceof HTMLElement && node.clientWidth > 0, + ) + } - const queueScrollUpdate = (next: { x: number; y: number }) => { - pending = next - if (scrollFrame !== undefined) return + const queueScrollUpdate = (next: { x: number; y: number }) => { + pending = next + if (scrollFrame !== undefined) return - scrollFrame = requestAnimationFrame(() => { - scrollFrame = undefined + scrollFrame = requestAnimationFrame(() => { + scrollFrame = undefined - const next = pending - pending = undefined - if (!next) return + const next = pending + pending = undefined + if (!next) return - view().setScroll(tab, next) - }) - } + view().setScroll(tab, next) + }) + } - const handleCodeScroll = (event: Event) => { - const el = scroll - if (!el) return + const handleCodeScroll = (event: Event) => { + const el = scroll + if (!el) return - const target = event.currentTarget - if (!(target instanceof HTMLElement)) return + const target = event.currentTarget + if (!(target instanceof HTMLElement)) return - queueScrollUpdate({ - x: target.scrollLeft, - y: el.scrollTop, - }) - } + queueScrollUpdate({ + x: target.scrollLeft, + y: el.scrollTop, + }) + } - const syncCodeScroll = () => { - const next = getCodeScroll() - if (next.length === codeScroll.length && next.every((el, i) => el === codeScroll[i])) return + const syncCodeScroll = () => { + const next = getCodeScroll() + if (next.length === codeScroll.length && next.every((el, i) => el === codeScroll[i])) return - for (const item of codeScroll) { - item.removeEventListener("scroll", handleCodeScroll) - } + for (const item of codeScroll) { + item.removeEventListener("scroll", handleCodeScroll) + } - codeScroll = next + codeScroll = next - for (const item of codeScroll) { - item.addEventListener("scroll", handleCodeScroll) + for (const item of codeScroll) { + item.addEventListener("scroll", handleCodeScroll) + } } - } - const restoreScroll = () => { - const el = scroll - if (!el) return + const restoreScroll = () => { + const el = scroll + if (!el) return - const s = view()?.scroll(tab) - if (!s) return + const s = view()?.scroll(tab) + if (!s) return - syncCodeScroll() + syncCodeScroll() - if (codeScroll.length > 0) { - for (const item of codeScroll) { - if (item.scrollLeft !== s.x) item.scrollLeft = s.x + if (codeScroll.length > 0) { + for (const item of codeScroll) { + if (item.scrollLeft !== s.x) item.scrollLeft = s.x + } } + + if (el.scrollTop !== s.y) el.scrollTop = s.y + + if (codeScroll.length > 0) return + + if (el.scrollLeft !== s.x) el.scrollLeft = s.x } - if (el.scrollTop !== s.y) el.scrollTop = s.y + const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { + if (codeScroll.length === 0) syncCodeScroll() - if (codeScroll.length > 0) return + queueScrollUpdate({ + x: codeScroll[0]?.scrollLeft ?? event.currentTarget.scrollLeft, + y: event.currentTarget.scrollTop, + }) + } - if (el.scrollLeft !== s.x) el.scrollLeft = s.x - } + createEffect( + on( + () => state()?.loaded, + (loaded) => { + if (!loaded) return + requestAnimationFrame(restoreScroll) + }, + { defer: true }, + ), + ) - const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { - if (codeScroll.length === 0) syncCodeScroll() + createEffect( + on( + () => file.ready(), + (ready) => { + if (!ready) return + requestAnimationFrame(restoreScroll) + }, + { defer: true }, + ), + ) + + createEffect( + on( + () => tabs().active() === tab, + (active) => { + if (!active) return + if (!state()?.loaded) return + requestAnimationFrame(restoreScroll) + }, + ), + ) + + onCleanup(() => { + for (const item of codeScroll) { + item.removeEventListener("scroll", handleCodeScroll) + } - queueScrollUpdate({ - x: codeScroll[0]?.scrollLeft ?? event.currentTarget.scrollLeft, - y: event.currentTarget.scrollTop, + if (scrollFrame === undefined) return + cancelAnimationFrame(scrollFrame) }) - } - - createEffect( - on( - () => state()?.loaded, - (loaded) => { - if (!loaded) return - requestAnimationFrame(restoreScroll) - }, - { defer: true }, - ), - ) - - createEffect( - on( - () => file.ready(), - (ready) => { - if (!ready) return - requestAnimationFrame(restoreScroll) - }, - { defer: true }, - ), - ) - - createEffect( - on( - () => tabs().active() === tab, - (active) => { - if (!active) return - if (!state()?.loaded) return - requestAnimationFrame(restoreScroll) - }, - ), - ) - - onCleanup(() => { - if (commentFrame !== undefined) cancelAnimationFrame(commentFrame) - for (const item of codeScroll) { - item.removeEventListener("scroll", handleCodeScroll) - } - if (scrollFrame === undefined) return - cancelAnimationFrame(scrollFrame) - }) - - return ( - { - scroll = el - restoreScroll() + return ( + { + scroll = el + restoreScroll() + }} + onScroll={handleScroll} + > + + +
+ {path()} requestAnimationFrame(restoreScroll)} + /> +
+
+ +
+ {renderCode(svgContent() ?? "", "")} + +
+ {path()} +
+
+
+
+ {renderCode(contents(), "pb-40")} + +
{language.t("common.loading")}...
+
+ + {(err) =>
{err()}
} +
+
+
+ ) + }} +
+ + + + {(tab) => { + const path = createMemo(() => file.pathFromTab(tab())) + return ( +
+ {(p) => } +
+ ) + }} +
+
+ + } + > +
+
+ + + {language.t("session.review.loadingChanges")}
+ } + > + addCommentToContext({ ...comment, origin: "review" })} + comments={comments.all()} + focusedComment={comments.focus()} + onFocusedCommentChange={comments.setFocus} + onViewFile={(path) => { + const value = file.tab(path) + tabs().open(value) + file.load(path) }} - onScroll={handleScroll} - > - - -
- {path()} requestAnimationFrame(restoreScroll)} - /> -
-
- -
- {renderCode(svgContent() ?? "", "")} - -
- {path()} -
-
-
-
- {renderCode(contents(), "pb-40")} - -
{language.t("common.loading")}...
-
- - {(err) =>
{err()}
} -
-
- - ) - }} - - - - - {(tab) => { - const path = createMemo(() => file.pathFromTab(tab())) - return ( -
- {(p) => } -
- ) - }} -
-
- + /> + + + +
+ +
No changes in this session yet
+
+
+ +
+
@@ -2647,7 +2585,7 @@ export default function Page() { - Changes + {reviewCount()} {reviewCount() === 1 ? "Change" : "Changes"} All files @@ -2662,7 +2600,7 @@ export default function Page() { > d.file)} + allowed={diffFiles()} draggable={false} tooltip={false} onFileClick={(node) => focusReviewDiff(node.path)} @@ -2675,7 +2613,7 @@ export default function Page() { - openTab(file.tab(node.path))} /> + openTab(file.tab(node.path))} />
diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index 2f3c914e146..f02b7deb54a 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -219,7 +219,6 @@ height: auto; padding: 6px; gap: 4px; - border-bottom: 1px solid var(--border-weak-base); background-color: var(--background-base); &::after { @@ -230,7 +229,7 @@ [data-slot="tabs-trigger-wrapper"] { height: 32px; border: none; - border-radius: 999px; + border-radius: var(--radius-sm); background-color: transparent; gap: 0; From 37f1a1a4ef36eacb60ad5493db8aeb1130c5fa91 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:08:29 -0600 Subject: [PATCH 066/232] chore: cleanup --- packages/app/src/components/file-tree.tsx | 2 +- packages/ui/src/components/tabs.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index c27ccbe6dd5..283bceac720 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -96,7 +96,7 @@ export default function FileTree(props: { Date: Mon, 26 Jan 2026 10:04:59 -0600 Subject: [PATCH 067/232] chore(app): createStore over signals --- .../src/components/dialog-edit-project.tsx | 31 ++-- .../session/session-sortable-terminal-tab.tsx | 51 +++--- .../app/src/components/settings-keybinds.tsx | 45 +++--- .../app/src/components/status-popover.tsx | 23 ++- packages/app/src/context/command.tsx | 18 ++- packages/app/src/context/comments.tsx | 18 ++- packages/app/src/context/server.tsx | 39 ++--- packages/app/src/pages/layout.tsx | 147 ++++++++++-------- packages/app/src/pages/session.tsx | 99 ++++++++---- packages/app/src/utils/speech.ts | 41 +++-- 10 files changed, 294 insertions(+), 218 deletions(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 9e2bddc6be4..2ab25ceeb4e 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { TextField } from "@opencode-ai/ui/text-field" import { Icon } from "@opencode-ai/ui/icon" -import { createMemo, createSignal, For, Show } from "solid-js" +import { createMemo, For, Show } from "solid-js" import { createStore } from "solid-js/store" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" @@ -29,35 +29,34 @@ export function DialogEditProject(props: { project: LocalProject }) { iconUrl: props.project.icon?.override || "", startup: props.project.commands?.start ?? "", saving: false, + dragOver: false, + iconHover: false, }) - const [dragOver, setDragOver] = createSignal(false) - const [iconHover, setIconHover] = createSignal(false) - function handleFileSelect(file: File) { if (!file.type.startsWith("image/")) return const reader = new FileReader() reader.onload = (e) => { setStore("iconUrl", e.target?.result as string) - setIconHover(false) + setStore("iconHover", false) } reader.readAsDataURL(file) } function handleDrop(e: DragEvent) { e.preventDefault() - setDragOver(false) + setStore("dragOver", false) const file = e.dataTransfer?.files[0] if (file) handleFileSelect(file) } function handleDragOver(e: DragEvent) { e.preventDefault() - setDragOver(true) + setStore("dragOver", true) } function handleDragLeave() { - setDragOver(false) + setStore("dragOver", false) } function handleInputChange(e: Event) { @@ -116,19 +115,23 @@ export function DialogEditProject(props: { project: LocalProject }) {
-
setIconHover(true)} onMouseLeave={() => setIconHover(false)}> +
setStore("iconHover", true)} + onMouseLeave={() => setStore("iconHover", false)} + >
{ - if (store.iconUrl && iconHover()) { + if (store.iconUrl && store.iconHover) { clearIcon() } else { document.getElementById("icon-upload")?.click() @@ -166,7 +169,7 @@ export function DialogEditProject(props: { project: LocalProject }) { "border-radius": "6px", "z-index": 10, "pointer-events": "none", - opacity: iconHover() && !store.iconUrl ? 1 : 0, + opacity: store.iconHover && !store.iconUrl ? 1 : 0, display: "flex", "align-items": "center", "justify-content": "center", @@ -185,7 +188,7 @@ export function DialogEditProject(props: { project: LocalProject }) { "border-radius": "6px", "z-index": 10, "pointer-events": "none", - opacity: iconHover() && store.iconUrl ? 1 : 0, + opacity: store.iconHover && store.iconUrl ? 1 : 0, display: "flex", "align-items": "center", "justify-content": "center", 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 d16379e80ab..75e9b22f998 100644 --- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx +++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx @@ -1,5 +1,6 @@ import type { JSX } from "solid-js" -import { createSignal, Show } from "solid-js" +import { Show } from "solid-js" +import { createStore } from "solid-js/store" import { createSortable } from "@thisbeyond/solid-dnd" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tabs } from "@opencode-ai/ui/tabs" @@ -12,11 +13,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => const terminal = useTerminal() const language = useLanguage() const sortable = createSortable(props.terminal.id) - const [editing, setEditing] = createSignal(false) - const [title, setTitle] = createSignal(props.terminal.title) - const [menuOpen, setMenuOpen] = createSignal(false) - const [menuPosition, setMenuPosition] = createSignal({ x: 0, y: 0 }) - const [blurEnabled, setBlurEnabled] = createSignal(false) + const [store, setStore] = createStore({ + editing: false, + title: props.terminal.title, + menuOpen: false, + menuPosition: { x: 0, y: 0 }, + blurEnabled: false, + }) const isDefaultTitle = () => { const number = props.terminal.titleNumber @@ -47,7 +50,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => } const focus = () => { - if (editing()) return + if (store.editing) return if (document.activeElement instanceof HTMLElement) { document.activeElement.blur() @@ -71,26 +74,26 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => e.preventDefault() } - setBlurEnabled(false) - setTitle(props.terminal.title) - setEditing(true) + setStore("blurEnabled", false) + setStore("title", props.terminal.title) + setStore("editing", true) setTimeout(() => { const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement if (!input) return input.focus() input.select() - setTimeout(() => setBlurEnabled(true), 100) + setTimeout(() => setStore("blurEnabled", true), 100) }, 10) } const save = () => { - if (!blurEnabled()) return + if (!store.blurEnabled) return - const value = title().trim() + const value = store.title.trim() if (value && value !== props.terminal.title) { terminal.update({ id: props.terminal.id, title: value }) } - setEditing(false) + setStore("editing", false) } const keydown = (e: KeyboardEvent) => { @@ -101,14 +104,14 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => } if (e.key === "Escape") { e.preventDefault() - setEditing(false) + setStore("editing", false) } } const menu = (e: MouseEvent) => { e.preventDefault() - setMenuPosition({ x: e.clientX, y: e.clientY }) - setMenuOpen(true) + setStore("menuPosition", { x: e.clientX, y: e.clientY }) + setStore("menuOpen", true) } return ( @@ -143,17 +146,17 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => /> } > - + {label()} - +
setTitle(e.currentTarget.value)} + value={store.title} + onInput={(e) => setStore("title", e.currentTarget.value)} onBlur={save} onKeyDown={keydown} onMouseDown={(e) => e.stopPropagation()} @@ -161,13 +164,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => />
- + setStore("menuOpen", open)}> diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index 4dc39714955..7ff3425ab2c 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -1,4 +1,5 @@ -import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js" +import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -111,24 +112,26 @@ export const SettingsKeybinds: Component = () => { const language = useLanguage() const settings = useSettings() - const [active, setActive] = createSignal(null) - const [filter, setFilter] = createSignal("") + const [store, setStore] = createStore({ + active: null as string | null, + filter: "", + }) const stop = () => { - if (!active()) return - setActive(null) + if (!store.active) return + setStore("active", null) command.keybinds(true) } const start = (id: string) => { - if (active() === id) { + if (store.active === id) { stop() return } - if (active()) stop() + if (store.active) stop() - setActive(id) + setStore("active", id) command.keybinds(false) } @@ -203,7 +206,7 @@ export const SettingsKeybinds: Component = () => { }) const filtered = createMemo(() => { - const query = filter().toLowerCase().trim() + const query = store.filter.toLowerCase().trim() if (!query) return grouped() const map = list() @@ -285,7 +288,7 @@ export const SettingsKeybinds: Component = () => { onMount(() => { const handle = (event: KeyboardEvent) => { - const id = active() + const id = store.active if (!id) return event.preventDefault() @@ -345,7 +348,7 @@ export const SettingsKeybinds: Component = () => { }) onCleanup(() => { - if (active()) command.keybinds(true) + if (store.active) command.keybinds(true) }) return ( @@ -370,8 +373,8 @@ export const SettingsKeybinds: Component = () => { setStore("filter", v)} placeholder={language.t("settings.shortcuts.search.placeholder")} spellcheck={false} autocorrect="off" @@ -379,8 +382,8 @@ export const SettingsKeybinds: Component = () => { autocapitalize="off" class="flex-1" /> - - setFilter("")} /> + + setStore("filter", "")} />
@@ -402,13 +405,13 @@ export const SettingsKeybinds: Component = () => { classList={{ "h-8 px-3 rounded-md text-12-regular": true, "bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active": - active() !== id, - "border border-border-weak-base bg-surface-inset-base text-text-weak": active() === id, + store.active !== id, + "border border-border-weak-base bg-surface-inset-base text-text-weak": store.active === id, }} onClick={() => start(id)} > {language.t("settings.shortcuts.pressKeys")} @@ -423,11 +426,11 @@ export const SettingsKeybinds: Component = () => { )} - +
{language.t("settings.shortcuts.search.empty")} - - "{filter()}" + + "{store.filter}"
diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index cb45c36895b..c2c4d268a24 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -39,9 +39,10 @@ export function StatusPopover() { const language = useLanguage() const navigate = useNavigate() - const [loading, setLoading] = createSignal(null) const [store, setStore] = createStore({ status: {} as Record, + loading: null as string | null, + defaultServerUrl: undefined as string | undefined, }) const servers = createMemo(() => { @@ -97,8 +98,8 @@ export function StatusPopover() { const mcpConnected = createMemo(() => mcpItems().filter((i) => i.status === "connected").length) const toggleMcp = async (name: string) => { - if (loading()) return - setLoading(name) + if (store.loading) return + setStore("loading", name) const status = sync.data.mcp[name] if (status?.status === "connected") { await sdk.client.mcp.disconnect({ name }) @@ -107,7 +108,7 @@ export function StatusPopover() { } const result = await sdk.client.mcp.status() if (result.data) sync.set("mcp", result.data) - setLoading(null) + setStore("loading", null) } const lspItems = createMemo(() => sync.data.lsp ?? []) @@ -123,19 +124,17 @@ export function StatusPopover() { const serverCount = createMemo(() => sortedServers().length) - const [defaultServerUrl, setDefaultServerUrl] = createSignal() - const refreshDefaultServerUrl = () => { const result = platform.getDefaultServerUrl?.() if (!result) { - setDefaultServerUrl(undefined) + setStore("defaultServerUrl", undefined) return } if (result instanceof Promise) { - result.then((url) => setDefaultServerUrl(url ? normalizeServerUrl(url) : undefined)) + result.then((url) => setStore("defaultServerUrl", url ? normalizeServerUrl(url) : undefined)) return } - setDefaultServerUrl(normalizeServerUrl(result)) + setStore("defaultServerUrl", normalizeServerUrl(result)) } createEffect(() => { @@ -220,7 +219,7 @@ export function StatusPopover() { {(url) => { const isActive = () => url === server.url - const isDefault = () => url === defaultServerUrl() + const isDefault = () => url === store.defaultServerUrl const status = () => store.status[url] const isBlocked = () => status()?.healthy === false const [truncated, setTruncated] = createSignal(false) @@ -329,7 +328,7 @@ export function StatusPopover() { type="button" class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left" onClick={() => toggleMcp(item.name)} - disabled={loading() === item.name} + disabled={store.loading === item.name} >
event.stopPropagation()}> toggleMcp(item.name)} />
diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index dc5228b82de..79156958400 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js" +import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -165,8 +165,10 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const dialog = useDialog() const settings = useSettings() const language = useLanguage() - const [registrations, setRegistrations] = createSignal[]>([]) - const [suspendCount, setSuspendCount] = createSignal(0) + const [store, setStore] = createStore({ + registrations: [] as Accessor[], + suspendCount: 0, + }) const [catalog, setCatalog, _, catalogReady] = persisted( Persist.global("command.catalog.v1"), @@ -184,7 +186,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const seen = new Set() const all: CommandOption[] = [] - for (const reg of registrations()) { + for (const reg of store.registrations) { for (const opt of reg()) { if (seen.has(opt.id)) continue seen.add(opt.id) @@ -230,7 +232,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex ] }) - const suspended = () => suspendCount() > 0 + const suspended = () => store.suspendCount > 0 const palette = createMemo(() => { const config = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND @@ -297,9 +299,9 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex return { register(cb: () => CommandOption[]) { const results = createMemo(cb) - setRegistrations((arr) => [results, ...arr]) + setStore("registrations", (arr) => [results, ...arr]) onCleanup(() => { - setRegistrations((arr) => arr.filter((x) => x !== results)) + setStore("registrations", (arr) => arr.filter((x) => x !== results)) }) }, trigger(id: string, source?: "palette" | "keybind" | "slash") { @@ -321,7 +323,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex }, show: showPalette, keybinds(enabled: boolean) { - setSuspendCount((count) => count + (enabled ? -1 : 1)) + setStore("suspendCount", (count) => count + (enabled ? -1 : 1)) }, suspended, get catalog() { diff --git a/packages/app/src/context/comments.tsx b/packages/app/src/context/comments.tsx index 41123042d8b..f5551458748 100644 --- a/packages/app/src/context/comments.tsx +++ b/packages/app/src/context/comments.tsx @@ -1,4 +1,4 @@ -import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js" +import { batch, createMemo, createRoot, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { useParams } from "@solidjs/router" @@ -37,8 +37,16 @@ function createCommentSession(dir: string, id: string | undefined) { }), ) - const [focus, setFocus] = createSignal(null) - const [active, setActive] = createSignal(null) + const [state, setState] = createStore({ + focus: null as CommentFocus | null, + active: null as CommentFocus | null, + }) + + const setFocus = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) => + setState("focus", value) + + const setActive = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) => + setState("active", value) const list = (file: string) => store.comments[file] ?? [] @@ -74,10 +82,10 @@ function createCommentSession(dir: string, id: string | undefined) { all, add, remove, - focus: createMemo(() => focus()), + focus: createMemo(() => state.focus), setFocus, clearFocus: () => setFocus(null), - active: createMemo(() => active()), + active: createMemo(() => state.active), setActive, clearActive: () => setActive(null), } diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 1076570928f..4a3f3c6d1ad 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -1,6 +1,6 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" -import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { batch, createEffect, createMemo, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { usePlatform } from "@/context/platform" import { Persist, persisted } from "@/utils/persist" @@ -40,12 +40,17 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( }), ) - const [active, setActiveRaw] = createSignal("") + const [state, setState] = createStore({ + active: "", + healthy: undefined as boolean | undefined, + }) + + const healthy = () => state.healthy function setActive(input: string) { const url = normalizeServerUrl(input) if (!url) return - setActiveRaw(url) + setState("active", url) } function add(input: string) { @@ -54,7 +59,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( const fallback = normalizeServerUrl(props.defaultUrl) if (fallback && url === fallback) { - setActiveRaw(url) + setState("active", url) return } @@ -62,7 +67,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( if (!store.list.includes(url)) { setStore("list", store.list.length, url) } - setActiveRaw(url) + setState("active", url) }) } @@ -71,25 +76,23 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( if (!url) return const list = store.list.filter((x) => x !== url) - const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active() + const next = state.active === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : state.active batch(() => { setStore("list", list) - setActiveRaw(next) + setState("active", next) }) } createEffect(() => { if (!ready()) return - if (active()) return + if (state.active) return const url = normalizeServerUrl(props.defaultUrl) if (!url) return - setActiveRaw(url) + setState("active", url) }) - const isReady = createMemo(() => ready() && !!active()) - - const [healthy, setHealthy] = createSignal(undefined) + const isReady = createMemo(() => ready() && !!state.active) const check = (url: string) => { const sdk = createOpencodeClient({ @@ -104,10 +107,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( } createEffect(() => { - const url = active() + const url = state.active if (!url) return - setHealthy(undefined) + setState("healthy", undefined) let alive = true let busy = false @@ -118,7 +121,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( void check(url) .then((next) => { if (!alive) return - setHealthy(next) + setState("healthy", next) }) .finally(() => { busy = false @@ -134,7 +137,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( }) }) - const origin = createMemo(() => projectsKey(active())) + const origin = createMemo(() => projectsKey(state.active)) const projectsList = createMemo(() => store.projects[origin()] ?? []) const isLocal = createMemo(() => origin() === "local") @@ -143,10 +146,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( healthy, isLocal, get url() { - return active() + return state.active }, get name() { - return serverDisplayName(active()) + return serverDisplayName(state.active) }, get list() { return store.list diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 7ad97a989f4..fd6f6e527db 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -91,7 +91,6 @@ export default function Layout(props: ParentProps) { let scrollContainerRef: HTMLDivElement | undefined const params = useParams() - const [autoselect, setAutoselect] = createSignal(!params.dir) const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() const layout = useLayout() @@ -117,27 +116,31 @@ export default function Layout(props: ParentProps) { } const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme]) + const [state, setState] = createStore({ + autoselect: !params.dir, + busyWorkspaces: new Set(), + hoverSession: undefined as string | undefined, + hoverProject: undefined as string | undefined, + scrollSessionKey: undefined as string | undefined, + nav: undefined as HTMLElement | undefined, + }) + const [editor, setEditor] = createStore({ active: "" as string, value: "", }) - const [busyWorkspaces, setBusyWorkspaces] = createSignal>(new Set()) const setBusy = (directory: string, value: boolean) => { const key = workspaceKey(directory) - setBusyWorkspaces((prev) => { + setState("busyWorkspaces", (prev) => { const next = new Set(prev) if (value) next.add(key) else next.delete(key) return next }) } - const isBusy = (directory: string) => busyWorkspaces().has(workspaceKey(directory)) + const isBusy = (directory: string) => state.busyWorkspaces.has(workspaceKey(directory)) const editorRef = { current: undefined as HTMLInputElement | undefined } - const [hoverSession, setHoverSession] = createSignal() - const [hoverProject, setHoverProject] = createSignal() - - const [nav, setNav] = createSignal(undefined) const navLeave = { current: undefined as number | undefined } onCleanup(() => { @@ -145,18 +148,18 @@ export default function Layout(props: ParentProps) { clearTimeout(navLeave.current) }) - const sidebarHovering = createMemo(() => !layout.sidebar.opened() && hoverProject() !== undefined) + const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering()) const hoverProjectData = createMemo(() => { - const id = hoverProject() + const id = state.hoverProject if (!id) return return layout.projects.list().find((project) => project.worktree === id) }) createEffect(() => { if (!layout.sidebar.opened()) return - setHoverProject(undefined) + setState("hoverProject", undefined) }) createEffect( @@ -164,9 +167,9 @@ export default function Layout(props: ParentProps) { () => ({ dir: params.dir, id: params.id }), () => { if (layout.sidebar.opened()) return - if (!hoverProject()) return - setHoverSession(undefined) - setHoverProject(undefined) + if (!state.hoverProject) return + setState("hoverSession", undefined) + setState("hoverProject", undefined) }, { defer: true }, ), @@ -175,7 +178,7 @@ export default function Layout(props: ParentProps) { const autoselecting = createMemo(() => { if (params.dir) return false if (initialDir) return false - if (!autoselect()) return false + if (!state.autoselect) return false if (!pageReady()) return true if (!layoutReady()) return true const list = layout.projects.list() @@ -483,20 +486,18 @@ export default function Layout(props: ParentProps) { } } - const [scrollSessionKey, setScrollSessionKey] = createSignal(undefined) - function scrollToSession(sessionId: string, sessionKey: string) { if (!scrollContainerRef) return - if (scrollSessionKey() === sessionKey) return + if (state.scrollSessionKey === sessionKey) return const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`) if (!element) return const containerRect = scrollContainerRef.getBoundingClientRect() const elementRect = element.getBoundingClientRect() if (elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom) { - setScrollSessionKey(sessionKey) + setState("scrollSessionKey", sessionKey) return } - setScrollSessionKey(sessionKey) + setState("scrollSessionKey", sessionKey) element.scrollIntoView({ block: "nearest", behavior: "smooth" }) } @@ -544,7 +545,7 @@ export default function Layout(props: ParentProps) { (value) => { if (!value.ready) return if (!value.layoutReady) return - if (!autoselect()) return + if (!state.autoselect) return if (initialDir) return if (value.dir) return if (value.list.length === 0) return @@ -552,7 +553,7 @@ export default function Layout(props: ParentProps) { const last = server.projects.last() const next = value.list.find((project) => project.worktree === last) ?? value.list[0] if (!next) return - setAutoselect(false) + setState("autoselect", false) openProject(next.worktree, false) navigateToProject(next.worktree) }, @@ -1066,8 +1067,8 @@ export default function Layout(props: ParentProps) { function navigateToProject(directory: string | undefined) { if (!directory) return if (!layout.sidebar.opened()) { - setHoverSession(undefined) - setHoverProject(undefined) + setState("hoverSession", undefined) + setState("hoverProject", undefined) } server.projects.touch(directory) const lastSession = store.lastSession[directory] @@ -1078,8 +1079,8 @@ export default function Layout(props: ParentProps) { function navigateToSession(session: Session | undefined) { if (!session) return if (!layout.sidebar.opened()) { - setHoverSession(undefined) - setHoverProject(undefined) + setState("hoverSession", undefined) + setState("hoverProject", undefined) } navigate(`/${base64Encode(session.directory)}/session/${session.id}`) layout.mobileSidebar.hide() @@ -1472,7 +1473,7 @@ export default function Layout(props: ParentProps) { function handleDragStart(event: unknown) { const id = getDraggableId(event) if (!id) return - setHoverProject(undefined) + setState("hoverProject", undefined) setStore("activeProject", id) } @@ -1632,8 +1633,10 @@ export default function Layout(props: ParentProps) { const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded()) const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const isActive = createMemo(() => props.session.id === params.id) - const [menuOpen, setMenuOpen] = createSignal(false) - const [pendingRename, setPendingRename] = createSignal(false) + const [menu, setMenu] = createStore({ + open: false, + pendingRename: false, + }) const messageLabel = (message: Message) => { const parts = sessionStore.part[message.id] ?? [] @@ -1644,13 +1647,13 @@ export default function Layout(props: ParentProps) { const item = (
prefetchSession(props.session, "high")} onFocus={() => prefetchSession(props.session, "high")} onClick={() => { - setHoverSession(undefined) + setState("hoverSession", undefined) if (layout.sidebar.opened()) return - queueMicrotask(() => setHoverProject(undefined)) + queueMicrotask(() => setState("hoverProject", undefined)) }} >
@@ -1713,9 +1716,9 @@ export default function Layout(props: ParentProps) { gutter={16} shift={-2} trigger={item} - mount={!props.mobile ? nav() : undefined} - open={hoverSession() === props.session.id} - onOpenChange={(open) => setHoverSession(open ? props.session.id : undefined)} + mount={!props.mobile ? state.nav : undefined} + open={state.hoverSession === props.session.id} + onOpenChange={(open) => setState("hoverSession", open ? props.session.id : undefined)} > - + setMenu("open", open)}> - + { - if (!pendingRename()) return + if (!menu.pendingRename) return event.preventDefault() - setPendingRename(false) + setMenu("pendingRename", false) openEditor(`session:${props.session.id}`, props.session.title) }} > { - setPendingRename(true) - setMenuOpen(false) + setMenu("pendingRename", true) + setMenu("open", false) }} > {language.t("common.rename")} @@ -1802,9 +1805,9 @@ export default function Layout(props: ParentProps) { end class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`} onClick={() => { - setHoverSession(undefined) + setState("hoverSession", undefined) if (layout.sidebar.opened()) return - queueMicrotask(() => setHoverProject(undefined)) + queueMicrotask(() => setState("hoverProject", undefined)) }} >
@@ -1884,8 +1887,10 @@ export default function Layout(props: ParentProps) { const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => { const sortable = createSortable(props.directory) const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false }) - const [menuOpen, setMenuOpen] = createSignal(false) - const [pendingRename, setPendingRename] = createSignal(false) + const [menu, setMenu] = createStore({ + open: false, + pendingRename: false, + }) const slug = createMemo(() => base64Encode(props.directory)) const sessions = createMemo(() => workspaceStore.session @@ -1995,13 +2000,17 @@ export default function Layout(props: ParentProps) {
- + setMenu("open", open)} + > - + { - if (!pendingRename()) return + if (!menu.pendingRename) return event.preventDefault() - setPendingRename(false) + setMenu("pendingRename", false) openEditor(`workspace:${props.directory}`, workspaceValue()) }} > { - setPendingRename(true) - setMenuOpen(false) + setMenu("pendingRename", true) + setMenu("open", false) }} > {language.t("common.rename")} @@ -2103,7 +2112,7 @@ export default function Layout(props: ParentProps) { const preview = createMemo(() => !props.mobile && layout.sidebar.opened()) const overlay = createMemo(() => !props.mobile && !layout.sidebar.opened()) - const active = createMemo(() => (preview() ? open() : overlay() && hoverProject() === props.project.worktree)) + const active = createMemo(() => (preview() ? open() : overlay() && state.hoverProject === props.project.worktree)) createEffect(() => { if (preview()) return @@ -2155,14 +2164,14 @@ export default function Layout(props: ParentProps) { onMouseEnter={() => { if (!overlay()) return globalSync.child(props.project.worktree) - setHoverProject(props.project.worktree) - setHoverSession(undefined) + setState("hoverProject", props.project.worktree) + setState("hoverSession", undefined) }} onFocus={() => { if (!overlay()) return globalSync.child(props.project.worktree) - setHoverProject(props.project.worktree) - setHoverSession(undefined) + setState("hoverProject", props.project.worktree) + setState("hoverSession", undefined) }} onClick={() => navigateToProject(props.project.worktree)} onBlur={() => setOpen(false)} @@ -2184,7 +2193,7 @@ export default function Layout(props: ParentProps) { trigger={trigger} onOpenChange={(value) => { setOpen(value) - if (value) setHoverSession(undefined) + if (value) setState("hoverSession", undefined) }} >
@@ -2323,8 +2332,8 @@ export default function Layout(props: ParentProps) { const createWorkspace = async (project: LocalProject) => { if (!layout.sidebar.opened()) { - setHoverSession(undefined) - setHoverProject(undefined) + setState("hoverSession", undefined) + setState("hoverProject", undefined) } const created = await globalSDK.client.worktree .create({ directory: project.worktree }) @@ -2427,7 +2436,7 @@ export default function Layout(props: ParentProps) { class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active" aria-label={language.t("common.moreOptions")} /> - + dialog.show(() => )}> {language.t("common.edit")} @@ -2476,8 +2485,8 @@ export default function Layout(props: ParentProps) { class="w-full" onClick={() => { if (!layout.sidebar.opened()) { - setHoverSession(undefined) - setHoverProject(undefined) + setState("hoverSession", undefined) + setState("hoverProject", undefined) } navigate(`/${base64Encode(p.worktree)}/session`) layout.mobileSidebar.hide() @@ -2668,7 +2677,7 @@ export default function Layout(props: ParentProps) { }} style={{ width: layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "64px" }} ref={(el) => { - setNav(el) + setState("nav", el) }} onMouseEnter={() => { if (navLeave.current === undefined) return @@ -2681,8 +2690,8 @@ export default function Layout(props: ParentProps) { if (navLeave.current !== undefined) clearTimeout(navLeave.current) navLeave.current = window.setTimeout(() => { navLeave.current = undefined - setHoverProject(undefined) - setHoverSession(undefined) + setState("hoverProject", undefined) + setState("hoverSession", undefined) }, 300) }} > diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 458decfc414..e9b29c03e39 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" @@ -198,12 +198,17 @@ export default function Page() { return next }) - const [responding, setResponding] = createSignal(false) + const [ui, setUi] = createStore({ + responding: false, + pendingMessage: undefined as string | undefined, + scrollGesture: 0, + autoCreated: false, + }) createEffect( on( () => request()?.id, - () => setResponding(false), + () => setUi("responding", false), { defer: true }, ), ) @@ -211,18 +216,17 @@ export default function Page() { const decide = (response: "once" | "always" | "reject") => { const perm = request() if (!perm) return - if (responding()) return + if (ui.responding) return - setResponding(true) + setUi("responding", true) sdk.client.permission .respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) .catch((err: unknown) => { const message = err instanceof Error ? err.message : String(err) showToast({ title: language.t("common.requestFailed"), description: message }) }) - .finally(() => setResponding(false)) + .finally(() => setUi("responding", false)) } - const [pendingMessage, setPendingMessage] = createSignal(undefined) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) const view = createMemo(() => layout.view(sessionKey)) @@ -439,7 +443,6 @@ export default function Page() { let promptDock: HTMLDivElement | undefined let scroller: HTMLDivElement | undefined - const [scrollGesture, setScrollGesture] = createSignal(0) const scrollGestureWindowMs = 250 const markScrollGesture = (target?: EventTarget | null) => { @@ -450,26 +453,24 @@ export default function Page() { const nested = el?.closest("[data-scrollable]") if (nested && nested !== root) return - setScrollGesture(Date.now()) + setUi("scrollGesture", Date.now()) } - const hasScrollGesture = () => Date.now() - scrollGesture() < scrollGestureWindowMs + const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs createEffect(() => { if (!params.id) return sync.session.sync(params.id) }) - const [autoCreated, setAutoCreated] = createSignal(false) - createEffect(() => { if (!view().terminal.opened()) { - setAutoCreated(false) + setUi("autoCreated", false) return } - if (!terminal.ready() || terminal.all().length !== 0 || autoCreated()) return + if (!terminal.ready() || terminal.all().length !== 0 || ui.autoCreated) return terminal.new() - setAutoCreated(true) + setUi("autoCreated", true) }) createEffect( @@ -1019,9 +1020,18 @@ export default function Page() { const showTabs = createMemo(() => view().reviewPanel.opened()) - const [fileTreeTab, setFileTreeTab] = createSignal<"changes" | "all">("changes") - const [reviewScroll, setReviewScroll] = createSignal(undefined) - const [pendingDiff, setPendingDiff] = createSignal(undefined) + const [tree, setTree] = createStore({ + fileTreeTab: "changes" as "changes" | "all", + reviewScroll: undefined as HTMLDivElement | undefined, + pendingDiff: undefined as string | undefined, + }) + + const fileTreeTab = () => tree.fileTreeTab + const setFileTreeTab = (value: "changes" | "all") => setTree("fileTreeTab", value) + const reviewScroll = () => tree.reviewScroll + const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value) + const pendingDiff = () => tree.pendingDiff + const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value) createEffect(() => { if (!layout.fileTree.opened()) return @@ -1316,7 +1326,7 @@ export default function Page() { if (pendingSessionID !== sessionID) return sessionStorage.removeItem("opencode.pendingMessage") - setPendingMessage(messageID) + setUi("pendingMessage", messageID) }) const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { @@ -1484,7 +1494,7 @@ export default function Page() { store.turnStart const targetId = - pendingMessage() ?? + ui.pendingMessage ?? (() => { const hash = window.location.hash.slice(1) const match = hash.match(/^message-(.+)$/) @@ -1496,7 +1506,7 @@ export default function Page() { const msg = visibleUserMessages().find((m) => m.id === targetId) if (!msg) return - if (pendingMessage() === targetId) setPendingMessage(undefined) + if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined) requestAnimationFrame(() => scrollToMessage(msg, "auto")) }) @@ -1877,18 +1887,18 @@ export default function Page() {
- -
@@ -2144,11 +2154,40 @@ export default function Page() { const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection)) - const [openedComment, setOpenedComment] = createSignal(null) - const [commenting, setCommenting] = createSignal(null) - const [draft, setDraft] = createSignal("") - const [positions, setPositions] = createSignal>({}) - const [draftTop, setDraftTop] = createSignal(undefined) + const [note, setNote] = createStore({ + openedComment: null as string | null, + commenting: null as SelectedLineRange | null, + draft: "", + positions: {} as Record, + draftTop: undefined as number | undefined, + }) + + const openedComment = () => note.openedComment + const setOpenedComment = ( + value: + | typeof note.openedComment + | ((value: typeof note.openedComment) => typeof note.openedComment), + ) => setNote("openedComment", value) + + const commenting = () => note.commenting + const setCommenting = ( + value: typeof note.commenting | ((value: typeof note.commenting) => typeof note.commenting), + ) => setNote("commenting", value) + + const draft = () => note.draft + const setDraft = ( + value: typeof note.draft | ((value: typeof note.draft) => typeof note.draft), + ) => setNote("draft", value) + + const positions = () => note.positions + const setPositions = ( + value: typeof note.positions | ((value: typeof note.positions) => typeof note.positions), + ) => setNote("positions", value) + + const draftTop = () => note.draftTop + const setDraftTop = ( + value: typeof note.draftTop | ((value: typeof note.draftTop) => typeof note.draftTop), + ) => setNote("draftTop", value) const commentLabel = (range: SelectedLineRange) => { const start = Math.min(range.start, range.end) @@ -2695,7 +2734,7 @@ export default function Page() { terminal={pty} onClose={() => { view().terminal.close() - setAutoCreated(false) + setUi("autoCreated", false) }} /> )} diff --git a/packages/app/src/utils/speech.ts b/packages/app/src/utils/speech.ts index 921e0a159bc..c8acf5241c1 100644 --- a/packages/app/src/utils/speech.ts +++ b/packages/app/src/utils/speech.ts @@ -1,4 +1,5 @@ -import { createSignal, onCleanup } from "solid-js" +import { onCleanup } from "solid-js" +import { createStore } from "solid-js/store" // Minimal types to avoid relying on non-standard DOM typings type RecognitionResult = { @@ -59,9 +60,15 @@ export function createSpeechRecognition(opts?: { typeof window !== "undefined" && Boolean((window as any).webkitSpeechRecognition || (window as any).SpeechRecognition) - const [isRecording, setIsRecording] = createSignal(false) - const [committed, setCommitted] = createSignal("") - const [interim, setInterim] = createSignal("") + const [store, setStore] = createStore({ + isRecording: false, + committed: "", + interim: "", + }) + + const isRecording = () => store.isRecording + const committed = () => store.committed + const interim = () => store.interim let recognition: Recognition | undefined let shouldContinue = false @@ -82,7 +89,7 @@ export function createSpeechRecognition(opts?: { const nextCommitted = appendSegment(committedText, segment) if (nextCommitted === committedText) return committedText = nextCommitted - setCommitted(committedText) + setStore("committed", committedText) if (opts?.onFinal) opts.onFinal(segment.trim()) } @@ -98,7 +105,7 @@ export function createSpeechRecognition(opts?: { pendingHypothesis = "" lastInterimSuffix = "" shrinkCandidate = undefined - setInterim("") + setStore("interim", "") if (opts?.onInterim) opts.onInterim("") } @@ -107,7 +114,7 @@ export function createSpeechRecognition(opts?: { pendingHypothesis = hypothesis lastInterimSuffix = suffix shrinkCandidate = undefined - setInterim(suffix) + setStore("interim", suffix) if (opts?.onInterim) { opts.onInterim(suffix ? appendSegment(committedText, suffix) : "") } @@ -122,7 +129,7 @@ export function createSpeechRecognition(opts?: { pendingHypothesis = "" lastInterimSuffix = "" shrinkCandidate = undefined - setInterim("") + setStore("interim", "") if (opts?.onInterim) opts.onInterim("") }, COMMIT_DELAY) } @@ -162,7 +169,7 @@ export function createSpeechRecognition(opts?: { pendingHypothesis = "" lastInterimSuffix = "" shrinkCandidate = undefined - setInterim("") + setStore("interim", "") if (opts?.onInterim) opts.onInterim("") return } @@ -211,7 +218,7 @@ export function createSpeechRecognition(opts?: { lastInterimSuffix = "" shrinkCandidate = undefined if (e.error === "no-speech" && shouldContinue) { - setInterim("") + setStore("interim", "") if (opts?.onInterim) opts.onInterim("") setTimeout(() => { try { @@ -221,7 +228,7 @@ export function createSpeechRecognition(opts?: { return } shouldContinue = false - setIsRecording(false) + setStore("isRecording", false) } recognition.onstart = () => { @@ -230,16 +237,16 @@ export function createSpeechRecognition(opts?: { cancelPendingCommit() lastInterimSuffix = "" shrinkCandidate = undefined - setInterim("") + setStore("interim", "") if (opts?.onInterim) opts.onInterim("") - setIsRecording(true) + setStore("isRecording", true) } recognition.onend = () => { cancelPendingCommit() lastInterimSuffix = "" shrinkCandidate = undefined - setIsRecording(false) + setStore("isRecording", false) if (shouldContinue) { setTimeout(() => { try { @@ -258,7 +265,7 @@ export function createSpeechRecognition(opts?: { cancelPendingCommit() lastInterimSuffix = "" shrinkCandidate = undefined - setInterim("") + setStore("interim", "") try { recognition.start() } catch {} @@ -271,7 +278,7 @@ export function createSpeechRecognition(opts?: { cancelPendingCommit() lastInterimSuffix = "" shrinkCandidate = undefined - setInterim("") + setStore("interim", "") if (opts?.onInterim) opts.onInterim("") try { recognition.stop() @@ -284,7 +291,7 @@ export function createSpeechRecognition(opts?: { cancelPendingCommit() lastInterimSuffix = "" shrinkCandidate = undefined - setInterim("") + setStore("interim", "") if (opts?.onInterim) opts.onInterim("") try { recognition?.stop() From ec2ab639bb60ce42f84d74c04641dc4281e4a82c Mon Sep 17 00:00:00 2001 From: zerone0x Date: Tue, 27 Jan 2026 01:32:16 +0800 Subject: [PATCH 068/232] fix(enterprise): add message navigation to share page desktop view (#10071) Co-authored-by: Claude --- packages/enterprise/src/routes/share/[shareID].tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 483db4d9324..a2607891c8a 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -20,6 +20,7 @@ import { createStore } from "solid-js/store" import z from "zod" import NotFound from "../[...404]" import { Tabs } from "@opencode-ai/ui/tabs" +import { MessageNav } from "@opencode-ai/ui/message-nav" import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr" import { clientOnly } from "@solidjs/start" @@ -362,6 +363,15 @@ export default function () { {title()}
+ 1}> + + Date: Mon, 26 Jan 2026 11:57:59 -0600 Subject: [PATCH 069/232] chore: fix changelog page --- packages/console/app/src/routes/changelog.json.ts | 15 +++++++++++++++ .../console/app/src/routes/changelog/index.tsx | 10 +++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts index 9e3b75e5cd2..48dd985d7f4 100644 --- a/packages/console/app/src/routes/changelog.json.ts +++ b/packages/console/app/src/routes/changelog.json.ts @@ -20,6 +20,12 @@ type HighlightGroup = { items: HighlightItem[] } +const cors = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +} + const ok = "public, max-age=1, s-maxage=300, stale-while-revalidate=86400, stale-if-error=86400" const error = "public, max-age=1, s-maxage=60, stale-while-revalidate=600, stale-if-error=86400" @@ -106,6 +112,7 @@ export async function GET() { headers: { "Content-Type": "application/json", "Cache-Control": error, + ...cors, }, }) @@ -134,7 +141,15 @@ export async function GET() { headers: { "Content-Type": "application/json", "Cache-Control": ok, + ...cors, }, }, ) } + +export async function OPTIONS() { + return new Response(null, { + status: 200, + headers: cors, + }) +} diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index 2e2818da502..c7dca97a76a 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -31,11 +31,15 @@ type ChangelogRelease = { sections: { title: string; items: string[] }[] } -async function getReleases() { +function endpoint() { const event = getRequestEvent() - const url = event ? new URL("/changelog.json", event.request.url).toString() : "/changelog.json" + if (event) return new URL("/changelog.json", event.request.url).toString() + if (!import.meta.env.SSR) return "/changelog.json" + return `${config.baseUrl}/changelog.json` +} - const response = await fetch(url).catch(() => undefined) +async function getReleases() { + const response = await fetch(endpoint()).catch(() => undefined) if (!response?.ok) return [] const json = await response.json().catch(() => undefined) From 4c9d879624fe4233950fd2365cc190f60e074154 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:09:02 -0600 Subject: [PATCH 070/232] Revert "fix(app): restore external link opening in system browser (#10697)" This reverts commit 984518b1c0cb74db0b8eb9f77bb15fb97224a4e2. --- packages/desktop/src/index.tsx | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index b0ac8330738..fe9e3f92e24 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -328,23 +328,18 @@ render(() => { const [serverPassword, setServerPassword] = createSignal(null) const platform = createPlatform(() => serverPassword()) - onMount(() => { - // Handle external links - open in system browser instead of webview - const handleClick = (e: MouseEvent) => { - const target = e.target as HTMLElement - const link = target.closest("a") as HTMLAnchorElement | null - - if (link?.href && !link.href.startsWith("javascript:") && !link.href.startsWith("#")) { - e.preventDefault() - e.stopPropagation() - e.stopImmediatePropagation() - void shellOpen(link.href).catch(() => undefined) - } + function handleClick(e: MouseEvent) { + const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null + if (link?.href) { + e.preventDefault() + platform.openLink(link.href) } + } - document.addEventListener("click", handleClick, true) + onMount(() => { + document.addEventListener("click", handleClick) onCleanup(() => { - document.removeEventListener("click", handleClick, true) + document.removeEventListener("click", handleClick) }) }) From 7795cae0b55c09544dcdcf3051cefa2d473efe86 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 26 Jan 2026 12:51:48 -0500 Subject: [PATCH 071/232] ignore: tweak ai deps --- .opencode/command/ai-deps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.opencode/command/ai-deps.md b/.opencode/command/ai-deps.md index 4d23c76a4d5..83783d5b9be 100644 --- a/.opencode/command/ai-deps.md +++ b/.opencode/command/ai-deps.md @@ -7,7 +7,7 @@ Please read @package.json and @packages/opencode/package.json. Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes). I want a report of every dependency and the version that can be upgraded to. -What would be even better is if you can give me links to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added. +What would be even better is if you can give me brief summary of the changes for each dep and a link to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added. Consider using subagents for each dep to save your context window. From ac53a372b0b97fca93f887036f5957eddb88b606 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 26 Jan 2026 13:17:59 -0500 Subject: [PATCH 072/232] feat: use anthropic compat messages api for anthropic models through copilot --- packages/opencode/src/plugin/copilot.ts | 22 ++++++++++++++++------ packages/opencode/src/provider/provider.ts | 9 +++++---- packages/opencode/src/session/llm.ts | 22 ++++++++++++++-------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/plugin/copilot.ts b/packages/opencode/src/plugin/copilot.ts index 369acf800f3..659090dd3a1 100644 --- a/packages/opencode/src/plugin/copilot.ts +++ b/packages/opencode/src/plugin/copilot.ts @@ -26,6 +26,9 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { const info = await getAuth() if (!info || info.type !== "oauth") return {} + const enterpriseUrl = info.enterpriseUrl + const baseURL = enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : undefined + if (provider && provider.models) { for (const model of Object.values(provider.models)) { model.cost = { @@ -36,16 +39,23 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { write: 0, }, } + + // TODO: move some of this hacky-ness to models.dev presets once we have better grasp of things here... + const base = baseURL ?? model.api.url + const claude = model.id.includes("claude") + const url = iife(() => { + if (!claude) return base + if (base.endsWith("/v1")) return base + if (base.endsWith("/")) return `${base}v1` + return `${base}/v1` + }) + + model.api.url = url + model.api.npm = claude ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot" } } - const enterpriseUrl = info.enterpriseUrl - const baseURL = enterpriseUrl - ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` - : "https://api.githubcopilot.com" - return { - baseURL, apiKey: "", async fetch(request: RequestInfo | URL, init?: RequestInit) { const info = await getAuth() diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index f898d3be430..ee7ee75c9f5 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -132,6 +132,7 @@ export namespace Provider { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { + if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID) return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID) }, options: {}, @@ -141,6 +142,7 @@ export namespace Provider { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { + if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID) return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID) }, options: {}, @@ -601,10 +603,7 @@ export namespace Provider { api: { id: model.id, url: provider.api!, - npm: iife(() => { - if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot" - return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible" - }), + npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", }, status: model.status ?? "active", headers: model.headers ?? {}, @@ -924,6 +923,8 @@ export namespace Provider { ) delete provider.models[modelID] + model.variants = mapValues(ProviderTransform.variants(model), (v) => v) + // Filter out disabled variants from config const configVariants = configProvider?.models?.[modelID]?.variants if (configVariants && model.variants) { diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 1e409b03fe6..d651308032e 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -150,14 +150,20 @@ export namespace LLM { }, ) - const maxOutputTokens = isCodex - ? undefined - : ProviderTransform.maxOutputTokens( - input.model.api.npm, - params.options, - input.model.limit.output, - OUTPUT_TOKEN_MAX, - ) + const maxOutputTokens = isCodex ? undefined : undefined + log.info("max_output_tokens", { + tokens: ProviderTransform.maxOutputTokens( + input.model.api.npm, + params.options, + input.model.limit.output, + OUTPUT_TOKEN_MAX, + ), + modelOptions: params.options, + outputLimit: input.model.limit.output, + }) + // tokens = 32000 + // outputLimit = 64000 + // modelOptions={"reasoningEffort":"minimal"} const tools = await resolveTools(input) From b0f865eae5a79742560ddea472fa8ba25871aa3d Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:21:24 -0600 Subject: [PATCH 073/232] chore: debug changelog --- .../console/app/src/routes/changelog.json.ts | 33 ++++- .../app/src/routes/changelog/index.tsx | 115 ++++++++++++++++-- 2 files changed, 136 insertions(+), 12 deletions(-) diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts index 48dd985d7f4..f82c8421d98 100644 --- a/packages/console/app/src/routes/changelog.json.ts +++ b/packages/console/app/src/routes/changelog.json.ts @@ -104,7 +104,12 @@ export async function GET() { cacheTtl: 60 * 5, cacheEverything: true, }, - } as any).catch(() => undefined) + } as any).catch((err) => { + console.error("[changelog.json] fetch failed", { + error: err instanceof Error ? err.message : String(err), + }) + return undefined + }) const fail = () => new Response(JSON.stringify({ releases: [] }), { @@ -116,10 +121,30 @@ export async function GET() { }, }) - if (!response?.ok) return fail() + if (!response) return fail() + if (!response.ok) { + const body = await response.text().catch(() => undefined) + console.error("[changelog.json] github non-ok", { + status: response.status, + remaining: response.headers.get("x-ratelimit-remaining"), + reset: response.headers.get("x-ratelimit-reset"), + body: body?.slice(0, 300), + }) + return fail() + } - const data = await response.json().catch(() => undefined) - if (!Array.isArray(data)) return fail() + const data = await response.json().catch((err) => { + console.error("[changelog.json] json parse failed", { + error: err instanceof Error ? err.message : String(err), + }) + return undefined + }) + if (!Array.isArray(data)) { + console.error("[changelog.json] invalid json", { + type: typeof data, + }) + return fail() + } const releases = data as Release[] diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index c7dca97a76a..ec5cd4e9d80 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -1,11 +1,11 @@ import "./index.css" import { Title, Meta, Link } from "@solidjs/meta" -import { createAsync } from "@solidjs/router" +import { createAsync, useSearchParams } from "@solidjs/router" import { Header } from "~/component/header" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" import { config } from "~/config" -import { For, Show, createSignal } from "solid-js" +import { For, Show, createSignal, onMount } from "solid-js" import { getRequestEvent } from "solid-js/web" type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } @@ -31,6 +31,21 @@ type ChangelogRelease = { sections: { title: string; items: string[] }[] } +type LoadMeta = { + endpoint: string + ssr: boolean + hasEvent: boolean + ok: boolean + status?: number + contentType?: string + error?: string +} + +type Load = { + releases: ChangelogRelease[] + meta: LoadMeta +} + function endpoint() { const event = getRequestEvent() if (event) return new URL("/changelog.json", event.request.url).toString() @@ -38,12 +53,64 @@ function endpoint() { return `${config.baseUrl}/changelog.json` } -async function getReleases() { - const response = await fetch(endpoint()).catch(() => undefined) - if (!response?.ok) return [] +async function getReleases(debug = false): Promise { + const url = endpoint() + const meta = { + endpoint: url, + ssr: import.meta.env.SSR, + hasEvent: Boolean(getRequestEvent()), + ok: false, + } satisfies LoadMeta + + const response = await fetch(url).catch((err) => { + console.error("[changelog] fetch failed", { + ...meta, + error: err instanceof Error ? err.message : String(err), + }) + return undefined + }) + + if (!response) return { releases: [], meta: { ...meta, error: "fetch_failed" } } + if (!response.ok) { + const contentType = response.headers.get("content-type") ?? undefined + const body = debug ? await response.text().catch(() => undefined) : undefined + console.error("[changelog] fetch non-ok", { + ...meta, + status: response.status, + contentType, + body: body?.slice(0, 300), + }) + return { releases: [], meta: { ...meta, status: response.status, contentType, error: "bad_status" } } + } + + const contentType = response.headers.get("content-type") ?? undefined + const copy = debug ? response.clone() : undefined + const json = await response.json().catch(async (err) => { + const body = copy ? await copy.text().catch(() => undefined) : undefined + console.error("[changelog] json parse failed", { + ...meta, + status: response.status, + contentType, + error: err instanceof Error ? err.message : String(err), + body: body?.slice(0, 300), + }) + return undefined + }) + + const releases = Array.isArray(json?.releases) ? (json.releases as ChangelogRelease[]) : [] + if (!releases.length) { + console.error("[changelog] empty releases", { + ...meta, + status: response.status, + contentType, + keys: json && typeof json === "object" ? Object.keys(json) : undefined, + }) + } - const json = await response.json().catch(() => undefined) - return Array.isArray(json?.releases) ? (json.releases as ChangelogRelease[]) : [] + return { + releases, + meta: { ...meta, ok: true, status: response.status, contentType }, + } } function formatDate(dateString: string) { @@ -134,7 +201,22 @@ function CollapsibleSections(props: { sections: { title: string; items: string[] } export default function Changelog() { - const releases = createAsync(() => getReleases()) + const [params] = useSearchParams() + const debug = () => params.debug === "1" + const data = createAsync(() => getReleases(debug())) + const [client, setClient] = createSignal(undefined) + const releases = () => client()?.releases ?? data()?.releases ?? [] + + onMount(() => { + queueMicrotask(async () => { + const server = data()?.releases + if (!server) return + if (server.length) return + + const next = await getReleases(debug()) + setClient(next) + }) + }) return (
@@ -152,6 +234,23 @@ export default function Changelog() {
+ +

+ No changelog entries found. View JSON +

+ + +
+                {JSON.stringify(
+                  {
+                    server: data()?.meta,
+                    client: client()?.meta,
+                  },
+                  null,
+                  2,
+                )}
+              
+
{(release) => { return ( From 837037cd04d94841cfa470eb8639f2397bd3ba87 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 Jan 2026 07:50:04 +1300 Subject: [PATCH 074/232] fix: ensure openai 404 errors are retried (#10590) --- packages/opencode/src/session/message-v2.ts | 9 ++++++++- packages/opencode/test/session/retry.test.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 7c8689037ef..650fda6e949 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -656,6 +656,13 @@ export namespace MessageV2 { return result } + const isOpenAiErrorRetryable = (e: APICallError) => { + const status = e.statusCode + if (!status) return e.isRetryable + // openai sometimes returns 404 for models that are actually available + return status === 404 || e.isRetryable + } + export function fromError(e: unknown, ctx: { providerID: string }) { switch (true) { case e instanceof DOMException && e.name === "AbortError": @@ -724,7 +731,7 @@ export namespace MessageV2 { { message, statusCode: e.statusCode, - isRetryable: e.isRetryable, + isRetryable: ctx.providerID.startsWith("openai") ? isOpenAiErrorRetryable(e) : e.isRetryable, responseHeaders: e.responseHeaders, responseBody: e.responseBody, metadata, diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index b130e927e4a..10137ed988c 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test" +import { APICallError } from "ai" import { SessionRetry } from "../../src/session/retry" import { MessageV2 } from "../../src/session/message-v2" @@ -128,4 +129,18 @@ describe("session.message-v2.fromError", () => { expect(retryable).toBeDefined() expect(retryable).toBe("Connection reset by server") }) + + test("marks OpenAI 404 status codes as retryable", () => { + const error = new APICallError({ + message: "boom", + url: "https://api.openai.com/v1/chat/completions", + requestBodyValues: {}, + statusCode: 404, + responseHeaders: { "content-type": "application/json" }, + responseBody: "{\"error\":\"boom\"}", + isRetryable: false, + }) + const result = MessageV2.fromError(error, { providerID: "openai" }) as MessageV2.APIError + expect(result.data.isRetryable).toBe(true) + }) }) From 18bfc740c839e9a6b931c7c5d0ed1a8bec98c904 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 26 Jan 2026 18:50:58 +0000 Subject: [PATCH 075/232] chore: generate --- packages/opencode/test/session/retry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 10137ed988c..22ffb0cb117 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -137,7 +137,7 @@ describe("session.message-v2.fromError", () => { requestBodyValues: {}, statusCode: 404, responseHeaders: { "content-type": "application/json" }, - responseBody: "{\"error\":\"boom\"}", + responseBody: '{"error":"boom"}', isRetryable: false, }) const result = MessageV2.fromError(error, { providerID: "openai" }) as MessageV2.APIError From 8b17ac656cb428b1eee5f425deb195dc61f844ba Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:44:17 -0600 Subject: [PATCH 076/232] test(app): e2e test for sidebar nav --- .../app/e2e/sidebar-session-links.spec.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 packages/app/e2e/sidebar-session-links.spec.ts diff --git a/packages/app/e2e/sidebar-session-links.spec.ts b/packages/app/e2e/sidebar-session-links.spec.ts new file mode 100644 index 00000000000..fab64736e26 --- /dev/null +++ b/packages/app/e2e/sidebar-session-links.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from "./fixtures" +import { modKey, promptSelector } from "./utils" + +type Locator = { + first: () => Locator + getAttribute: (name: string) => Promise + scrollIntoViewIfNeeded: () => Promise + click: () => Promise +} + +type Page = { + locator: (selector: string) => Locator + keyboard: { + press: (key: string) => Promise + } +} + +type Fixtures = { + page: Page + slug: string + sdk: { + session: { + create: (input: { title: string }) => Promise<{ data?: { id?: string } }> + delete: (input: { sessionID: string }) => Promise + } + } + gotoSession: (sessionID?: string) => Promise +} + +test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }: Fixtures) => { + const stamp = Date.now() + + const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data) + const two = await sdk.session.create({ title: `e2e sidebar nav 2 ${stamp}` }).then((r) => r.data) + + if (!one?.id) throw new Error("Session create did not return an id") + if (!two?.id) throw new Error("Session create did not return an id") + + try { + await gotoSession(one.id) + + const main = page.locator("main") + const collapsed = ((await main.getAttribute("class")) ?? "").includes("xl:border-l") + if (collapsed) { + await page.keyboard.press(`${modKey}+B`) + await expect(main).not.toHaveClass(/xl:border-l/) + } + + const target = page.locator(`[data-session-id="${two.id}"] a`).first() + await expect(target).toBeVisible() + await target.scrollIntoViewIfNeeded() + await target.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/) + } finally { + await sdk.session.delete({ sessionID: one.id }).catch(() => undefined) + await sdk.session.delete({ sessionID: two.id }).catch(() => undefined) + } +}) From de3b654dcd195448f1d19e55562b2d84a2db7c91 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:51:32 -0600 Subject: [PATCH 077/232] chore: refactor changelog --- packages/console/app/src/lib/changelog.ts | 146 +++++++++++++++ .../console/app/src/routes/changelog.json.ts | 166 +----------------- .../app/src/routes/changelog/index.tsx | 142 +-------------- 3 files changed, 160 insertions(+), 294 deletions(-) create mode 100644 packages/console/app/src/lib/changelog.ts diff --git a/packages/console/app/src/lib/changelog.ts b/packages/console/app/src/lib/changelog.ts new file mode 100644 index 00000000000..93a0d423c67 --- /dev/null +++ b/packages/console/app/src/lib/changelog.ts @@ -0,0 +1,146 @@ +import { query } from "@solidjs/router" + +type Release = { + tag_name: string + name: string + body: string + published_at: string + html_url: string +} + +export type HighlightMedia = + | { type: "video"; src: string } + | { type: "image"; src: string; width: string; height: string } + +export type HighlightItem = { + title: string + description: string + shortDescription?: string + media: HighlightMedia +} + +export type HighlightGroup = { + source: string + items: HighlightItem[] +} + +export type ChangelogRelease = { + tag: string + name: string + date: string + url: string + highlights: HighlightGroup[] + sections: { title: string; items: string[] }[] +} + +export type ChangelogData = { + ok: boolean + releases: ChangelogRelease[] +} + +export async function loadChangelog(): Promise { + const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "OpenCode-Console", + }, + cf: { + // best-effort edge caching (ignored outside Cloudflare) + cacheTtl: 60 * 5, + cacheEverything: true, + }, + } as RequestInit).catch(() => undefined) + + if (!response?.ok) return { ok: false, releases: [] } + + const data = await response.json().catch(() => undefined) + if (!Array.isArray(data)) return { ok: false, releases: [] } + + const releases = (data as Release[]).map((release) => { + const parsed = parseMarkdown(release.body || "") + return { + tag: release.tag_name, + name: release.name, + date: release.published_at, + url: release.html_url, + highlights: parsed.highlights, + sections: parsed.sections, + } + }) + + return { ok: true, releases } +} + +export const changelog = query(async () => { + "use server" + const result = await loadChangelog() + return result.releases +}, "changelog") + +function parseHighlights(body: string): HighlightGroup[] { + const groups = new Map() + const regex = /([\s\S]*?)<\/highlight>/g + let match + + while ((match = regex.exec(body)) !== null) { + const source = match[1] + const content = match[2] + + const titleMatch = content.match(/

([^<]+)<\/h2>/) + const pMatch = content.match(/([^<]+)<\/p>/) + const imgMatch = content.match(/ { + if (videoMatch) return { type: "video", src: videoMatch[1] } satisfies HighlightMedia + if (imgMatch) { + return { + type: "image", + src: imgMatch[3], + width: imgMatch[1], + height: imgMatch[2], + } satisfies HighlightMedia + } + })() + + if (!titleMatch || !media) continue + + const item: HighlightItem = { + title: titleMatch[1], + description: pMatch?.[2] || "", + shortDescription: pMatch?.[1], + media, + } + + if (!groups.has(source)) groups.set(source, []) + groups.get(source)!.push(item) + } + + return Array.from(groups.entries()).map(([source, items]) => ({ source, items })) +} + +function parseMarkdown(body: string) { + const lines = body.split("\n") + const sections: { title: string; items: string[] }[] = [] + let current: { title: string; items: string[] } | null = null + let skip = false + + for (const line of lines) { + if (line.startsWith("## ")) { + if (current) sections.push(current) + current = { title: line.slice(3).trim(), items: [] } + skip = false + continue + } + + if (line.startsWith("**Thank you")) { + skip = true + continue + } + + if (line.startsWith("- ") && !skip) current?.items.push(line.slice(2).trim()) + } + + if (current) sections.push(current) + return { sections, highlights: parseHighlights(body) } +} diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts index f82c8421d98..f06c1be9b40 100644 --- a/packages/console/app/src/routes/changelog.json.ts +++ b/packages/console/app/src/routes/changelog.json.ts @@ -1,24 +1,4 @@ -type Release = { - tag_name: string - name: string - body: string - published_at: string - html_url: string -} - -type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } - -type HighlightItem = { - title: string - description: string - shortDescription?: string - media: HighlightMedia -} - -type HighlightGroup = { - source: string - items: HighlightItem[] -} +import { loadChangelog } from "~/lib/changelog" const cors = { "Access-Control-Allow-Origin": "*", @@ -29,147 +9,17 @@ const cors = { const ok = "public, max-age=1, s-maxage=300, stale-while-revalidate=86400, stale-if-error=86400" const error = "public, max-age=1, s-maxage=60, stale-while-revalidate=600, stale-if-error=86400" -function parseHighlights(body: string): HighlightGroup[] { - const groups = new Map() - const regex = /([\s\S]*?)<\/highlight>/g - let match - - while ((match = regex.exec(body)) !== null) { - const source = match[1] - const content = match[2] - - const titleMatch = content.match(/

([^<]+)<\/h2>/) - const pMatch = content.match(/([^<]+)<\/p>/) - const imgMatch = content.match(/ ({ source, items })) -} - -function parseMarkdown(body: string) { - const lines = body.split("\n") - const sections: { title: string; items: string[] }[] = [] - let current: { title: string; items: string[] } | null = null - let skip = false - - for (const line of lines) { - if (line.startsWith("## ")) { - if (current) sections.push(current) - const title = line.slice(3).trim() - current = { title, items: [] } - skip = false - } else if (line.startsWith("**Thank you")) { - skip = true - } else if (line.startsWith("- ") && !skip) { - current?.items.push(line.slice(2).trim()) - } - } - if (current) sections.push(current) - - const highlights = parseHighlights(body) - - return { sections, highlights } -} - export async function GET() { - const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", { + const result = await loadChangelog().catch(() => ({ ok: false, releases: [] })) + + return new Response(JSON.stringify({ releases: result.releases }), { + status: result.ok ? 200 : 503, headers: { - Accept: "application/vnd.github.v3+json", - "User-Agent": "OpenCode-Console", - }, - cf: { - // best-effort edge caching (ignored outside Cloudflare) - cacheTtl: 60 * 5, - cacheEverything: true, + "Content-Type": "application/json", + "Cache-Control": result.ok ? ok : error, + ...cors, }, - } as any).catch((err) => { - console.error("[changelog.json] fetch failed", { - error: err instanceof Error ? err.message : String(err), - }) - return undefined }) - - const fail = () => - new Response(JSON.stringify({ releases: [] }), { - status: 503, - headers: { - "Content-Type": "application/json", - "Cache-Control": error, - ...cors, - }, - }) - - if (!response) return fail() - if (!response.ok) { - const body = await response.text().catch(() => undefined) - console.error("[changelog.json] github non-ok", { - status: response.status, - remaining: response.headers.get("x-ratelimit-remaining"), - reset: response.headers.get("x-ratelimit-reset"), - body: body?.slice(0, 300), - }) - return fail() - } - - const data = await response.json().catch((err) => { - console.error("[changelog.json] json parse failed", { - error: err instanceof Error ? err.message : String(err), - }) - return undefined - }) - if (!Array.isArray(data)) { - console.error("[changelog.json] invalid json", { - type: typeof data, - }) - return fail() - } - - const releases = data as Release[] - - return new Response( - JSON.stringify({ - releases: releases.map((release) => { - const parsed = parseMarkdown(release.body || "") - return { - tag: release.tag_name, - name: release.name, - date: release.published_at, - url: release.html_url, - highlights: parsed.highlights, - sections: parsed.sections, - } - }), - }), - { - headers: { - "Content-Type": "application/json", - "Cache-Control": ok, - ...cors, - }, - }, - ) } export async function OPTIONS() { diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index ec5cd4e9d80..dff0a427f7a 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -1,117 +1,13 @@ import "./index.css" import { Title, Meta, Link } from "@solidjs/meta" -import { createAsync, useSearchParams } from "@solidjs/router" +import { createAsync } from "@solidjs/router" import { Header } from "~/component/header" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" import { config } from "~/config" -import { For, Show, createSignal, onMount } from "solid-js" -import { getRequestEvent } from "solid-js/web" - -type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } - -type HighlightItem = { - title: string - description: string - shortDescription?: string - media: HighlightMedia -} - -type HighlightGroup = { - source: string - items: HighlightItem[] -} - -type ChangelogRelease = { - tag: string - name: string - date: string - url: string - highlights: HighlightGroup[] - sections: { title: string; items: string[] }[] -} - -type LoadMeta = { - endpoint: string - ssr: boolean - hasEvent: boolean - ok: boolean - status?: number - contentType?: string - error?: string -} - -type Load = { - releases: ChangelogRelease[] - meta: LoadMeta -} - -function endpoint() { - const event = getRequestEvent() - if (event) return new URL("/changelog.json", event.request.url).toString() - if (!import.meta.env.SSR) return "/changelog.json" - return `${config.baseUrl}/changelog.json` -} - -async function getReleases(debug = false): Promise { - const url = endpoint() - const meta = { - endpoint: url, - ssr: import.meta.env.SSR, - hasEvent: Boolean(getRequestEvent()), - ok: false, - } satisfies LoadMeta - - const response = await fetch(url).catch((err) => { - console.error("[changelog] fetch failed", { - ...meta, - error: err instanceof Error ? err.message : String(err), - }) - return undefined - }) - - if (!response) return { releases: [], meta: { ...meta, error: "fetch_failed" } } - if (!response.ok) { - const contentType = response.headers.get("content-type") ?? undefined - const body = debug ? await response.text().catch(() => undefined) : undefined - console.error("[changelog] fetch non-ok", { - ...meta, - status: response.status, - contentType, - body: body?.slice(0, 300), - }) - return { releases: [], meta: { ...meta, status: response.status, contentType, error: "bad_status" } } - } - - const contentType = response.headers.get("content-type") ?? undefined - const copy = debug ? response.clone() : undefined - const json = await response.json().catch(async (err) => { - const body = copy ? await copy.text().catch(() => undefined) : undefined - console.error("[changelog] json parse failed", { - ...meta, - status: response.status, - contentType, - error: err instanceof Error ? err.message : String(err), - body: body?.slice(0, 300), - }) - return undefined - }) - - const releases = Array.isArray(json?.releases) ? (json.releases as ChangelogRelease[]) : [] - if (!releases.length) { - console.error("[changelog] empty releases", { - ...meta, - status: response.status, - contentType, - keys: json && typeof json === "object" ? Object.keys(json) : undefined, - }) - } - - return { - releases, - meta: { ...meta, ok: true, status: response.status, contentType }, - } -} +import { changelog } from "~/lib/changelog" +import type { HighlightGroup } from "~/lib/changelog" +import { For, Show, createSignal } from "solid-js" function formatDate(dateString: string) { const date = new Date(dateString) @@ -201,22 +97,8 @@ function CollapsibleSections(props: { sections: { title: string; items: string[] } export default function Changelog() { - const [params] = useSearchParams() - const debug = () => params.debug === "1" - const data = createAsync(() => getReleases(debug())) - const [client, setClient] = createSignal(undefined) - const releases = () => client()?.releases ?? data()?.releases ?? [] - - onMount(() => { - queueMicrotask(async () => { - const server = data()?.releases - if (!server) return - if (server.length) return - - const next = await getReleases(debug()) - setClient(next) - }) - }) + const data = createAsync(() => changelog()) + const releases = () => data() ?? [] return (
@@ -239,18 +121,6 @@ export default function Changelog() { No changelog entries found. View JSON

- -
-                {JSON.stringify(
-                  {
-                    server: data()?.meta,
-                    client: client()?.meta,
-                  },
-                  null,
-                  2,
-                )}
-              
-
{(release) => { return ( From 319ad2a3916862adaedcc52a8b88d8e131516b1f Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:58:46 -0600 Subject: [PATCH 078/232] fix(app): session load cap --- packages/app/src/context/global-sync.tsx | 121 ++++++++++++++++------- packages/app/src/pages/layout.tsx | 48 ++++++++- 2 files changed, 129 insertions(+), 40 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 37d4d68912b..15a92058415 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -225,6 +225,65 @@ function createGlobalSync() { const sessionLoads = new Map>() const sessionMeta = new Map() + const sessionRecentWindow = 4 * 60 * 60 * 1000 + const sessionRecentLimit = 50 + + function sessionUpdatedAt(session: Session) { + return session.time.updated ?? session.time.created + } + + function compareSessionRecent(a: Session, b: Session) { + const aUpdated = sessionUpdatedAt(a) + const bUpdated = sessionUpdatedAt(b) + if (aUpdated !== bUpdated) return bUpdated - aUpdated + return a.id.localeCompare(b.id) + } + + function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) { + if (limit <= 0) return [] as Session[] + const selected: Session[] = [] + const seen = new Set() + for (const session of sessions) { + if (!session?.id) continue + if (seen.has(session.id)) continue + seen.add(session.id) + + if (sessionUpdatedAt(session) <= cutoff) continue + + const index = selected.findIndex((x) => compareSessionRecent(session, x) < 0) + if (index === -1) selected.push(session) + if (index !== -1) selected.splice(index, 0, session) + if (selected.length > limit) selected.pop() + } + return selected + } + + function trimSessions(input: Session[], options: { limit: number; permission: Record }) { + const limit = Math.max(0, options.limit) + const cutoff = Date.now() - sessionRecentWindow + const all = input + .filter((s) => !!s?.id) + .filter((s) => !s.time?.archived) + .sort((a, b) => a.id.localeCompare(b.id)) + + const roots = all.filter((s) => !s.parentID) + const children = all.filter((s) => !!s.parentID) + + const base = roots.slice(0, limit) + const recent = takeRecentSessions(roots.slice(limit), sessionRecentLimit, cutoff) + const keepRoots = [...base, ...recent] + + const keepRootIds = new Set(keepRoots.map((s) => s.id)) + const keepChildren = children.filter((s) => { + if (s.parentID && keepRootIds.has(s.parentID)) return true + const perms = options.permission[s.id] ?? [] + if (perms.length > 0) return true + return sessionUpdatedAt(s) > cutoff + }) + + return [...keepRoots, ...keepChildren].sort((a, b) => a.id.localeCompare(b.id)) + } + function ensureChild(directory: string) { if (!directory) console.error("No directory provided") if (!children[directory]) { @@ -323,7 +382,13 @@ function createGlobalSync() { const [store, setStore] = child(directory, { bootstrap: false }) const meta = sessionMeta.get(directory) - if (meta && meta.limit >= store.limit) return + if (meta && meta.limit >= store.limit) { + const next = trimSessions(store.session, { limit: store.limit, permission: store.permission }) + if (next.length !== store.session.length) { + setStore("session", reconcile(next, { key: "id" })) + } + return + } const promise = globalSDK.client.session .list({ directory, roots: true }) @@ -337,21 +402,9 @@ function createGlobalSync() { // a request is in-flight still get the expanded result. const limit = store.limit - const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory)) - if (sandboxWorkspace) { - setStore("sessionTotal", nonArchived.length) - setStore("session", reconcile(nonArchived, { key: "id" })) - sessionMeta.set(directory, { limit }) - return - } + const children = store.session.filter((s) => !!s.parentID) + const sessions = trimSessions([...nonArchived, ...children], { limit, permission: store.permission }) - const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000 - // Include up to the limit, plus any updated in the last 4 hours - const sessions = nonArchived.filter((s, i) => { - if (i < limit) return true - const updated = new Date(s.time?.updated ?? s.time?.created).getTime() - return updated > fourHoursAgo - }) // Store total session count (used for "load more" pagination) setStore("sessionTotal", nonArchived.length) setStore("session", reconcile(sessions, { key: "id" })) @@ -536,25 +589,25 @@ function createGlobalSync() { break } case "session.created": { - const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) + const info = event.properties.info + const result = Binary.search(store.session, info.id, (s) => s.id) if (result.found) { - setStore("session", result.index, reconcile(event.properties.info)) + setStore("session", result.index, reconcile(info)) break } - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) - if (!event.properties.info.parentID) { - setStore("sessionTotal", store.sessionTotal + 1) + const next = store.session.slice() + next.splice(result.index, 0, info) + const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission }) + setStore("session", reconcile(trimmed, { key: "id" })) + if (!info.parentID) { + setStore("sessionTotal", (value) => value + 1) } break } case "session.updated": { - const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) - if (event.properties.info.time.archived) { + const info = event.properties.info + const result = Binary.search(store.session, info.id, (s) => s.id) + if (info.time.archived) { if (result.found) { setStore( "session", @@ -563,20 +616,18 @@ function createGlobalSync() { }), ) } - if (event.properties.info.parentID) break + if (info.parentID) break setStore("sessionTotal", (value) => Math.max(0, value - 1)) break } if (result.found) { - setStore("session", result.index, reconcile(event.properties.info)) + setStore("session", result.index, reconcile(info)) break } - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) + const next = store.session.slice() + next.splice(result.index, 0, info) + const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission }) + setStore("session", reconcile(trimmed, { key: "id" })) break } case "session.deleted": { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index fd6f6e527db..b13cb1ac34e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1592,6 +1592,7 @@ export default function Layout(props: ParentProps) { mobile?: boolean dense?: boolean popover?: boolean + children?: Map }): JSX.Element => { const notification = useNotification() const notifications = createMemo(() => notification.session.unseen(props.session.id)) @@ -1600,6 +1601,16 @@ export default function Layout(props: ParentProps) { const hasPermissions = createMemo(() => { const permissions = sessionStore.permission?.[props.session.id] ?? [] if (permissions.length > 0) return true + + const childIDs = props.children?.get(props.session.id) + if (childIDs) { + for (const id of childIDs) { + const childPermissions = sessionStore.permission?.[id] ?? [] + if (childPermissions.length > 0) return true + } + return false + } + const childSessions = sessionStore.session.filter((s) => s.parentID === props.session.id) for (const child of childSessions) { const childPermissions = sessionStore.permission?.[child.id] ?? [] @@ -1898,6 +1909,19 @@ export default function Layout(props: ParentProps) { .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions(Date.now())), ) + const children = createMemo(() => { + const map = new Map() + for (const session of workspaceStore.session) { + if (!session.parentID) continue + const existing = map.get(session.parentID) + if (existing) { + existing.push(session.id) + continue + } + map.set(session.parentID, [session.id]) + } + return map + }) const local = createMemo(() => props.directory === props.project.worktree) const active = createMemo(() => { const current = params.dir ? base64Decode(params.dir) : "" @@ -1911,10 +1935,9 @@ export default function Layout(props: ParentProps) { const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local()) const boot = createMemo(() => open() || active()) const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0) - const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length) + const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length) const busy = createMemo(() => isBusy(props.directory)) const loadMore = async () => { - if (!local()) return setWorkspaceStore("limit", (limit) => limit + 5) await globalSync.project.loadSessions(props.directory) } @@ -2073,7 +2096,9 @@ export default function Layout(props: ParentProps) { - {(session) => } + {(session) => ( + + )}
@@ -2288,8 +2313,21 @@ export default function Layout(props: ParentProps) { .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions(Date.now())), ) + const children = createMemo(() => { + const map = new Map() + for (const session of workspaceStore.session) { + if (!session.parentID) continue + const existing = map.get(session.parentID) + if (existing) { + existing.push(session.id) + continue + } + map.set(session.parentID, [session.id]) + } + return map + }) const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0) - const hasMore = createMemo(() => workspaceStore.sessionTotal > workspaceStore.session.length) + const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length) const loadMore = async () => { setWorkspaceStore("limit", (limit) => limit + 5) await globalSync.project.loadSessions(props.project.worktree) @@ -2308,7 +2346,7 @@ export default function Layout(props: ParentProps) { - {(session) => } + {(session) => }
From 97aec21cb3e74cabcae77fd89a2e8be67376b3a5 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:18:10 -0600 Subject: [PATCH 079/232] chore(app): missing i18n strings --- .../app/src/components/dialog-settings.tsx | 2 +- packages/app/src/components/prompt-input.tsx | 10 +- .../src/components/session/session-header.tsx | 4 +- packages/app/src/context/file.tsx | 2 +- packages/app/src/i18n/en.ts | 20 ++ packages/app/src/pages/session.tsx | 191 ++++++++++-------- packages/ui/src/components/line-comment.tsx | 21 +- packages/ui/src/components/session-review.tsx | 4 +- packages/ui/src/i18n/en.ts | 9 + 9 files changed, 161 insertions(+), 102 deletions(-) diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index 5efee5a3c69..dbae37c9d85 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -50,7 +50,7 @@ export const DialogSettings: Component = () => {
- OpenCode Desktop + {language.t("app.name.desktop")} v{platform.version}

diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 5ec0eb1ea04..84f16d67eed 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1565,7 +1565,7 @@ export const PromptInput: Component = (props) => { const timeoutMs = 5 * 60 * 1000 const timeout = new Promise>>((resolve) => { setTimeout(() => { - resolve({ status: "failed", message: "Workspace is still preparing" }) + resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") }) }, timeoutMs) }) @@ -1863,9 +1863,9 @@ export const PromptInput: Component = (props) => { store.mode === "shell" ? language.t("prompt.placeholder.shell") : commentCount() > 1 - ? "Summarize comments…" + ? language.t("prompt.placeholder.summarizeComments") : commentCount() === 1 - ? "Summarize comment…" + ? language.t("prompt.placeholder.summarizeComment") : language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) }) } contenteditable="true" @@ -1887,9 +1887,9 @@ export const PromptInput: Component = (props) => { {store.mode === "shell" ? language.t("prompt.placeholder.shell") : commentCount() > 1 - ? "Summarize comments…" + ? language.t("prompt.placeholder.summarizeComments") : commentCount() === 1 - ? "Summarize comment…" + ? language.t("prompt.placeholder.summarizeComment") : language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 8480e606078..9db0cdcecbe 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -281,7 +281,7 @@ export function SessionHeader() {
@@ -1777,7 +1777,9 @@ export default function Page() { sync.session.history.loadMore(id) }} > - {historyLoading() ? "Loading earlier messages..." : "Load earlier messages"} + {historyLoading() + ? language.t("session.messages.loadingEarlier") + : language.t("session.messages.loadEarlier")}
@@ -1911,7 +1913,7 @@ export default function Page() { when={prompt.ready()} fallback={
- {handoff.prompt || "Loading prompt..."} + {handoff.prompt || language.t("prompt.loading")}
} > @@ -2057,7 +2059,7 @@ export default function Page() {
- No changes in this session yet + {language.t("session.review.empty")}
@@ -2071,7 +2073,9 @@ export default function Page() {
-
Select a file to open
+
+ {language.t("session.files.selectToOpen")} +
@@ -2609,7 +2613,9 @@ export default function Page() {
-
No changes in this session yet
+
+ {language.t("session.review.empty")} +
@@ -2624,10 +2630,11 @@ export default function Page() { - {reviewCount()} {reviewCount() === 1 ? "Change" : "Changes"} + {reviewCount()}{" "} + {language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")} - All files + {language.t("session.files.all")} @@ -2635,7 +2642,12 @@ export default function Page() { Loading...
} + fallback={ +
+ {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
+ } > -
No changes
+
+ {language.t("session.review.noChanges")} +
@@ -2702,9 +2716,14 @@ export default function Page() { )}
-
Loading...
+
+ {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
+
+
+ {language.t("terminal.loading")}
-
Loading terminal...
} > diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx index f8869748cbb..81e4759b01f 100644 --- a/packages/ui/src/components/line-comment.tsx +++ b/packages/ui/src/components/line-comment.tsx @@ -1,6 +1,7 @@ import { onMount, Show, splitProps, type JSX } from "solid-js" import { Button } from "./button" import { Icon } from "./icon" +import { useI18n } from "../context/i18n" export type LineCommentVariant = "default" | "editor" @@ -60,13 +61,18 @@ export type LineCommentProps = Omit { + const i18n = useI18n() const [split, rest] = splitProps(props, ["comment", "selection"]) return (
{split.comment}
-
Comment on {split.selection}
+
+ {i18n.t("ui.lineComment.label.prefix")} + {split.selection} + {i18n.t("ui.lineComment.label.suffix")} +
) @@ -86,6 +92,7 @@ export type LineCommentEditorProps = Omit { + const i18n = useI18n() const [split, rest] = splitProps(props, [ "value", "selection", @@ -125,7 +132,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { }} data-slot="line-comment-textarea" rows={split.rows ?? 3} - placeholder={split.placeholder ?? "Add comment"} + placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")} value={split.value} onInput={(e) => split.onInput(e.currentTarget.value)} onKeyDown={(e) => { @@ -143,12 +150,16 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { }} />
-
Commenting on {split.selection}
+
+ {i18n.t("ui.lineComment.editorLabel.prefix")} + {split.selection} + {i18n.t("ui.lineComment.editorLabel.suffix")} +
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 9a337c4538a..8dfbbb1ca68 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -530,12 +530,12 @@ export const SessionReview = (props: SessionReviewProps) => { - Added + {i18n.t("ui.sessionReview.change.added")} - Removed + {i18n.t("ui.sessionReview.change.removed")} diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 502d047f7ff..da77b04aa52 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -4,6 +4,15 @@ export const dict = { "ui.sessionReview.diffStyle.split": "Split", "ui.sessionReview.expandAll": "Expand all", "ui.sessionReview.collapseAll": "Collapse all", + "ui.sessionReview.change.added": "Added", + "ui.sessionReview.change.removed": "Removed", + + "ui.lineComment.label.prefix": "Comment on ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "Commenting on ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "Add comment", + "ui.lineComment.submit": "Comment", "ui.sessionTurn.steps.show": "Show steps", "ui.sessionTurn.steps.hide": "Hide steps", From 04337f62024ab054a62467332c94b62ab292f3c8 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:03:29 -0600 Subject: [PATCH 080/232] chore: cleanup --- packages/app/src/i18n/ar.ts | 32 ++++++++++ packages/app/src/i18n/br.ts | 37 ++++++++++++ packages/app/src/i18n/da.ts | 111 +++++++++++++++++++++++++++++++++++ packages/app/src/i18n/de.ts | 111 +++++++++++++++++++++++++++++++++++ packages/app/src/i18n/es.ts | 111 +++++++++++++++++++++++++++++++++++ packages/app/src/i18n/fr.ts | 111 +++++++++++++++++++++++++++++++++++ packages/app/src/i18n/ja.ts | 111 +++++++++++++++++++++++++++++++++++ packages/app/src/i18n/ko.ts | 111 +++++++++++++++++++++++++++++++++++ packages/app/src/i18n/no.ts | 93 +++++++++++++++++++++++++++++ packages/app/src/i18n/pl.ts | 32 ++++++++++ packages/app/src/i18n/ru.ts | 32 ++++++++++ packages/app/src/i18n/zh.ts | 111 +++++++++++++++++++++++++++++++++++ packages/app/src/i18n/zht.ts | 110 ++++++++++++++++++++++++++++++++++ packages/ui/src/i18n/ar.ts | 8 +++ packages/ui/src/i18n/br.ts | 8 +++ packages/ui/src/i18n/da.ts | 8 +++ packages/ui/src/i18n/de.ts | 8 +++ packages/ui/src/i18n/es.ts | 8 +++ packages/ui/src/i18n/fr.ts | 8 +++ packages/ui/src/i18n/ja.ts | 8 +++ packages/ui/src/i18n/ko.ts | 8 +++ packages/ui/src/i18n/no.ts | 8 +++ packages/ui/src/i18n/pl.ts | 8 +++ packages/ui/src/i18n/ru.ts | 8 +++ packages/ui/src/i18n/zh.ts | 8 +++ packages/ui/src/i18n/zht.ts | 8 +++ 26 files changed, 1217 insertions(+) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 9fcd8ba6dbf..b442b05a4ee 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "سمة", "command.category.language": "لغة", "command.category.file": "ملف", + "command.category.context": "سياق", "command.category.terminal": "محطة طرفية", "command.category.model": "نموذج", "command.category.mcp": "MCP", @@ -42,7 +43,10 @@ export const dict = { "command.session.new": "جلسة جديدة", "command.file.open": "فتح ملف", "command.file.open.description": "البحث في الملفات والأوامر", + "command.context.addSelection": "إضافة التحديد إلى السياق", + "command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي", "command.terminal.toggle": "تبديل المحطة الطرفية", + "command.fileTree.toggle": "تبديل شجرة الملفات", "command.review.toggle": "تبديل المراجعة", "command.terminal.new": "محطة طرفية جديدة", "command.terminal.new.description": "إنشاء علامة تبويب جديدة للمحطة الطرفية", @@ -137,6 +141,8 @@ export const dict = { "provider.connect.toast.connected.title": "تم توصيل {{provider}}", "provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.", + "provider.disconnect.toast.disconnected.title": "تم فصل {{provider}}", + "provider.disconnect.toast.disconnected.description": "لم تعد نماذج {{provider}} متاحة.", "model.tag.free": "مجاني", "model.tag.latest": "الأحدث", "model.provider.anthropic": "Anthropic", @@ -159,6 +165,8 @@ export const dict = { "common.loading": "جارٍ التحميل", "common.loading.ellipsis": "...", "common.cancel": "إلغاء", + "common.connect": "اتصال", + "common.disconnect": "قطع الاتصال", "common.submit": "إرسال", "common.save": "حفظ", "common.saving": "جارٍ الحفظ...", @@ -167,6 +175,8 @@ export const dict = { "prompt.placeholder.shell": "أدخل أمر shell...", "prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"', + "prompt.placeholder.summarizeComments": "لخّص التعليقات…", + "prompt.placeholder.summarizeComment": "لخّص التعليق…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc للخروج", @@ -270,6 +280,9 @@ export const dict = { "dialog.project.edit.color": "لون", "dialog.project.edit.color.select": "اختر لون {{color}}", + "dialog.project.edit.worktree.startup": "سكريبت بدء تشغيل مساحة العمل", + "dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).", + "dialog.project.edit.worktree.startup.placeholder": "مثال: bun install", "context.breakdown.title": "تفصيل السياق", "context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال. يشمل "أخرى" تعريفات الأدوات والنفقات العامة.', "context.breakdown.system": "النظام", @@ -335,6 +348,9 @@ export const dict = { "toast.file.loadFailed.title": "فشل تحميل الملف", + "toast.file.listFailed.title": "فشل سرد الملفات", + "toast.context.noLineSelection.title": "لا يوجد تحديد للأسطر", + "toast.context.noLineSelection.description": "حدد نطاق أسطر في تبويب ملف أولاً.", "toast.session.share.copyFailed.title": "فشل نسخ عنوان URL إلى الحافظة", "toast.session.share.success.title": "تمت مشاركة الجلسة", "toast.session.share.success.description": "تم نسخ عنوان URL للمشاركة إلى الحافظة!", @@ -408,8 +424,13 @@ export const dict = { "session.tab.context": "سياق", "session.panel.reviewAndFiles": "المراجعة والملفات", "session.review.filesChanged": "تم تغيير {{count}} ملفات", + "session.review.change.one": "تغيير", + "session.review.change.other": "تغييرات", "session.review.loadingChanges": "جارٍ تحميل التغييرات...", "session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد", + "session.review.noChanges": "لا توجد تغييرات", + "session.files.selectToOpen": "اختر ملفًا لفتحه", + "session.files.all": "كل الملفات", "session.messages.renderEarlier": "عرض الرسائل السابقة", "session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...", "session.messages.loadEarlier": "تحميل الرسائل السابقة", @@ -483,7 +504,9 @@ export const dict = { "sidebar.project.recentSessions": "الجلسات الحديثة", "sidebar.project.viewAllSessions": "عرض جميع الجلسات", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "سطح المكتب", + "settings.section.server": "الخادم", "settings.tab.general": "عام", "settings.tab.shortcuts": "اختصارات", @@ -505,6 +528,7 @@ export const dict = { "font.option.hack": "Hack", "font.option.inconsolata": "Inconsolata", "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", "font.option.jetbrainsMono": "JetBrains Mono", "font.option.mesloLgs": "Meslo LGS", "font.option.robotoMono": "Roboto Mono", @@ -590,6 +614,13 @@ export const dict = { "settings.providers.title": "الموفرون", "settings.providers.description": "ستكون إعدادات الموفر قابلة للتكوين هنا.", + "settings.providers.section.connected": "الموفرون المتصلون", + "settings.providers.connected.empty": "لا يوجد موفرون متصلون", + "settings.providers.section.popular": "الموفرون الشائعون", + "settings.providers.tag.environment": "البيئة", + "settings.providers.tag.config": "التكوين", + "settings.providers.tag.custom": "مخصص", + "settings.providers.tag.other": "أخرى", "settings.models.title": "النماذج", "settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.", "settings.agents.title": "الوكلاء", @@ -657,6 +688,7 @@ export const dict = { "workspace.reset.failed.title": "فشل إعادة تعيين مساحة العمل", "workspace.reset.success.title": "تمت إعادة تعيين مساحة العمل", "workspace.reset.success.description": "مساحة العمل تطابق الآن الفرع الافتراضي.", + "workspace.error.stillPreparing": "مساحة العمل لا تزال قيد الإعداد", "workspace.status.checking": "التحقق من التغييرات غير المدمجة...", "workspace.status.error": "تعذر التحقق من حالة git.", "workspace.status.clean": "لم يتم اكتشاف تغييرات غير مدمجة.", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index a0c904dfbc2..f6d59bd0065 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "Tema", "command.category.language": "Idioma", "command.category.file": "Arquivo", + "command.category.context": "Contexto", "command.category.terminal": "Terminal", "command.category.model": "Modelo", "command.category.mcp": "MCP", @@ -42,7 +43,10 @@ export const dict = { "command.session.new": "Nova sessão", "command.file.open": "Abrir arquivo", "command.file.open.description": "Buscar arquivos e comandos", + "command.context.addSelection": "Adicionar seleção ao contexto", + "command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual", "command.terminal.toggle": "Alternar terminal", + "command.fileTree.toggle": "Alternar árvore de arquivos", "command.review.toggle": "Alternar revisão", "command.terminal.new": "Novo terminal", "command.terminal.new.description": "Criar uma nova aba de terminal", @@ -137,6 +141,8 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} conectado", "provider.connect.toast.connected.description": "Modelos do {{provider}} agora estão disponíveis para uso.", + "provider.disconnect.toast.disconnected.title": "{{provider}} desconectado", + "provider.disconnect.toast.disconnected.description": "Os modelos de {{provider}} não estão mais disponíveis.", "model.tag.free": "Grátis", "model.tag.latest": "Mais recente", "model.provider.anthropic": "Anthropic", @@ -159,6 +165,8 @@ export const dict = { "common.loading": "Carregando", "common.loading.ellipsis": "...", "common.cancel": "Cancelar", + "common.connect": "Conectar", + "common.disconnect": "Desconectar", "common.submit": "Enviar", "common.save": "Salvar", "common.saving": "Salvando...", @@ -167,6 +175,8 @@ export const dict = { "prompt.placeholder.shell": "Digite comando do shell...", "prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"', + "prompt.placeholder.summarizeComments": "Resumir comentários…", + "prompt.placeholder.summarizeComment": "Resumir comentário…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc para sair", @@ -223,6 +233,8 @@ export const dict = { "dialog.mcp.description": "{{enabled}} de {{total}} habilitados", "dialog.mcp.empty": "Nenhum MCP configurado", + "dialog.lsp.empty": "LSPs detectados automaticamente pelos tipos de arquivo", + "dialog.plugins.empty": "Plugins configurados em opencode.json", "mcp.status.connected": "conectado", "mcp.status.failed": "falhou", "mcp.status.needs_auth": "precisa de autenticação", @@ -251,6 +263,12 @@ export const dict = { "dialog.server.default.clear": "Limpar", "dialog.server.action.remove": "Remover servidor", + "dialog.server.menu.edit": "Editar", + "dialog.server.menu.default": "Definir como padrão", + "dialog.server.menu.defaultRemove": "Remover padrão", + "dialog.server.menu.delete": "Excluir", + "dialog.server.current": "Servidor atual", + "dialog.server.status.default": "Padrão", "dialog.project.edit.title": "Editar projeto", "dialog.project.edit.name": "Nome", "dialog.project.edit.icon": "Ícone", @@ -329,6 +347,9 @@ export const dict = { "toast.file.loadFailed.title": "Falha ao carregar arquivo", + "toast.file.listFailed.title": "Falha ao listar arquivos", + "toast.context.noLineSelection.title": "Nenhuma seleção de linhas", + "toast.context.noLineSelection.description": "Selecione primeiro um intervalo de linhas em uma aba de arquivo.", "toast.session.share.copyFailed.title": "Falha ao copiar URL para a área de transferência", "toast.session.share.success.title": "Sessão compartilhada", "toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!", @@ -404,8 +425,13 @@ export const dict = { "session.tab.context": "Contexto", "session.panel.reviewAndFiles": "Revisão e arquivos", "session.review.filesChanged": "{{count}} Arquivos Alterados", + "session.review.change.one": "Alteração", + "session.review.change.other": "Alterações", "session.review.loadingChanges": "Carregando alterações...", "session.review.empty": "Nenhuma alteração nesta sessão ainda", + "session.review.noChanges": "Sem alterações", + "session.files.selectToOpen": "Selecione um arquivo para abrir", + "session.files.all": "Todos os arquivos", "session.messages.renderEarlier": "Renderizar mensagens anteriores", "session.messages.loadingEarlier": "Carregando mensagens anteriores...", "session.messages.loadEarlier": "Carregar mensagens anteriores", @@ -482,7 +508,9 @@ export const dict = { "sidebar.project.recentSessions": "Sessões recentes", "sidebar.project.viewAllSessions": "Ver todas as sessões", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", + "settings.section.server": "Servidor", "settings.tab.general": "Geral", "settings.tab.shortcuts": "Atalhos", @@ -504,6 +532,7 @@ export const dict = { "font.option.hack": "Hack", "font.option.inconsolata": "Inconsolata", "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", "font.option.jetbrainsMono": "JetBrains Mono", "font.option.mesloLgs": "Meslo LGS", "font.option.robotoMono": "Roboto Mono", @@ -591,6 +620,13 @@ export const dict = { "settings.providers.title": "Provedores", "settings.providers.description": "Configurações de provedores estarão disponíveis aqui.", + "settings.providers.section.connected": "Provedores conectados", + "settings.providers.connected.empty": "Nenhum provedor conectado", + "settings.providers.section.popular": "Provedores populares", + "settings.providers.tag.environment": "Ambiente", + "settings.providers.tag.config": "Configuração", + "settings.providers.tag.custom": "Personalizado", + "settings.providers.tag.other": "Outro", "settings.models.title": "Modelos", "settings.models.description": "Configurações de modelos estarão disponíveis aqui.", "settings.agents.title": "Agentes", @@ -658,6 +694,7 @@ export const dict = { "workspace.reset.failed.title": "Falha ao redefinir espaço de trabalho", "workspace.reset.success.title": "Espaço de trabalho redefinido", "workspace.reset.success.description": "Espaço de trabalho agora corresponde ao branch padrão.", + "workspace.error.stillPreparing": "O espaço de trabalho ainda está sendo preparado", "workspace.status.checking": "Verificando alterações não mescladas...", "workspace.status.error": "Não foi possível verificar o status do git.", "workspace.status.clean": "Nenhuma alteração não mesclada detectada.", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 4dc4a2cfb2c..8f4d010c353 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "Tema", "command.category.language": "Sprog", "command.category.file": "Fil", + "command.category.context": "Kontekst", "command.category.terminal": "Terminal", "command.category.model": "Model", "command.category.mcp": "MCP", @@ -15,6 +16,7 @@ export const dict = { "command.category.permissions": "Tilladelser", "command.category.workspace": "Arbejdsområde", + "command.category.settings": "Indstillinger", "theme.scheme.system": "System", "theme.scheme.light": "Lys", "theme.scheme.dark": "Mørk", @@ -23,6 +25,7 @@ export const dict = { "command.project.open": "Åbn projekt", "command.provider.connect": "Tilslut udbyder", "command.server.switch": "Skift server", + "command.settings.open": "Åbn indstillinger", "command.session.previous": "Forrige session", "command.session.next": "Næste session", "command.session.archive": "Arkivér session", @@ -40,7 +43,10 @@ export const dict = { "command.session.new": "Ny session", "command.file.open": "Åbn fil", "command.file.open.description": "Søg i filer og kommandoer", + "command.context.addSelection": "Tilføj markering til kontekst", + "command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil", "command.terminal.toggle": "Skift terminal", + "command.fileTree.toggle": "Skift filtræ", "command.review.toggle": "Skift gennemgang", "command.terminal.new": "Ny terminal", "command.terminal.new.description": "Opret en ny terminalfane", @@ -117,6 +123,7 @@ export const dict = { "provider.connect.opencodeZen.line2": "Med en enkelt API-nøgle får du adgang til modeller som Claude, GPT, Gemini, GLM og flere.", "provider.connect.opencodeZen.visit.prefix": "Besøg ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", "provider.connect.opencodeZen.visit.suffix": " for at hente din API-nøgle.", "provider.connect.oauth.code.visit.prefix": "Besøg ", "provider.connect.oauth.code.visit.link": "dette link", @@ -134,13 +141,32 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} forbundet", "provider.connect.toast.connected.description": "{{provider}} modeller er nu tilgængelige.", + "provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet", + "provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke længere tilgængelige.", "model.tag.free": "Gratis", "model.tag.latest": "Nyeste", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "tekst", + "model.input.image": "billede", + "model.input.audio": "lyd", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Tillader: {{inputs}}", + "model.tooltip.reasoning.allowed": "Tillader tænkning", + "model.tooltip.reasoning.none": "Ingen tænkning", + "model.tooltip.context": "Kontekstgrænse {{limit}}", "common.search.placeholder": "Søg", "common.goBack": "Gå tilbage", "common.loading": "Indlæser", + "common.loading.ellipsis": "...", "common.cancel": "Annuller", + "common.connect": "Forbind", + "common.disconnect": "Frakobl", "common.submit": "Indsend", "common.save": "Gem", "common.saving": "Gemmer...", @@ -149,6 +175,8 @@ export const dict = { "prompt.placeholder.shell": "Indtast shell-kommando...", "prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"', + "prompt.placeholder.summarizeComments": "Opsummér kommentarer…", + "prompt.placeholder.summarizeComment": "Opsummér kommentar…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc for at afslutte", @@ -252,6 +280,9 @@ export const dict = { "dialog.project.edit.color": "Farve", "dialog.project.edit.color.select": "Vælg farven {{color}}", + "dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde", + "dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).", + "dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install", "context.breakdown.title": "Kontekstfordeling", "context.breakdown.note": 'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.', @@ -318,6 +349,9 @@ export const dict = { "toast.file.loadFailed.title": "Kunne ikke indlæse fil", + "toast.file.listFailed.title": "Kunne ikke liste filer", + "toast.context.noLineSelection.title": "Ingen linjevalg", + "toast.context.noLineSelection.description": "Vælg først et linjeinterval i en filfane.", "toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til udklipsholder", "toast.session.share.success.title": "Session delt", "toast.session.share.success.description": "Delings-URL kopieret til udklipsholder!", @@ -392,13 +426,19 @@ export const dict = { "session.tab.context": "Kontekst", "session.panel.reviewAndFiles": "Gennemgang og filer", "session.review.filesChanged": "{{count}} Filer ændret", + "session.review.change.one": "Ændring", + "session.review.change.other": "Ændringer", "session.review.loadingChanges": "Indlæser ændringer...", "session.review.empty": "Ingen ændringer i denne session endnu", + "session.review.noChanges": "Ingen ændringer", + "session.files.selectToOpen": "Vælg en fil at åbne", + "session.files.all": "Alle filer", "session.messages.renderEarlier": "Vis tidligere beskeder", "session.messages.loadingEarlier": "Indlæser tidligere beskeder...", "session.messages.loadEarlier": "Indlæs tidligere beskeder", "session.messages.loading": "Indlæser beskeder...", + "session.messages.jumpToLatest": "Gå til seneste", "session.context.addToContext": "Tilføj {{selection}} til kontekst", "session.new.worktree.main": "Hovedgren", @@ -440,6 +480,8 @@ export const dict = { "terminal.title.numbered": "Terminal {{number}}", "terminal.close": "Luk terminal", + "terminal.connectionLost.title": "Forbindelse mistet", + "terminal.connectionLost.description": "Terminalforbindelsen blev afbrudt. Dette kan ske, når serveren genstarter.", "common.closeTab": "Luk fane", "common.dismiss": "Afvis", "common.requestFailed": "Forespørgsel mislykkedes", @@ -453,6 +495,8 @@ export const dict = { "common.edit": "Rediger", "common.loadMore": "Indlæs flere", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "Skift menu", "sidebar.nav.projectsAndSessions": "Projekter og sessioner", "sidebar.settings": "Indstillinger", "sidebar.help": "Hjælp", @@ -464,7 +508,9 @@ export const dict = { "sidebar.project.recentSessions": "Seneste sessioner", "sidebar.project.viewAllSessions": "Vis alle sessioner", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", + "settings.section.server": "Server", "settings.tab.general": "Generelt", "settings.tab.shortcuts": "Genveje", @@ -481,6 +527,63 @@ export const dict = { "settings.general.row.font.title": "Skrifttype", "settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "Alarm 01", + "sound.option.alert02": "Alarm 02", + "sound.option.alert03": "Alarm 03", + "sound.option.alert04": "Alarm 04", + "sound.option.alert05": "Alarm 05", + "sound.option.alert06": "Alarm 06", + "sound.option.alert07": "Alarm 07", + "sound.option.alert08": "Alarm 08", + "sound.option.alert09": "Alarm 09", + "sound.option.alert10": "Alarm 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nej 01", + "sound.option.nope02": "Nej 02", + "sound.option.nope03": "Nej 03", + "sound.option.nope04": "Nej 04", + "sound.option.nope05": "Nej 05", + "sound.option.nope06": "Nej 06", + "sound.option.nope07": "Nej 07", + "sound.option.nope08": "Nej 08", + "sound.option.nope09": "Nej 09", + "sound.option.nope10": "Nej 10", + "sound.option.nope11": "Nej 11", + "sound.option.nope12": "Nej 12", + "sound.option.yup01": "Ja 01", + "sound.option.yup02": "Ja 02", + "sound.option.yup03": "Ja 03", + "sound.option.yup04": "Ja 04", + "sound.option.yup05": "Ja 05", + "sound.option.yup06": "Ja 06", "settings.general.notifications.agent.title": "Agent", "settings.general.notifications.agent.description": "Vis systemmeddelelse når agenten er færdig eller kræver opmærksomhed", @@ -516,6 +619,13 @@ export const dict = { "settings.providers.title": "Udbydere", "settings.providers.description": "Udbyderindstillinger vil kunne konfigureres her.", + "settings.providers.section.connected": "Forbundne udbydere", + "settings.providers.connected.empty": "Ingen forbundne udbydere", + "settings.providers.section.popular": "Populære udbydere", + "settings.providers.tag.environment": "Miljø", + "settings.providers.tag.config": "Konfiguration", + "settings.providers.tag.custom": "Brugerdefineret", + "settings.providers.tag.other": "Andet", "settings.models.title": "Modeller", "settings.models.description": "Modelindstillinger vil kunne konfigureres her.", "settings.agents.title": "Agenter", @@ -583,6 +693,7 @@ export const dict = { "workspace.reset.failed.title": "Kunne ikke nulstille arbejdsområde", "workspace.reset.success.title": "Arbejdsområde nulstillet", "workspace.reset.success.description": "Arbejdsområdet matcher nu hovedgrenen.", + "workspace.error.stillPreparing": "Arbejdsområdet er stadig ved at blive klargjort", "workspace.status.checking": "Tjekker for uflettede ændringer...", "workspace.status.error": "Kunne ikke bekræfte git-status.", "workspace.status.clean": "Ingen uflettede ændringer fundet.", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 69bf1fb4945..a54f19d180d 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -12,6 +12,7 @@ export const dict = { "command.category.theme": "Thema", "command.category.language": "Sprache", "command.category.file": "Datei", + "command.category.context": "Kontext", "command.category.terminal": "Terminal", "command.category.model": "Modell", "command.category.mcp": "MCP", @@ -19,6 +20,7 @@ export const dict = { "command.category.permissions": "Berechtigungen", "command.category.workspace": "Arbeitsbereich", + "command.category.settings": "Einstellungen", "theme.scheme.system": "System", "theme.scheme.light": "Hell", "theme.scheme.dark": "Dunkel", @@ -27,6 +29,7 @@ export const dict = { "command.project.open": "Projekt öffnen", "command.provider.connect": "Anbieter verbinden", "command.server.switch": "Server wechseln", + "command.settings.open": "Einstellungen öffnen", "command.session.previous": "Vorherige Sitzung", "command.session.next": "Nächste Sitzung", "command.session.archive": "Sitzung archivieren", @@ -44,7 +47,10 @@ export const dict = { "command.session.new": "Neue Sitzung", "command.file.open": "Datei öffnen", "command.file.open.description": "Dateien und Befehle durchsuchen", + "command.context.addSelection": "Auswahl zum Kontext hinzufügen", + "command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen", "command.terminal.toggle": "Terminal umschalten", + "command.fileTree.toggle": "Dateibaum umschalten", "command.review.toggle": "Überprüfung umschalten", "command.terminal.new": "Neues Terminal", "command.terminal.new.description": "Neuen Terminal-Tab erstellen", @@ -121,6 +127,7 @@ export const dict = { "provider.connect.opencodeZen.line2": "Mit einem einzigen API-Schlüssel erhalten Sie Zugriff auf Modelle wie Claude, GPT, Gemini, GLM und mehr.", "provider.connect.opencodeZen.visit.prefix": "Besuchen Sie ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", "provider.connect.opencodeZen.visit.suffix": ", um Ihren API-Schlüssel zu erhalten.", "provider.connect.oauth.code.visit.prefix": "Besuchen Sie ", "provider.connect.oauth.code.visit.link": "diesen Link", @@ -138,13 +145,32 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} verbunden", "provider.connect.toast.connected.description": "{{provider}} Modelle sind jetzt verfügbar.", + "provider.disconnect.toast.disconnected.title": "{{provider}} getrennt", + "provider.disconnect.toast.disconnected.description": "Die {{provider}}-Modelle sind nicht mehr verfügbar.", "model.tag.free": "Kostenlos", "model.tag.latest": "Neueste", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "Text", + "model.input.image": "Bild", + "model.input.audio": "Audio", + "model.input.video": "Video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Erlaubt: {{inputs}}", + "model.tooltip.reasoning.allowed": "Erlaubt Reasoning", + "model.tooltip.reasoning.none": "Kein Reasoning", + "model.tooltip.context": "Kontextlimit {{limit}}", "common.search.placeholder": "Suchen", "common.goBack": "Zurück", "common.loading": "Laden", + "common.loading.ellipsis": "...", "common.cancel": "Abbrechen", + "common.connect": "Verbinden", + "common.disconnect": "Trennen", "common.submit": "Absenden", "common.save": "Speichern", "common.saving": "Speichert...", @@ -153,6 +179,8 @@ export const dict = { "prompt.placeholder.shell": "Shell-Befehl eingeben...", "prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"', + "prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…", + "prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc zum Verlassen", @@ -257,6 +285,9 @@ export const dict = { "dialog.project.edit.color": "Farbe", "dialog.project.edit.color.select": "{{color}}-Farbe auswählen", + "dialog.project.edit.worktree.startup": "Startup-Skript für Arbeitsbereich", + "dialog.project.edit.worktree.startup.description": "Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.", + "dialog.project.edit.worktree.startup.placeholder": "z. B. bun install", "context.breakdown.title": "Kontext-Aufschlüsselung", "context.breakdown.note": 'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.', @@ -323,6 +354,9 @@ export const dict = { "toast.file.loadFailed.title": "Datei konnte nicht geladen werden", + "toast.file.listFailed.title": "Dateien konnten nicht aufgelistet werden", + "toast.context.noLineSelection.title": "Keine Zeilenauswahl", + "toast.context.noLineSelection.description": "Wählen Sie zuerst einen Zeilenbereich in einem Datei-Tab aus.", "toast.session.share.copyFailed.title": "URL konnte nicht in die Zwischenablage kopiert werden", "toast.session.share.success.title": "Sitzung geteilt", "toast.session.share.success.description": "Teilen-URL in die Zwischenablage kopiert!", @@ -399,13 +433,19 @@ export const dict = { "session.tab.context": "Kontext", "session.panel.reviewAndFiles": "Überprüfung und Dateien", "session.review.filesChanged": "{{count}} Dateien geändert", + "session.review.change.one": "Änderung", + "session.review.change.other": "Änderungen", "session.review.loadingChanges": "Lade Änderungen...", "session.review.empty": "Noch keine Änderungen in dieser Sitzung", + "session.review.noChanges": "Keine Änderungen", + "session.files.selectToOpen": "Datei zum Öffnen auswählen", + "session.files.all": "Alle Dateien", "session.messages.renderEarlier": "Frühere Nachrichten rendern", "session.messages.loadingEarlier": "Lade frühere Nachrichten...", "session.messages.loadEarlier": "Frühere Nachrichten laden", "session.messages.loading": "Lade Nachrichten...", + "session.messages.jumpToLatest": "Zum neuesten springen", "session.context.addToContext": "{{selection}} zum Kontext hinzufügen", "session.new.worktree.main": "Haupt-Branch", @@ -447,6 +487,8 @@ export const dict = { "terminal.title.numbered": "Terminal {{number}}", "terminal.close": "Terminal schließen", + "terminal.connectionLost.title": "Verbindung verloren", + "terminal.connectionLost.description": "Die Terminalverbindung wurde unterbrochen. Das kann passieren, wenn der Server neu startet.", "common.closeTab": "Tab schließen", "common.dismiss": "Verwerfen", "common.requestFailed": "Anfrage fehlgeschlagen", @@ -460,6 +502,8 @@ export const dict = { "common.edit": "Bearbeiten", "common.loadMore": "Mehr laden", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "Menü umschalten", "sidebar.nav.projectsAndSessions": "Projekte und Sitzungen", "sidebar.settings": "Einstellungen", "sidebar.help": "Hilfe", @@ -472,7 +516,9 @@ export const dict = { "sidebar.project.recentSessions": "Letzte Sitzungen", "sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", + "settings.section.server": "Server", "settings.tab.general": "Allgemein", "settings.tab.shortcuts": "Tastenkombinationen", @@ -489,6 +535,63 @@ export const dict = { "settings.general.row.font.title": "Schriftart", "settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "Alarm 01", + "sound.option.alert02": "Alarm 02", + "sound.option.alert03": "Alarm 03", + "sound.option.alert04": "Alarm 04", + "sound.option.alert05": "Alarm 05", + "sound.option.alert06": "Alarm 06", + "sound.option.alert07": "Alarm 07", + "sound.option.alert08": "Alarm 08", + "sound.option.alert09": "Alarm 09", + "sound.option.alert10": "Alarm 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nein 01", + "sound.option.nope02": "Nein 02", + "sound.option.nope03": "Nein 03", + "sound.option.nope04": "Nein 04", + "sound.option.nope05": "Nein 05", + "sound.option.nope06": "Nein 06", + "sound.option.nope07": "Nein 07", + "sound.option.nope08": "Nein 08", + "sound.option.nope09": "Nein 09", + "sound.option.nope10": "Nein 10", + "sound.option.nope11": "Nein 11", + "sound.option.nope12": "Nein 12", + "sound.option.yup01": "Ja 01", + "sound.option.yup02": "Ja 02", + "sound.option.yup03": "Ja 03", + "sound.option.yup04": "Ja 04", + "sound.option.yup05": "Ja 05", + "sound.option.yup06": "Ja 06", "settings.general.notifications.agent.title": "Agent", "settings.general.notifications.agent.description": "Systembenachrichtigung anzeigen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt", @@ -525,6 +628,13 @@ export const dict = { "settings.providers.title": "Anbieter", "settings.providers.description": "Anbietereinstellungen können hier konfiguriert werden.", + "settings.providers.section.connected": "Verbundene Anbieter", + "settings.providers.connected.empty": "Keine verbundenen Anbieter", + "settings.providers.section.popular": "Beliebte Anbieter", + "settings.providers.tag.environment": "Umgebung", + "settings.providers.tag.config": "Konfiguration", + "settings.providers.tag.custom": "Benutzerdefiniert", + "settings.providers.tag.other": "Andere", "settings.models.title": "Modelle", "settings.models.description": "Modelleinstellungen können hier konfiguriert werden.", "settings.agents.title": "Agenten", @@ -592,6 +702,7 @@ export const dict = { "workspace.reset.failed.title": "Arbeitsbereich konnte nicht zurückgesetzt werden", "workspace.reset.success.title": "Arbeitsbereich zurückgesetzt", "workspace.reset.success.description": "Der Arbeitsbereich entspricht jetzt dem Standard-Branch.", + "workspace.error.stillPreparing": "Arbeitsbereich wird noch vorbereitet", "workspace.status.checking": "Suche nach nicht zusammengeführten Änderungen...", "workspace.status.error": "Git-Status konnte nicht überprüft werden.", "workspace.status.clean": "Keine nicht zusammengeführten Änderungen erkannt.", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index c715bc048bd..06f92e07f04 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "Tema", "command.category.language": "Idioma", "command.category.file": "Archivo", + "command.category.context": "Contexto", "command.category.terminal": "Terminal", "command.category.model": "Modelo", "command.category.mcp": "MCP", @@ -15,6 +16,7 @@ export const dict = { "command.category.permissions": "Permisos", "command.category.workspace": "Espacio de trabajo", + "command.category.settings": "Ajustes", "theme.scheme.system": "Sistema", "theme.scheme.light": "Claro", "theme.scheme.dark": "Oscuro", @@ -23,6 +25,7 @@ export const dict = { "command.project.open": "Abrir proyecto", "command.provider.connect": "Conectar proveedor", "command.server.switch": "Cambiar servidor", + "command.settings.open": "Abrir ajustes", "command.session.previous": "Sesión anterior", "command.session.next": "Siguiente sesión", "command.session.archive": "Archivar sesión", @@ -40,7 +43,10 @@ export const dict = { "command.session.new": "Nueva sesión", "command.file.open": "Abrir archivo", "command.file.open.description": "Buscar archivos y comandos", + "command.context.addSelection": "Añadir selección al contexto", + "command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual", "command.terminal.toggle": "Alternar terminal", + "command.fileTree.toggle": "Alternar árbol de archivos", "command.review.toggle": "Alternar revisión", "command.terminal.new": "Nueva terminal", "command.terminal.new.description": "Crear una nueva pestaña de terminal", @@ -117,6 +123,7 @@ export const dict = { "provider.connect.opencodeZen.line2": "Con una sola clave API obtendrás acceso a modelos como Claude, GPT, Gemini, GLM y más.", "provider.connect.opencodeZen.visit.prefix": "Visita ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", "provider.connect.opencodeZen.visit.suffix": " para obtener tu clave API.", "provider.connect.oauth.code.visit.prefix": "Visita ", "provider.connect.oauth.code.visit.link": "este enlace", @@ -134,13 +141,32 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} conectado", "provider.connect.toast.connected.description": "Los modelos de {{provider}} ahora están disponibles para usar.", + "provider.disconnect.toast.disconnected.title": "{{provider}} desconectado", + "provider.disconnect.toast.disconnected.description": "Los modelos de {{provider}} ya no están disponibles.", "model.tag.free": "Gratis", "model.tag.latest": "Último", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "texto", + "model.input.image": "imagen", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Permite: {{inputs}}", + "model.tooltip.reasoning.allowed": "Permite razonamiento", + "model.tooltip.reasoning.none": "Sin razonamiento", + "model.tooltip.context": "Límite de contexto {{limit}}", "common.search.placeholder": "Buscar", "common.goBack": "Volver", "common.loading": "Cargando", + "common.loading.ellipsis": "...", "common.cancel": "Cancelar", + "common.connect": "Conectar", + "common.disconnect": "Desconectar", "common.submit": "Enviar", "common.save": "Guardar", "common.saving": "Guardando...", @@ -149,6 +175,8 @@ export const dict = { "prompt.placeholder.shell": "Introduce comando de shell...", "prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"', + "prompt.placeholder.summarizeComments": "Resumir comentarios…", + "prompt.placeholder.summarizeComment": "Resumir comentario…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc para salir", @@ -252,6 +280,9 @@ export const dict = { "dialog.project.edit.color": "Color", "dialog.project.edit.color.select": "Seleccionar color {{color}}", + "dialog.project.edit.worktree.startup": "Script de inicio del espacio de trabajo", + "dialog.project.edit.worktree.startup.description": "Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).", + "dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install", "context.breakdown.title": "Desglose de Contexto", "context.breakdown.note": 'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.', @@ -318,6 +349,9 @@ export const dict = { "toast.file.loadFailed.title": "Fallo al cargar archivo", + "toast.file.listFailed.title": "Fallo al listar archivos", + "toast.context.noLineSelection.title": "Sin selección de líneas", + "toast.context.noLineSelection.description": "Primero selecciona un rango de líneas en una pestaña de archivo.", "toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles", "toast.session.share.success.title": "Sesión compartida", "toast.session.share.success.description": "¡URL compartida copiada al portapapeles!", @@ -393,13 +427,19 @@ export const dict = { "session.tab.context": "Contexto", "session.panel.reviewAndFiles": "Revisión y archivos", "session.review.filesChanged": "{{count}} Archivos Cambiados", + "session.review.change.one": "Cambio", + "session.review.change.other": "Cambios", "session.review.loadingChanges": "Cargando cambios...", "session.review.empty": "No hay cambios en esta sesión aún", + "session.review.noChanges": "Sin cambios", + "session.files.selectToOpen": "Selecciona un archivo para abrir", + "session.files.all": "Todos los archivos", "session.messages.renderEarlier": "Renderizar mensajes anteriores", "session.messages.loadingEarlier": "Cargando mensajes anteriores...", "session.messages.loadEarlier": "Cargar mensajes anteriores", "session.messages.loading": "Cargando mensajes...", + "session.messages.jumpToLatest": "Ir al último", "session.context.addToContext": "Añadir {{selection}} al contexto", "session.new.worktree.main": "Rama principal", @@ -441,6 +481,8 @@ export const dict = { "terminal.title.numbered": "Terminal {{number}}", "terminal.close": "Cerrar terminal", + "terminal.connectionLost.title": "Conexión perdida", + "terminal.connectionLost.description": "La conexión del terminal se interrumpió. Esto puede ocurrir cuando el servidor se reinicia.", "common.closeTab": "Cerrar pestaña", "common.dismiss": "Descartar", "common.requestFailed": "Solicitud fallida", @@ -454,6 +496,8 @@ export const dict = { "common.edit": "Editar", "common.loadMore": "Cargar más", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "Alternar menú", "sidebar.nav.projectsAndSessions": "Proyectos y sesiones", "sidebar.settings": "Ajustes", "sidebar.help": "Ayuda", @@ -465,7 +509,9 @@ export const dict = { "sidebar.project.recentSessions": "Sesiones recientes", "sidebar.project.viewAllSessions": "Ver todas las sesiones", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Escritorio", + "settings.section.server": "Servidor", "settings.tab.general": "General", "settings.tab.shortcuts": "Atajos", @@ -482,6 +528,63 @@ export const dict = { "settings.general.row.font.title": "Fuente", "settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "Alerta 01", + "sound.option.alert02": "Alerta 02", + "sound.option.alert03": "Alerta 03", + "sound.option.alert04": "Alerta 04", + "sound.option.alert05": "Alerta 05", + "sound.option.alert06": "Alerta 06", + "sound.option.alert07": "Alerta 07", + "sound.option.alert08": "Alerta 08", + "sound.option.alert09": "Alerta 09", + "sound.option.alert10": "Alerta 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "No 01", + "sound.option.nope02": "No 02", + "sound.option.nope03": "No 03", + "sound.option.nope04": "No 04", + "sound.option.nope05": "No 05", + "sound.option.nope06": "No 06", + "sound.option.nope07": "No 07", + "sound.option.nope08": "No 08", + "sound.option.nope09": "No 09", + "sound.option.nope10": "No 10", + "sound.option.nope11": "No 11", + "sound.option.nope12": "No 12", + "sound.option.yup01": "Sí 01", + "sound.option.yup02": "Sí 02", + "sound.option.yup03": "Sí 03", + "sound.option.yup04": "Sí 04", + "sound.option.yup05": "Sí 05", + "sound.option.yup06": "Sí 06", "settings.general.notifications.agent.title": "Agente", "settings.general.notifications.agent.description": "Mostrar notificación del sistema cuando el agente termine o necesite atención", @@ -519,6 +622,13 @@ export const dict = { "settings.providers.title": "Proveedores", "settings.providers.description": "La configuración de proveedores estará disponible aquí.", + "settings.providers.section.connected": "Proveedores conectados", + "settings.providers.connected.empty": "No hay proveedores conectados", + "settings.providers.section.popular": "Proveedores populares", + "settings.providers.tag.environment": "Entorno", + "settings.providers.tag.config": "Configuración", + "settings.providers.tag.custom": "Personalizado", + "settings.providers.tag.other": "Otro", "settings.models.title": "Modelos", "settings.models.description": "La configuración de modelos estará disponible aquí.", "settings.agents.title": "Agentes", @@ -586,6 +696,7 @@ export const dict = { "workspace.reset.failed.title": "Fallo al restablecer espacio de trabajo", "workspace.reset.success.title": "Espacio de trabajo restablecido", "workspace.reset.success.description": "El espacio de trabajo ahora coincide con la rama predeterminada.", + "workspace.error.stillPreparing": "El espacio de trabajo aún se está preparando", "workspace.status.checking": "Comprobando cambios no fusionados...", "workspace.status.error": "No se pudo verificar el estado de git.", "workspace.status.clean": "No se detectaron cambios no fusionados.", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 8bd8dba25b8..3a3017f080b 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "Thème", "command.category.language": "Langue", "command.category.file": "Fichier", + "command.category.context": "Contexte", "command.category.terminal": "Terminal", "command.category.model": "Modèle", "command.category.mcp": "MCP", @@ -15,6 +16,7 @@ export const dict = { "command.category.permissions": "Permissions", "command.category.workspace": "Espace de travail", + "command.category.settings": "Paramètres", "theme.scheme.system": "Système", "theme.scheme.light": "Clair", "theme.scheme.dark": "Sombre", @@ -23,6 +25,7 @@ export const dict = { "command.project.open": "Ouvrir un projet", "command.provider.connect": "Connecter un fournisseur", "command.server.switch": "Changer de serveur", + "command.settings.open": "Ouvrir les paramètres", "command.session.previous": "Session précédente", "command.session.next": "Session suivante", "command.session.archive": "Archiver la session", @@ -40,7 +43,10 @@ export const dict = { "command.session.new": "Nouvelle session", "command.file.open": "Ouvrir un fichier", "command.file.open.description": "Rechercher des fichiers et des commandes", + "command.context.addSelection": "Ajouter la sélection au contexte", + "command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel", "command.terminal.toggle": "Basculer le terminal", + "command.fileTree.toggle": "Basculer l'arborescence des fichiers", "command.review.toggle": "Basculer la revue", "command.terminal.new": "Nouveau terminal", "command.terminal.new.description": "Créer un nouvel onglet de terminal", @@ -117,6 +123,7 @@ export const dict = { "provider.connect.opencodeZen.line2": "Avec une seule clé API, vous aurez accès à des modèles tels que Claude, GPT, Gemini, GLM et plus encore.", "provider.connect.opencodeZen.visit.prefix": "Visitez ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", "provider.connect.opencodeZen.visit.suffix": " pour récupérer votre clé API.", "provider.connect.oauth.code.visit.prefix": "Visitez ", "provider.connect.oauth.code.visit.link": "ce lien", @@ -134,13 +141,32 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} connecté", "provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.", + "provider.disconnect.toast.disconnected.title": "{{provider}} déconnecté", + "provider.disconnect.toast.disconnected.description": "Les modèles {{provider}} ne sont plus disponibles.", "model.tag.free": "Gratuit", "model.tag.latest": "Dernier", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "texte", + "model.input.image": "image", + "model.input.audio": "audio", + "model.input.video": "vidéo", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Autorise : {{inputs}}", + "model.tooltip.reasoning.allowed": "Autorise le raisonnement", + "model.tooltip.reasoning.none": "Sans raisonnement", + "model.tooltip.context": "Limite de contexte {{limit}}", "common.search.placeholder": "Rechercher", "common.goBack": "Retour", "common.loading": "Chargement", + "common.loading.ellipsis": "...", "common.cancel": "Annuler", + "common.connect": "Connecter", + "common.disconnect": "Déconnecter", "common.submit": "Soumettre", "common.save": "Enregistrer", "common.saving": "Enregistrement...", @@ -149,6 +175,8 @@ export const dict = { "prompt.placeholder.shell": "Entrez une commande shell...", "prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"', + "prompt.placeholder.summarizeComments": "Résumer les commentaires…", + "prompt.placeholder.summarizeComment": "Résumer le commentaire…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc pour quitter", @@ -252,6 +280,9 @@ export const dict = { "dialog.project.edit.color": "Couleur", "dialog.project.edit.color.select": "Sélectionner la couleur {{color}}", + "dialog.project.edit.worktree.startup": "Script de démarrage de l'espace de travail", + "dialog.project.edit.worktree.startup.description": "S'exécute après la création d'un nouvel espace de travail (arbre de travail).", + "dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install", "context.breakdown.title": "Répartition du contexte", "context.breakdown.note": "Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les frais généraux.", @@ -320,6 +351,9 @@ export const dict = { "toast.file.loadFailed.title": "Échec du chargement du fichier", + "toast.file.listFailed.title": "Échec de la liste des fichiers", + "toast.context.noLineSelection.title": "Aucune sélection de lignes", + "toast.context.noLineSelection.description": "Sélectionnez d'abord une plage de lignes dans un onglet de fichier.", "toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers", "toast.session.share.success.title": "Session partagée", "toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !", @@ -398,13 +432,19 @@ export const dict = { "session.tab.context": "Contexte", "session.panel.reviewAndFiles": "Revue et fichiers", "session.review.filesChanged": "{{count}} fichiers modifiés", + "session.review.change.one": "Modification", + "session.review.change.other": "Modifications", "session.review.loadingChanges": "Chargement des modifications...", "session.review.empty": "Aucune modification dans cette session pour l'instant", + "session.review.noChanges": "Aucune modification", + "session.files.selectToOpen": "Sélectionnez un fichier à ouvrir", + "session.files.all": "Tous les fichiers", "session.messages.renderEarlier": "Afficher les messages précédents", "session.messages.loadingEarlier": "Chargement des messages précédents...", "session.messages.loadEarlier": "Charger les messages précédents", "session.messages.loading": "Chargement des messages...", + "session.messages.jumpToLatest": "Aller au dernier", "session.context.addToContext": "Ajouter {{selection}} au contexte", "session.new.worktree.main": "Branche principale", @@ -446,6 +486,8 @@ export const dict = { "terminal.title.numbered": "Terminal {{number}}", "terminal.close": "Fermer le terminal", + "terminal.connectionLost.title": "Connexion perdue", + "terminal.connectionLost.description": "La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.", "common.closeTab": "Fermer l'onglet", "common.dismiss": "Ignorer", "common.requestFailed": "La demande a échoué", @@ -459,6 +501,8 @@ export const dict = { "common.edit": "Modifier", "common.loadMore": "Charger plus", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "Basculer le menu", "sidebar.nav.projectsAndSessions": "Projets et sessions", "sidebar.settings": "Paramètres", "sidebar.help": "Aide", @@ -472,7 +516,9 @@ export const dict = { "sidebar.project.recentSessions": "Sessions récentes", "sidebar.project.viewAllSessions": "Voir toutes les sessions", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Bureau", + "settings.section.server": "Serveur", "settings.tab.general": "Général", "settings.tab.shortcuts": "Raccourcis", @@ -489,6 +535,63 @@ export const dict = { "settings.general.row.font.title": "Police", "settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "Alerte 01", + "sound.option.alert02": "Alerte 02", + "sound.option.alert03": "Alerte 03", + "sound.option.alert04": "Alerte 04", + "sound.option.alert05": "Alerte 05", + "sound.option.alert06": "Alerte 06", + "sound.option.alert07": "Alerte 07", + "sound.option.alert08": "Alerte 08", + "sound.option.alert09": "Alerte 09", + "sound.option.alert10": "Alerte 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Non 01", + "sound.option.nope02": "Non 02", + "sound.option.nope03": "Non 03", + "sound.option.nope04": "Non 04", + "sound.option.nope05": "Non 05", + "sound.option.nope06": "Non 06", + "sound.option.nope07": "Non 07", + "sound.option.nope08": "Non 08", + "sound.option.nope09": "Non 09", + "sound.option.nope10": "Non 10", + "sound.option.nope11": "Non 11", + "sound.option.nope12": "Non 12", + "sound.option.yup01": "Oui 01", + "sound.option.yup02": "Oui 02", + "sound.option.yup03": "Oui 03", + "sound.option.yup04": "Oui 04", + "sound.option.yup05": "Oui 05", + "sound.option.yup06": "Oui 06", "settings.general.notifications.agent.title": "Agent", "settings.general.notifications.agent.description": "Afficher une notification système lorsque l'agent a terminé ou nécessite une attention", @@ -525,6 +628,13 @@ export const dict = { "settings.providers.title": "Fournisseurs", "settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.", + "settings.providers.section.connected": "Fournisseurs connectés", + "settings.providers.connected.empty": "Aucun fournisseur connecté", + "settings.providers.section.popular": "Fournisseurs populaires", + "settings.providers.tag.environment": "Environnement", + "settings.providers.tag.config": "Configuration", + "settings.providers.tag.custom": "Personnalisé", + "settings.providers.tag.other": "Autre", "settings.models.title": "Modèles", "settings.models.description": "Les paramètres des modèles seront configurables ici.", "settings.agents.title": "Agents", @@ -593,6 +703,7 @@ export const dict = { "workspace.reset.failed.title": "Échec de la réinitialisation de l'espace de travail", "workspace.reset.success.title": "Espace de travail réinitialisé", "workspace.reset.success.description": "L'espace de travail correspond maintenant à la branche par défaut.", + "workspace.error.stillPreparing": "L'espace de travail est encore en cours de préparation", "workspace.status.checking": "Vérification des modifications non fusionnées...", "workspace.status.error": "Impossible de vérifier le statut git.", "workspace.status.clean": "Aucune modification non fusionnée détectée.", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 5b98f5aa92c..7c629c3a01c 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "テーマ", "command.category.language": "言語", "command.category.file": "ファイル", + "command.category.context": "コンテキスト", "command.category.terminal": "ターミナル", "command.category.model": "モデル", "command.category.mcp": "MCP", @@ -15,6 +16,7 @@ export const dict = { "command.category.permissions": "権限", "command.category.workspace": "ワークスペース", + "command.category.settings": "設定", "theme.scheme.system": "システム", "theme.scheme.light": "ライト", "theme.scheme.dark": "ダーク", @@ -23,6 +25,7 @@ export const dict = { "command.project.open": "プロジェクトを開く", "command.provider.connect": "プロバイダーに接続", "command.server.switch": "サーバーの切り替え", + "command.settings.open": "設定を開く", "command.session.previous": "前のセッション", "command.session.next": "次のセッション", "command.session.archive": "セッションをアーカイブ", @@ -40,7 +43,10 @@ export const dict = { "command.session.new": "新しいセッション", "command.file.open": "ファイルを開く", "command.file.open.description": "ファイルとコマンドを検索", + "command.context.addSelection": "選択範囲をコンテキストに追加", + "command.context.addSelection.description": "現在のファイルから選択した行を追加", "command.terminal.toggle": "ターミナルの切り替え", + "command.fileTree.toggle": "ファイルツリーを切り替え", "command.review.toggle": "レビューの切り替え", "command.terminal.new": "新しいターミナル", "command.terminal.new.description": "新しいターミナルタブを作成", @@ -116,6 +122,7 @@ export const dict = { "OpenCode Zenは、コーディングエージェント向けに最適化された信頼性の高いモデルへのアクセスを提供します。", "provider.connect.opencodeZen.line2": "1つのAPIキーで、Claude、GPT、Gemini、GLMなどのモデルにアクセスできます。", "provider.connect.opencodeZen.visit.prefix": " ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", "provider.connect.opencodeZen.visit.suffix": " にアクセスしてAPIキーを取得してください。", "provider.connect.oauth.code.visit.prefix": " ", "provider.connect.oauth.code.visit.link": "このリンク", @@ -133,13 +140,32 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}}が接続されました", "provider.connect.toast.connected.description": "{{provider}}モデルが使用可能になりました。", + "provider.disconnect.toast.disconnected.title": "{{provider}}が切断されました", + "provider.disconnect.toast.disconnected.description": "{{provider}}のモデルは利用できなくなりました。", "model.tag.free": "無料", "model.tag.latest": "最新", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "テキスト", + "model.input.image": "画像", + "model.input.audio": "音声", + "model.input.video": "動画", + "model.input.pdf": "pdf", + "model.tooltip.allows": "対応: {{inputs}}", + "model.tooltip.reasoning.allowed": "推論を許可", + "model.tooltip.reasoning.none": "推論なし", + "model.tooltip.context": "コンテキスト上限 {{limit}}", "common.search.placeholder": "検索", "common.goBack": "戻る", "common.loading": "読み込み中", + "common.loading.ellipsis": "...", "common.cancel": "キャンセル", + "common.connect": "接続", + "common.disconnect": "切断", "common.submit": "送信", "common.save": "保存", "common.saving": "保存中...", @@ -148,6 +174,8 @@ export const dict = { "prompt.placeholder.shell": "シェルコマンドを入力...", "prompt.placeholder.normal": '何でも聞いてください... "{{example}}"', + "prompt.placeholder.summarizeComments": "コメントを要約…", + "prompt.placeholder.summarizeComment": "コメントを要約…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "escで終了", @@ -251,6 +279,9 @@ export const dict = { "dialog.project.edit.color": "色", "dialog.project.edit.color.select": "{{color}}の色を選択", + "dialog.project.edit.worktree.startup": "ワークスペース起動スクリプト", + "dialog.project.edit.worktree.startup.description": "新しいワークスペース (ワークツリー) を作成した後に実行されます。", + "dialog.project.edit.worktree.startup.placeholder": "例: bun install", "context.breakdown.title": "コンテキストの内訳", "context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。', "context.breakdown.system": "システム", @@ -316,6 +347,9 @@ export const dict = { "toast.file.loadFailed.title": "ファイルの読み込みに失敗しました", + "toast.file.listFailed.title": "ファイル一覧の取得に失敗しました", + "toast.context.noLineSelection.title": "行が選択されていません", + "toast.context.noLineSelection.description": "まずファイルタブで行範囲を選択してください。", "toast.session.share.copyFailed.title": "URLのコピーに失敗しました", "toast.session.share.success.title": "セッションを共有しました", "toast.session.share.success.description": "共有URLをクリップボードにコピーしました!", @@ -390,13 +424,19 @@ export const dict = { "session.tab.context": "コンテキスト", "session.panel.reviewAndFiles": "レビューとファイル", "session.review.filesChanged": "{{count}} ファイル変更", + "session.review.change.one": "変更", + "session.review.change.other": "変更", "session.review.loadingChanges": "変更を読み込み中...", "session.review.empty": "このセッションでの変更はまだありません", + "session.review.noChanges": "変更なし", + "session.files.selectToOpen": "開くファイルを選択", + "session.files.all": "すべてのファイル", "session.messages.renderEarlier": "以前のメッセージを表示", "session.messages.loadingEarlier": "以前のメッセージを読み込み中...", "session.messages.loadEarlier": "以前のメッセージを読み込む", "session.messages.loading": "メッセージを読み込み中...", + "session.messages.jumpToLatest": "最新へジャンプ", "session.context.addToContext": "{{selection}}をコンテキストに追加", "session.new.worktree.main": "メインブランチ", @@ -438,6 +478,8 @@ export const dict = { "terminal.title.numbered": "ターミナル {{number}}", "terminal.close": "ターミナルを閉じる", + "terminal.connectionLost.title": "接続が失われました", + "terminal.connectionLost.description": "ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。", "common.closeTab": "タブを閉じる", "common.dismiss": "閉じる", "common.requestFailed": "リクエスト失敗", @@ -451,6 +493,8 @@ export const dict = { "common.edit": "編集", "common.loadMore": "さらに読み込む", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "メニューを切り替え", "sidebar.nav.projectsAndSessions": "プロジェクトとセッション", "sidebar.settings": "設定", "sidebar.help": "ヘルプ", @@ -462,7 +506,9 @@ export const dict = { "sidebar.project.recentSessions": "最近のセッション", "sidebar.project.viewAllSessions": "すべてのセッションを表示", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "デスクトップ", + "settings.section.server": "サーバー", "settings.tab.general": "一般", "settings.tab.shortcuts": "ショートカット", @@ -479,6 +525,63 @@ export const dict = { "settings.general.row.font.title": "フォント", "settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "アラート 01", + "sound.option.alert02": "アラート 02", + "sound.option.alert03": "アラート 03", + "sound.option.alert04": "アラート 04", + "sound.option.alert05": "アラート 05", + "sound.option.alert06": "アラート 06", + "sound.option.alert07": "アラート 07", + "sound.option.alert08": "アラート 08", + "sound.option.alert09": "アラート 09", + "sound.option.alert10": "アラート 10", + "sound.option.bipbop01": "ビップボップ 01", + "sound.option.bipbop02": "ビップボップ 02", + "sound.option.bipbop03": "ビップボップ 03", + "sound.option.bipbop04": "ビップボップ 04", + "sound.option.bipbop05": "ビップボップ 05", + "sound.option.bipbop06": "ビップボップ 06", + "sound.option.bipbop07": "ビップボップ 07", + "sound.option.bipbop08": "ビップボップ 08", + "sound.option.bipbop09": "ビップボップ 09", + "sound.option.bipbop10": "ビップボップ 10", + "sound.option.staplebops01": "ステープルボップス 01", + "sound.option.staplebops02": "ステープルボップス 02", + "sound.option.staplebops03": "ステープルボップス 03", + "sound.option.staplebops04": "ステープルボップス 04", + "sound.option.staplebops05": "ステープルボップス 05", + "sound.option.staplebops06": "ステープルボップス 06", + "sound.option.staplebops07": "ステープルボップス 07", + "sound.option.nope01": "いいえ 01", + "sound.option.nope02": "いいえ 02", + "sound.option.nope03": "いいえ 03", + "sound.option.nope04": "いいえ 04", + "sound.option.nope05": "いいえ 05", + "sound.option.nope06": "いいえ 06", + "sound.option.nope07": "いいえ 07", + "sound.option.nope08": "いいえ 08", + "sound.option.nope09": "いいえ 09", + "sound.option.nope10": "いいえ 10", + "sound.option.nope11": "いいえ 11", + "sound.option.nope12": "いいえ 12", + "sound.option.yup01": "はい 01", + "sound.option.yup02": "はい 02", + "sound.option.yup03": "はい 03", + "sound.option.yup04": "はい 04", + "sound.option.yup05": "はい 05", + "sound.option.yup06": "はい 06", "settings.general.notifications.agent.title": "エージェント", "settings.general.notifications.agent.description": "エージェントが完了したか、注意が必要な場合にシステム通知を表示します", @@ -514,6 +617,13 @@ export const dict = { "settings.providers.title": "プロバイダー", "settings.providers.description": "プロバイダー設定はここで構成できます。", + "settings.providers.section.connected": "接続済みプロバイダー", + "settings.providers.connected.empty": "接続済みプロバイダーはありません", + "settings.providers.section.popular": "人気のプロバイダー", + "settings.providers.tag.environment": "環境", + "settings.providers.tag.config": "設定", + "settings.providers.tag.custom": "カスタム", + "settings.providers.tag.other": "その他", "settings.models.title": "モデル", "settings.models.description": "モデル設定はここで構成できます。", "settings.agents.title": "エージェント", @@ -580,6 +690,7 @@ export const dict = { "workspace.reset.failed.title": "ワークスペースのリセットに失敗しました", "workspace.reset.success.title": "ワークスペースをリセットしました", "workspace.reset.success.description": "ワークスペースはデフォルトブランチと一致しています。", + "workspace.error.stillPreparing": "ワークスペースはまだ準備中です", "workspace.status.checking": "未マージの変更を確認中...", "workspace.status.error": "gitステータスを確認できません。", "workspace.status.clean": "未マージの変更は検出されませんでした。", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index a016cd34a45..0a04d62511c 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -12,6 +12,7 @@ export const dict = { "command.category.theme": "테마", "command.category.language": "언어", "command.category.file": "파일", + "command.category.context": "컨텍스트", "command.category.terminal": "터미널", "command.category.model": "모델", "command.category.mcp": "MCP", @@ -19,6 +20,7 @@ export const dict = { "command.category.permissions": "권한", "command.category.workspace": "작업 공간", + "command.category.settings": "설정", "theme.scheme.system": "시스템", "theme.scheme.light": "라이트", "theme.scheme.dark": "다크", @@ -27,6 +29,7 @@ export const dict = { "command.project.open": "프로젝트 열기", "command.provider.connect": "공급자 연결", "command.server.switch": "서버 전환", + "command.settings.open": "설정 열기", "command.session.previous": "이전 세션", "command.session.next": "다음 세션", "command.session.archive": "세션 보관", @@ -44,7 +47,10 @@ export const dict = { "command.session.new": "새 세션", "command.file.open": "파일 열기", "command.file.open.description": "파일 및 명령어 검색", + "command.context.addSelection": "선택 영역을 컨텍스트에 추가", + "command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가", "command.terminal.toggle": "터미널 토글", + "command.fileTree.toggle": "파일 트리 토글", "command.review.toggle": "검토 토글", "command.terminal.new": "새 터미널", "command.terminal.new.description": "새 터미널 탭 생성", @@ -120,6 +126,7 @@ export const dict = { "OpenCode Zen은 코딩 에이전트를 위해 최적화된 신뢰할 수 있는 엄선된 모델에 대한 액세스를 제공합니다.", "provider.connect.opencodeZen.line2": "단일 API 키로 Claude, GPT, Gemini, GLM 등 다양한 모델에 액세스할 수 있습니다.", "provider.connect.opencodeZen.visit.prefix": "", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", "provider.connect.opencodeZen.visit.suffix": "를 방문하여 API 키를 받으세요.", "provider.connect.oauth.code.visit.prefix": "", "provider.connect.oauth.code.visit.link": "이 링크", @@ -137,13 +144,32 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} 연결됨", "provider.connect.toast.connected.description": "이제 {{provider}} 모델을 사용할 수 있습니다.", + "provider.disconnect.toast.disconnected.title": "{{provider}} 연결 해제됨", + "provider.disconnect.toast.disconnected.description": "{{provider}} 모델을 더 이상 사용할 수 없습니다.", "model.tag.free": "무료", "model.tag.latest": "최신", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "텍스트", + "model.input.image": "이미지", + "model.input.audio": "오디오", + "model.input.video": "비디오", + "model.input.pdf": "pdf", + "model.tooltip.allows": "지원: {{inputs}}", + "model.tooltip.reasoning.allowed": "추론 허용", + "model.tooltip.reasoning.none": "추론 없음", + "model.tooltip.context": "컨텍스트 제한 {{limit}}", "common.search.placeholder": "검색", "common.goBack": "뒤로 가기", "common.loading": "로딩 중", + "common.loading.ellipsis": "...", "common.cancel": "취소", + "common.connect": "연결", + "common.disconnect": "연결 해제", "common.submit": "제출", "common.save": "저장", "common.saving": "저장 중...", @@ -152,6 +178,8 @@ export const dict = { "prompt.placeholder.shell": "셸 명령어 입력...", "prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"', + "prompt.placeholder.summarizeComments": "댓글 요약…", + "prompt.placeholder.summarizeComment": "댓글 요약…", "prompt.mode.shell": "셸", "prompt.mode.shell.exit": "종료하려면 esc", @@ -255,6 +283,9 @@ export const dict = { "dialog.project.edit.color": "색상", "dialog.project.edit.color.select": "{{color}} 색상 선택", + "dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트", + "dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.", + "dialog.project.edit.worktree.startup.placeholder": "예: bun install", "context.breakdown.title": "컨텍스트 분석", "context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.', "context.breakdown.system": "시스템", @@ -320,6 +351,9 @@ export const dict = { "toast.file.loadFailed.title": "파일 로드 실패", + "toast.file.listFailed.title": "파일 목록을 불러오지 못했습니다", + "toast.context.noLineSelection.title": "줄 선택 없음", + "toast.context.noLineSelection.description": "먼저 파일 탭에서 줄 범위를 선택하세요.", "toast.session.share.copyFailed.title": "URL 클립보드 복사 실패", "toast.session.share.success.title": "세션 공유됨", "toast.session.share.success.description": "공유 URL이 클립보드에 복사되었습니다!", @@ -393,13 +427,19 @@ export const dict = { "session.tab.context": "컨텍스트", "session.panel.reviewAndFiles": "검토 및 파일", "session.review.filesChanged": "{{count}}개 파일 변경됨", + "session.review.change.one": "변경", + "session.review.change.other": "변경", "session.review.loadingChanges": "변경 사항 로드 중...", "session.review.empty": "이 세션에 변경 사항이 아직 없습니다", + "session.review.noChanges": "변경 없음", + "session.files.selectToOpen": "열 파일을 선택하세요", + "session.files.all": "모든 파일", "session.messages.renderEarlier": "이전 메시지 렌더링", "session.messages.loadingEarlier": "이전 메시지 로드 중...", "session.messages.loadEarlier": "이전 메시지 로드", "session.messages.loading": "메시지 로드 중...", + "session.messages.jumpToLatest": "최신으로 이동", "session.context.addToContext": "컨텍스트에 {{selection}} 추가", "session.new.worktree.main": "메인 브랜치", @@ -440,6 +480,8 @@ export const dict = { "terminal.title.numbered": "터미널 {{number}}", "terminal.close": "터미널 닫기", + "terminal.connectionLost.title": "연결 끊김", + "terminal.connectionLost.description": "터미널 연결이 중단되었습니다. 서버가 재시작하면 이런 일이 발생할 수 있습니다.", "common.closeTab": "탭 닫기", "common.dismiss": "닫기", "common.requestFailed": "요청 실패", @@ -453,6 +495,8 @@ export const dict = { "common.edit": "편집", "common.loadMore": "더 불러오기", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "메뉴 토글", "sidebar.nav.projectsAndSessions": "프로젝트 및 세션", "sidebar.settings": "설정", "sidebar.help": "도움말", @@ -464,7 +508,9 @@ export const dict = { "sidebar.project.recentSessions": "최근 세션", "sidebar.project.viewAllSessions": "모든 세션 보기", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "데스크톱", + "settings.section.server": "서버", "settings.tab.general": "일반", "settings.tab.shortcuts": "단축키", @@ -481,6 +527,63 @@ export const dict = { "settings.general.row.font.title": "글꼴", "settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "알림 01", + "sound.option.alert02": "알림 02", + "sound.option.alert03": "알림 03", + "sound.option.alert04": "알림 04", + "sound.option.alert05": "알림 05", + "sound.option.alert06": "알림 06", + "sound.option.alert07": "알림 07", + "sound.option.alert08": "알림 08", + "sound.option.alert09": "알림 09", + "sound.option.alert10": "알림 10", + "sound.option.bipbop01": "빕-밥 01", + "sound.option.bipbop02": "빕-밥 02", + "sound.option.bipbop03": "빕-밥 03", + "sound.option.bipbop04": "빕-밥 04", + "sound.option.bipbop05": "빕-밥 05", + "sound.option.bipbop06": "빕-밥 06", + "sound.option.bipbop07": "빕-밥 07", + "sound.option.bipbop08": "빕-밥 08", + "sound.option.bipbop09": "빕-밥 09", + "sound.option.bipbop10": "빕-밥 10", + "sound.option.staplebops01": "스테이플밥스 01", + "sound.option.staplebops02": "스테이플밥스 02", + "sound.option.staplebops03": "스테이플밥스 03", + "sound.option.staplebops04": "스테이플밥스 04", + "sound.option.staplebops05": "스테이플밥스 05", + "sound.option.staplebops06": "스테이플밥스 06", + "sound.option.staplebops07": "스테이플밥스 07", + "sound.option.nope01": "아니오 01", + "sound.option.nope02": "아니오 02", + "sound.option.nope03": "아니오 03", + "sound.option.nope04": "아니오 04", + "sound.option.nope05": "아니오 05", + "sound.option.nope06": "아니오 06", + "sound.option.nope07": "아니오 07", + "sound.option.nope08": "아니오 08", + "sound.option.nope09": "아니오 09", + "sound.option.nope10": "아니오 10", + "sound.option.nope11": "아니오 11", + "sound.option.nope12": "아니오 12", + "sound.option.yup01": "네 01", + "sound.option.yup02": "네 02", + "sound.option.yup03": "네 03", + "sound.option.yup04": "네 04", + "sound.option.yup05": "네 05", + "sound.option.yup06": "네 06", "settings.general.notifications.agent.title": "에이전트", "settings.general.notifications.agent.description": "에이전트가 완료되거나 주의가 필요할 때 시스템 알림 표시", "settings.general.notifications.permissions.title": "권한", @@ -515,6 +618,13 @@ export const dict = { "settings.providers.title": "공급자", "settings.providers.description": "공급자 설정은 여기서 구성할 수 있습니다.", + "settings.providers.section.connected": "연결된 공급자", + "settings.providers.connected.empty": "연결된 공급자 없음", + "settings.providers.section.popular": "인기 공급자", + "settings.providers.tag.environment": "환경", + "settings.providers.tag.config": "구성", + "settings.providers.tag.custom": "사용자 지정", + "settings.providers.tag.other": "기타", "settings.models.title": "모델", "settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.", "settings.agents.title": "에이전트", @@ -581,6 +691,7 @@ export const dict = { "workspace.reset.failed.title": "작업 공간 재설정 실패", "workspace.reset.success.title": "작업 공간 재설정됨", "workspace.reset.success.description": "작업 공간이 이제 기본 브랜치와 일치합니다.", + "workspace.error.stillPreparing": "작업 공간이 아직 준비 중입니다", "workspace.status.checking": "병합되지 않은 변경 사항 확인 중...", "workspace.status.error": "Git 상태를 확인할 수 없습니다.", "workspace.status.clean": "병합되지 않은 변경 사항이 감지되지 않았습니다.", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 153ee04122e..c2992d57a69 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -11,6 +11,7 @@ export const dict = { "command.category.theme": "Tema", "command.category.language": "Språk", "command.category.file": "Fil", + "command.category.context": "Kontekst", "command.category.terminal": "Terminal", "command.category.model": "Modell", "command.category.mcp": "MCP", @@ -45,7 +46,10 @@ export const dict = { "command.session.new": "Ny sesjon", "command.file.open": "Åpne fil", "command.file.open.description": "Søk i filer og kommandoer", + "command.context.addSelection": "Legg til markering i kontekst", + "command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil", "command.terminal.toggle": "Veksle terminal", + "command.fileTree.toggle": "Veksle filtre", "command.review.toggle": "Veksle gjennomgang", "command.terminal.new": "Ny terminal", "command.terminal.new.description": "Opprett en ny terminalfane", @@ -140,6 +144,8 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} tilkoblet", "provider.connect.toast.connected.description": "{{provider}}-modeller er nå tilgjengelige.", + "provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet", + "provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke lenger tilgjengelige.", "model.tag.free": "Gratis", "model.tag.latest": "Nyeste", "model.provider.anthropic": "Anthropic", @@ -162,6 +168,8 @@ export const dict = { "common.loading": "Laster", "common.loading.ellipsis": "...", "common.cancel": "Avbryt", + "common.connect": "Koble til", + "common.disconnect": "Koble fra", "common.submit": "Send inn", "common.save": "Lagre", "common.saving": "Lagrer...", @@ -170,6 +178,8 @@ export const dict = { "prompt.placeholder.shell": "Skriv inn shell-kommando...", "prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"', + "prompt.placeholder.summarizeComments": "Oppsummer kommentarer…", + "prompt.placeholder.summarizeComment": "Oppsummer kommentar…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "ESC for å avslutte", @@ -273,6 +283,9 @@ export const dict = { "dialog.project.edit.color": "Farge", "dialog.project.edit.color.select": "Velg fargen {{color}}", + "dialog.project.edit.worktree.startup": "Oppstartsskript for arbeidsområde", + "dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.", + "dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install", "context.breakdown.title": "Kontekstfordeling", "context.breakdown.note": 'Omtrentlig fordeling av input-tokens. "Annet" inkluderer verktøydefinisjoner og overhead.', "context.breakdown.system": "System", @@ -338,6 +351,9 @@ export const dict = { "toast.file.loadFailed.title": "Kunne ikke laste fil", + "toast.file.listFailed.title": "Kunne ikke liste filer", + "toast.context.noLineSelection.title": "Ingen linjevalg", + "toast.context.noLineSelection.description": "Velg først et linjeområde i en filfane.", "toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til utklippstavlen", "toast.session.share.success.title": "Sesjon delt", "toast.session.share.success.description": "Delings-URL kopiert til utklippstavlen!", @@ -412,8 +428,13 @@ export const dict = { "session.tab.context": "Kontekst", "session.panel.reviewAndFiles": "Gjennomgang og filer", "session.review.filesChanged": "{{count}} filer endret", + "session.review.change.one": "Endring", + "session.review.change.other": "Endringer", "session.review.loadingChanges": "Laster endringer...", "session.review.empty": "Ingen endringer i denne sesjonen ennå", + "session.review.noChanges": "Ingen endringer", + "session.files.selectToOpen": "Velg en fil å åpne", + "session.files.all": "Alle filer", "session.messages.renderEarlier": "Vis tidligere meldinger", "session.messages.loadingEarlier": "Laster inn tidligere meldinger...", "session.messages.loadEarlier": "Last inn tidligere meldinger", @@ -471,6 +492,7 @@ export const dict = { "common.learnMore": "Lær mer", "common.rename": "Gi nytt navn", "common.reset": "Tilbakestill", + "common.archive": "Arkiver", "common.delete": "Slett", "common.close": "Lukk", "common.edit": "Rediger", @@ -489,7 +511,9 @@ export const dict = { "sidebar.project.recentSessions": "Nylige sesjoner", "sidebar.project.viewAllSessions": "Vis alle sesjoner", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Skrivebord", + "settings.section.server": "Server", "settings.tab.general": "Generelt", "settings.tab.shortcuts": "Snarveier", @@ -506,6 +530,63 @@ export const dict = { "settings.general.row.font.title": "Skrift", "settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "Varsel 01", + "sound.option.alert02": "Varsel 02", + "sound.option.alert03": "Varsel 03", + "sound.option.alert04": "Varsel 04", + "sound.option.alert05": "Varsel 05", + "sound.option.alert06": "Varsel 06", + "sound.option.alert07": "Varsel 07", + "sound.option.alert08": "Varsel 08", + "sound.option.alert09": "Varsel 09", + "sound.option.alert10": "Varsel 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nei 01", + "sound.option.nope02": "Nei 02", + "sound.option.nope03": "Nei 03", + "sound.option.nope04": "Nei 04", + "sound.option.nope05": "Nei 05", + "sound.option.nope06": "Nei 06", + "sound.option.nope07": "Nei 07", + "sound.option.nope08": "Nei 08", + "sound.option.nope09": "Nei 09", + "sound.option.nope10": "Nei 10", + "sound.option.nope11": "Nei 11", + "sound.option.nope12": "Nei 12", + "sound.option.yup01": "Ja 01", + "sound.option.yup02": "Ja 02", + "sound.option.yup03": "Ja 03", + "sound.option.yup04": "Ja 04", + "sound.option.yup05": "Ja 05", + "sound.option.yup06": "Ja 06", "settings.general.notifications.agent.title": "Agent", "settings.general.notifications.agent.description": "Vis systemvarsel når agenten er ferdig eller trenger oppmerksomhet", @@ -541,6 +622,13 @@ export const dict = { "settings.providers.title": "Leverandører", "settings.providers.description": "Leverandørinnstillinger vil kunne konfigureres her.", + "settings.providers.section.connected": "Tilkoblede leverandører", + "settings.providers.connected.empty": "Ingen tilkoblede leverandører", + "settings.providers.section.popular": "Populære leverandører", + "settings.providers.tag.environment": "Miljø", + "settings.providers.tag.config": "Konfigurasjon", + "settings.providers.tag.custom": "Tilpasset", + "settings.providers.tag.other": "Annet", "settings.models.title": "Modeller", "settings.models.description": "Modellinnstillinger vil kunne konfigureres her.", "settings.agents.title": "Agenter", @@ -593,6 +681,10 @@ export const dict = { "settings.permissions.tool.doom_loop.title": "Doom Loop", "settings.permissions.tool.doom_loop.description": "Oppdager gjentatte verktøykall med identisk input", + "session.delete.failed.title": "Kunne ikke slette sesjon", + "session.delete.title": "Slett sesjon", + "session.delete.confirm": "Slette sesjonen \"{{name}}\"?", + "session.delete.button": "Slett sesjon", "workspace.new": "Nytt arbeidsområde", "workspace.type.local": "lokal", "workspace.type.sandbox": "sandkasse", @@ -603,6 +695,7 @@ export const dict = { "workspace.reset.failed.title": "Kunne ikke tilbakestille arbeidsområde", "workspace.reset.success.title": "Arbeidsområde tilbakestilt", "workspace.reset.success.description": "Arbeidsområdet samsvarer nå med standardgrenen.", + "workspace.error.stillPreparing": "Arbeidsområdet klargjøres fortsatt", "workspace.status.checking": "Sjekker for ikke-sammenslåtte endringer...", "workspace.status.error": "Kunne ikke bekrefte git-status.", "workspace.status.clean": "Ingen ikke-sammenslåtte endringer oppdaget.", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index db102628471..75b461487b6 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "Motyw", "command.category.language": "Język", "command.category.file": "Plik", + "command.category.context": "Kontekst", "command.category.terminal": "Terminal", "command.category.model": "Model", "command.category.mcp": "MCP", @@ -42,7 +43,10 @@ export const dict = { "command.session.new": "Nowa sesja", "command.file.open": "Otwórz plik", "command.file.open.description": "Szukaj plików i poleceń", + "command.context.addSelection": "Dodaj zaznaczenie do kontekstu", + "command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku", "command.terminal.toggle": "Przełącz terminal", + "command.fileTree.toggle": "Przełącz drzewo plików", "command.review.toggle": "Przełącz przegląd", "command.terminal.new": "Nowy terminal", "command.terminal.new.description": "Utwórz nową kartę terminala", @@ -137,6 +141,8 @@ export const dict = { "provider.connect.toast.connected.title": "Połączono {{provider}}", "provider.connect.toast.connected.description": "Modele {{provider}} są teraz dostępne do użycia.", + "provider.disconnect.toast.disconnected.title": "Rozłączono {{provider}}", + "provider.disconnect.toast.disconnected.description": "Modele {{provider}} nie są już dostępne.", "model.tag.free": "Darmowy", "model.tag.latest": "Najnowszy", "model.provider.anthropic": "Anthropic", @@ -159,6 +165,8 @@ export const dict = { "common.loading": "Ładowanie", "common.loading.ellipsis": "...", "common.cancel": "Anuluj", + "common.connect": "Połącz", + "common.disconnect": "Rozłącz", "common.submit": "Prześlij", "common.save": "Zapisz", "common.saving": "Zapisywanie...", @@ -167,6 +175,8 @@ export const dict = { "prompt.placeholder.shell": "Wpisz polecenie terminala...", "prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"', + "prompt.placeholder.summarizeComments": "Podsumuj komentarze…", + "prompt.placeholder.summarizeComment": "Podsumuj komentarz…", "prompt.mode.shell": "Terminal", "prompt.mode.shell.exit": "esc aby wyjść", @@ -270,6 +280,9 @@ export const dict = { "dialog.project.edit.color": "Kolor", "dialog.project.edit.color.select": "Wybierz kolor {{color}}", + "dialog.project.edit.worktree.startup": "Skrypt uruchamiania przestrzeni roboczej", + "dialog.project.edit.worktree.startup.description": "Uruchamiany po utworzeniu nowej przestrzeni roboczej (drzewa roboczego).", + "dialog.project.edit.worktree.startup.placeholder": "np. bun install", "context.breakdown.title": "Podział kontekstu", "context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.', "context.breakdown.system": "System", @@ -335,6 +348,9 @@ export const dict = { "toast.file.loadFailed.title": "Nie udało się załadować pliku", + "toast.file.listFailed.title": "Nie udało się wyświetlić listy plików", + "toast.context.noLineSelection.title": "Brak zaznaczenia linii", + "toast.context.noLineSelection.description": "Najpierw wybierz zakres linii w zakładce pliku.", "toast.session.share.copyFailed.title": "Nie udało się skopiować URL do schowka", "toast.session.share.success.title": "Sesja udostępniona", "toast.session.share.success.description": "Link udostępniania skopiowany do schowka!", @@ -410,8 +426,13 @@ export const dict = { "session.tab.context": "Kontekst", "session.panel.reviewAndFiles": "Przegląd i pliki", "session.review.filesChanged": "Zmieniono {{count}} plików", + "session.review.change.one": "Zmiana", + "session.review.change.other": "Zmiany", "session.review.loadingChanges": "Ładowanie zmian...", "session.review.empty": "Brak zmian w tej sesji", + "session.review.noChanges": "Brak zmian", + "session.files.selectToOpen": "Wybierz plik do otwarcia", + "session.files.all": "Wszystkie pliki", "session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości", "session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...", "session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości", @@ -488,7 +509,9 @@ export const dict = { "sidebar.project.recentSessions": "Ostatnie sesje", "sidebar.project.viewAllSessions": "Zobacz wszystkie sesje", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Pulpit", + "settings.section.server": "Serwer", "settings.tab.general": "Ogólne", "settings.tab.shortcuts": "Skróty", @@ -510,6 +533,7 @@ export const dict = { "font.option.hack": "Hack", "font.option.inconsolata": "Inconsolata", "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", "font.option.jetbrainsMono": "JetBrains Mono", "font.option.mesloLgs": "Meslo LGS", "font.option.robotoMono": "Roboto Mono", @@ -597,6 +621,13 @@ export const dict = { "settings.providers.title": "Dostawcy", "settings.providers.description": "Ustawienia dostawców będą tutaj konfigurowalne.", + "settings.providers.section.connected": "Połączeni dostawcy", + "settings.providers.connected.empty": "Brak połączonych dostawców", + "settings.providers.section.popular": "Popularni dostawcy", + "settings.providers.tag.environment": "Środowisko", + "settings.providers.tag.config": "Konfiguracja", + "settings.providers.tag.custom": "Niestandardowe", + "settings.providers.tag.other": "Inne", "settings.models.title": "Modele", "settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.", "settings.agents.title": "Agenci", @@ -663,6 +694,7 @@ export const dict = { "workspace.reset.failed.title": "Nie udało się zresetować przestrzeni roboczej", "workspace.reset.success.title": "Przestrzeń robocza zresetowana", "workspace.reset.success.description": "Przestrzeń robocza odpowiada teraz domyślnej gałęzi.", + "workspace.error.stillPreparing": "Przestrzeń robocza jest wciąż przygotowywana", "workspace.status.checking": "Sprawdzanie niezscalonych zmian...", "workspace.status.error": "Nie można zweryfikować statusu git.", "workspace.status.clean": "Nie wykryto niezscalonych zmian.", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index d8b94cb107d..766c253e3b6 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "Тема", "command.category.language": "Язык", "command.category.file": "Файл", + "command.category.context": "Контекст", "command.category.terminal": "Терминал", "command.category.model": "Модель", "command.category.mcp": "MCP", @@ -42,7 +43,10 @@ export const dict = { "command.session.new": "Новая сессия", "command.file.open": "Открыть файл", "command.file.open.description": "Поиск файлов и команд", + "command.context.addSelection": "Добавить выделение в контекст", + "command.context.addSelection.description": "Добавить выбранные строки из текущего файла", "command.terminal.toggle": "Переключить терминал", + "command.fileTree.toggle": "Переключить дерево файлов", "command.review.toggle": "Переключить обзор", "command.terminal.new": "Новый терминал", "command.terminal.new.description": "Создать новую вкладку терминала", @@ -137,6 +141,8 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} подключён", "provider.connect.toast.connected.description": "Модели {{provider}} теперь доступны.", + "provider.disconnect.toast.disconnected.title": "{{provider}} отключён", + "provider.disconnect.toast.disconnected.description": "Модели {{provider}} больше недоступны.", "model.tag.free": "Бесплатно", "model.tag.latest": "Последняя", "model.provider.anthropic": "Anthropic", @@ -159,6 +165,8 @@ export const dict = { "common.loading": "Загрузка", "common.loading.ellipsis": "...", "common.cancel": "Отмена", + "common.connect": "Подключить", + "common.disconnect": "Отключить", "common.submit": "Отправить", "common.save": "Сохранить", "common.saving": "Сохранение...", @@ -167,6 +175,8 @@ export const dict = { "prompt.placeholder.shell": "Введите команду оболочки...", "prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"', + "prompt.placeholder.summarizeComments": "Суммировать комментарии…", + "prompt.placeholder.summarizeComment": "Суммировать комментарий…", "prompt.mode.shell": "Оболочка", "prompt.mode.shell.exit": "esc для выхода", @@ -270,6 +280,9 @@ export const dict = { "dialog.project.edit.color": "Цвет", "dialog.project.edit.color.select": "Выбрать цвет {{color}}", + "dialog.project.edit.worktree.startup": "Скрипт запуска рабочего пространства", + "dialog.project.edit.worktree.startup.description": "Запускается после создания нового рабочего пространства (worktree).", + "dialog.project.edit.worktree.startup.placeholder": "например, bun install", "context.breakdown.title": "Разбивка контекста", "context.breakdown.note": 'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.', @@ -336,6 +349,9 @@ export const dict = { "toast.file.loadFailed.title": "Не удалось загрузить файл", + "toast.file.listFailed.title": "Не удалось получить список файлов", + "toast.context.noLineSelection.title": "Нет выделения строк", + "toast.context.noLineSelection.description": "Сначала выберите диапазон строк во вкладке файла.", "toast.session.share.copyFailed.title": "Не удалось скопировать URL в буфер обмена", "toast.session.share.success.title": "Сессия опубликована", "toast.session.share.success.description": "URL скопирован в буфер обмена!", @@ -412,8 +428,13 @@ export const dict = { "session.tab.context": "Контекст", "session.panel.reviewAndFiles": "Обзор и файлы", "session.review.filesChanged": "{{count}} файлов изменено", + "session.review.change.one": "Изменение", + "session.review.change.other": "Изменения", "session.review.loadingChanges": "Загрузка изменений...", "session.review.empty": "Изменений в этой сессии пока нет", + "session.review.noChanges": "Нет изменений", + "session.files.selectToOpen": "Выберите файл, чтобы открыть", + "session.files.all": "Все файлы", "session.messages.renderEarlier": "Показать предыдущие сообщения", "session.messages.loadingEarlier": "Загрузка предыдущих сообщений...", "session.messages.loadEarlier": "Загрузить предыдущие сообщения", @@ -491,7 +512,9 @@ export const dict = { "sidebar.project.recentSessions": "Недавние сессии", "sidebar.project.viewAllSessions": "Посмотреть все сессии", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Приложение", + "settings.section.server": "Сервер", "settings.tab.general": "Основные", "settings.tab.shortcuts": "Горячие клавиши", @@ -513,6 +536,7 @@ export const dict = { "font.option.hack": "Hack", "font.option.inconsolata": "Inconsolata", "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", "font.option.jetbrainsMono": "JetBrains Mono", "font.option.mesloLgs": "Meslo LGS", "font.option.robotoMono": "Roboto Mono", @@ -600,6 +624,13 @@ export const dict = { "settings.providers.title": "Провайдеры", "settings.providers.description": "Настройки провайдеров будут доступны здесь.", + "settings.providers.section.connected": "Подключённые провайдеры", + "settings.providers.connected.empty": "Нет подключённых провайдеров", + "settings.providers.section.popular": "Популярные провайдеры", + "settings.providers.tag.environment": "Среда", + "settings.providers.tag.config": "Конфигурация", + "settings.providers.tag.custom": "Пользовательский", + "settings.providers.tag.other": "Другое", "settings.models.title": "Модели", "settings.models.description": "Настройки моделей будут доступны здесь.", "settings.agents.title": "Агенты", @@ -667,6 +698,7 @@ export const dict = { "workspace.reset.failed.title": "Не удалось сбросить рабочее пространство", "workspace.reset.success.title": "Рабочее пространство сброшено", "workspace.reset.success.description": "Рабочее пространство теперь соответствует ветке по умолчанию.", + "workspace.error.stillPreparing": "Рабочее пространство всё ещё готовится", "workspace.status.checking": "Проверка наличия неслитых изменений...", "workspace.status.error": "Не удалось проверить статус git.", "workspace.status.clean": "Неслитые изменения не обнаружены.", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index cfecb739d88..863df08df93 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -12,6 +12,7 @@ export const dict = { "command.category.theme": "主题", "command.category.language": "语言", "command.category.file": "文件", + "command.category.context": "上下文", "command.category.terminal": "终端", "command.category.model": "模型", "command.category.mcp": "MCP", @@ -19,6 +20,7 @@ export const dict = { "command.category.permissions": "权限", "command.category.workspace": "工作区", + "command.category.settings": "设置", "theme.scheme.system": "系统", "theme.scheme.light": "浅色", "theme.scheme.dark": "深色", @@ -27,6 +29,7 @@ export const dict = { "command.project.open": "打开项目", "command.provider.connect": "连接提供商", "command.server.switch": "切换服务器", + "command.settings.open": "打开设置", "command.session.previous": "上一个会话", "command.session.next": "下一个会话", "command.session.archive": "归档会话", @@ -44,7 +47,10 @@ export const dict = { "command.session.new": "新建会话", "command.file.open": "打开文件", "command.file.open.description": "搜索文件和命令", + "command.context.addSelection": "将所选内容添加到上下文", + "command.context.addSelection.description": "添加当前文件中选中的行", "command.terminal.toggle": "切换终端", + "command.fileTree.toggle": "切换文件树", "command.review.toggle": "切换审查", "command.terminal.new": "新建终端", "command.terminal.new.description": "创建新的终端标签页", @@ -119,6 +125,7 @@ export const dict = { "provider.connect.opencodeZen.line1": "OpenCode Zen 为你提供一组精选的可靠优化模型,用于代码智能体。", "provider.connect.opencodeZen.line2": "只需一个 API 密钥,你就能使用 Claude、GPT、Gemini、GLM 等模型。", "provider.connect.opencodeZen.visit.prefix": "访问 ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", "provider.connect.opencodeZen.visit.suffix": " 获取你的 API 密钥。", "provider.connect.oauth.code.visit.prefix": "访问 ", "provider.connect.oauth.code.visit.link": "此链接", @@ -134,13 +141,32 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} 已连接", "provider.connect.toast.connected.description": "现在可以使用 {{provider}} 模型了。", + "provider.disconnect.toast.disconnected.title": "{{provider}} 已断开连接", + "provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。", "model.tag.free": "免费", "model.tag.latest": "最新", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "文本", + "model.input.image": "图像", + "model.input.audio": "音频", + "model.input.video": "视频", + "model.input.pdf": "pdf", + "model.tooltip.allows": "支持: {{inputs}}", + "model.tooltip.reasoning.allowed": "支持推理", + "model.tooltip.reasoning.none": "不支持推理", + "model.tooltip.context": "上下文上限 {{limit}}", "common.search.placeholder": "搜索", "common.goBack": "返回", "common.loading": "加载中", + "common.loading.ellipsis": "...", "common.cancel": "取消", + "common.connect": "连接", + "common.disconnect": "断开连接", "common.submit": "提交", "common.save": "保存", "common.saving": "保存中...", @@ -149,6 +175,8 @@ export const dict = { "prompt.placeholder.shell": "输入 shell 命令...", "prompt.placeholder.normal": '随便问点什么... "{{example}}"', + "prompt.placeholder.summarizeComments": "总结评论…", + "prompt.placeholder.summarizeComment": "总结该评论…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "按 esc 退出", @@ -251,6 +279,9 @@ export const dict = { "dialog.project.edit.color": "颜色", "dialog.project.edit.color.select": "选择{{color}}颜色", + "dialog.project.edit.worktree.startup": "工作区启动脚本", + "dialog.project.edit.worktree.startup.description": "在创建新的工作区 (worktree) 后运行。", + "dialog.project.edit.worktree.startup.placeholder": "例如 bun install", "context.breakdown.title": "上下文拆分", "context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。", "context.breakdown.system": "系统", @@ -316,6 +347,9 @@ export const dict = { "toast.file.loadFailed.title": "加载文件失败", + "toast.file.listFailed.title": "列出文件失败", + "toast.context.noLineSelection.title": "未选择行", + "toast.context.noLineSelection.description": "请先在文件标签中选择行范围。", "toast.session.share.copyFailed.title": "无法复制链接到剪贴板", "toast.session.share.success.title": "会话已分享", "toast.session.share.success.description": "分享链接已复制到剪贴板", @@ -388,13 +422,19 @@ export const dict = { "session.tab.context": "上下文", "session.panel.reviewAndFiles": "审查和文件", "session.review.filesChanged": "{{count}} 个文件变更", + "session.review.change.one": "更改", + "session.review.change.other": "更改", "session.review.loadingChanges": "正在加载更改...", "session.review.empty": "此会话暂无更改", + "session.review.noChanges": "无更改", + "session.files.selectToOpen": "选择要打开的文件", + "session.files.all": "所有文件", "session.messages.renderEarlier": "显示更早的消息", "session.messages.loadingEarlier": "正在加载更早的消息...", "session.messages.loadEarlier": "加载更早的消息", "session.messages.loading": "正在加载消息...", + "session.messages.jumpToLatest": "跳转到最新", "session.context.addToContext": "将 {{selection}} 添加到上下文", "session.new.worktree.main": "主分支", @@ -434,6 +474,8 @@ export const dict = { "terminal.title.numbered": "终端 {{number}}", "terminal.close": "关闭终端", + "terminal.connectionLost.title": "连接已丢失", + "terminal.connectionLost.description": "终端连接已中断。这可能发生在服务器重启时。", "common.closeTab": "关闭标签页", "common.dismiss": "忽略", "common.requestFailed": "请求失败", @@ -447,6 +489,8 @@ export const dict = { "common.edit": "编辑", "common.loadMore": "加载更多", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "切换菜单", "sidebar.nav.projectsAndSessions": "项目和会话", "sidebar.settings": "设置", "sidebar.help": "帮助", @@ -458,7 +502,9 @@ export const dict = { "sidebar.project.recentSessions": "最近会话", "sidebar.project.viewAllSessions": "查看全部会话", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "桌面", + "settings.section.server": "服务器", "settings.tab.general": "通用", "settings.tab.shortcuts": "快捷键", @@ -475,6 +521,63 @@ export const dict = { "settings.general.row.font.title": "字体", "settings.general.row.font.description": "自定义代码块使用的等宽字体", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "警报 01", + "sound.option.alert02": "警报 02", + "sound.option.alert03": "警报 03", + "sound.option.alert04": "警报 04", + "sound.option.alert05": "警报 05", + "sound.option.alert06": "警报 06", + "sound.option.alert07": "警报 07", + "sound.option.alert08": "警报 08", + "sound.option.alert09": "警报 09", + "sound.option.alert10": "警报 10", + "sound.option.bipbop01": "哔啵 01", + "sound.option.bipbop02": "哔啵 02", + "sound.option.bipbop03": "哔啵 03", + "sound.option.bipbop04": "哔啵 04", + "sound.option.bipbop05": "哔啵 05", + "sound.option.bipbop06": "哔啵 06", + "sound.option.bipbop07": "哔啵 07", + "sound.option.bipbop08": "哔啵 08", + "sound.option.bipbop09": "哔啵 09", + "sound.option.bipbop10": "哔啵 10", + "sound.option.staplebops01": "斯泰普博普斯 01", + "sound.option.staplebops02": "斯泰普博普斯 02", + "sound.option.staplebops03": "斯泰普博普斯 03", + "sound.option.staplebops04": "斯泰普博普斯 04", + "sound.option.staplebops05": "斯泰普博普斯 05", + "sound.option.staplebops06": "斯泰普博普斯 06", + "sound.option.staplebops07": "斯泰普博普斯 07", + "sound.option.nope01": "否 01", + "sound.option.nope02": "否 02", + "sound.option.nope03": "否 03", + "sound.option.nope04": "否 04", + "sound.option.nope05": "否 05", + "sound.option.nope06": "否 06", + "sound.option.nope07": "否 07", + "sound.option.nope08": "否 08", + "sound.option.nope09": "否 09", + "sound.option.nope10": "否 10", + "sound.option.nope11": "否 11", + "sound.option.nope12": "否 12", + "sound.option.yup01": "是 01", + "sound.option.yup02": "是 02", + "sound.option.yup03": "是 03", + "sound.option.yup04": "是 04", + "sound.option.yup05": "是 05", + "sound.option.yup06": "是 06", "settings.general.notifications.agent.title": "智能体", "settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知", "settings.general.notifications.permissions.title": "权限", @@ -509,6 +612,13 @@ export const dict = { "settings.providers.title": "提供商", "settings.providers.description": "提供商设置将在此处可配置。", + "settings.providers.section.connected": "已连接的提供商", + "settings.providers.connected.empty": "没有已连接的提供商", + "settings.providers.section.popular": "热门提供商", + "settings.providers.tag.environment": "环境", + "settings.providers.tag.config": "配置", + "settings.providers.tag.custom": "自定义", + "settings.providers.tag.other": "其他", "settings.models.title": "模型", "settings.models.description": "模型设置将在此处可配置。", "settings.agents.title": "智能体", @@ -575,6 +685,7 @@ export const dict = { "workspace.reset.failed.title": "重置工作区失败", "workspace.reset.success.title": "工作区已重置", "workspace.reset.success.description": "工作区已与默认分支保持一致。", + "workspace.error.stillPreparing": "工作区仍在准备中", "workspace.status.checking": "正在检查未合并的更改...", "workspace.status.error": "无法验证 git 状态。", "workspace.status.clean": "未检测到未合并的更改。", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 050c160cdfb..98e74e805b1 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -12,6 +12,7 @@ export const dict = { "command.category.theme": "主題", "command.category.language": "語言", "command.category.file": "檔案", + "command.category.context": "上下文", "command.category.terminal": "終端機", "command.category.model": "模型", "command.category.mcp": "MCP", @@ -19,6 +20,7 @@ export const dict = { "command.category.permissions": "權限", "command.category.workspace": "工作區", + "command.category.settings": "設定", "theme.scheme.system": "系統", "theme.scheme.light": "淺色", "theme.scheme.dark": "深色", @@ -27,6 +29,7 @@ export const dict = { "command.project.open": "開啟專案", "command.provider.connect": "連接提供者", "command.server.switch": "切換伺服器", + "command.settings.open": "開啟設定", "command.session.previous": "上一個工作階段", "command.session.next": "下一個工作階段", "command.session.archive": "封存工作階段", @@ -44,7 +47,10 @@ export const dict = { "command.session.new": "新增工作階段", "command.file.open": "開啟檔案", "command.file.open.description": "搜尋檔案和命令", + "command.context.addSelection": "將選取內容加入上下文", + "command.context.addSelection.description": "加入目前檔案中選取的行", "command.terminal.toggle": "切換終端機", + "command.fileTree.toggle": "切換檔案樹", "command.review.toggle": "切換審查", "command.terminal.new": "新增終端機", "command.terminal.new.description": "建立新的終端機標籤頁", @@ -136,13 +142,32 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} 已連線", "provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。", + "provider.disconnect.toast.disconnected.title": "{{provider}} 已中斷連線", + "provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。", "model.tag.free": "免費", "model.tag.latest": "最新", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "文字", + "model.input.image": "圖片", + "model.input.audio": "音訊", + "model.input.video": "影片", + "model.input.pdf": "pdf", + "model.tooltip.allows": "支援: {{inputs}}", + "model.tooltip.reasoning.allowed": "支援推理", + "model.tooltip.reasoning.none": "不支援推理", + "model.tooltip.context": "上下文上限 {{limit}}", "common.search.placeholder": "搜尋", "common.goBack": "返回", "common.loading": "載入中", + "common.loading.ellipsis": "...", "common.cancel": "取消", + "common.connect": "連線", + "common.disconnect": "中斷連線", "common.submit": "提交", "common.save": "儲存", "common.saving": "儲存中...", @@ -151,6 +176,8 @@ export const dict = { "prompt.placeholder.shell": "輸入 shell 命令...", "prompt.placeholder.normal": '隨便問點什麼... "{{example}}"', + "prompt.placeholder.summarizeComments": "摘要評論…", + "prompt.placeholder.summarizeComment": "摘要這則評論…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "按 esc 退出", @@ -253,6 +280,9 @@ export const dict = { "dialog.project.edit.color": "顏色", "dialog.project.edit.color.select": "選擇{{color}}顏色", + "dialog.project.edit.worktree.startup": "工作區啟動腳本", + "dialog.project.edit.worktree.startup.description": "在建立新的工作區 (worktree) 後執行。", + "dialog.project.edit.worktree.startup.placeholder": "例如 bun install", "context.breakdown.title": "上下文拆分", "context.breakdown.note": "輸入 token 的大致拆分。「其他」包含工具定義和額外開銷。", "context.breakdown.system": "系統", @@ -318,6 +348,9 @@ export const dict = { "toast.file.loadFailed.title": "載入檔案失敗", + "toast.file.listFailed.title": "列出檔案失敗", + "toast.context.noLineSelection.title": "未選取行", + "toast.context.noLineSelection.description": "請先在檔案分頁中選取行範圍。", "toast.session.share.copyFailed.title": "無法複製連結到剪貼簿", "toast.session.share.success.title": "工作階段已分享", "toast.session.share.success.description": "分享連結已複製到剪貼簿", @@ -390,13 +423,19 @@ export const dict = { "session.tab.context": "上下文", "session.panel.reviewAndFiles": "審查與檔案", "session.review.filesChanged": "{{count}} 個檔案變更", + "session.review.change.one": "變更", + "session.review.change.other": "變更", "session.review.loadingChanges": "正在載入變更...", "session.review.empty": "此工作階段暫無變更", + "session.review.noChanges": "沒有變更", + "session.files.selectToOpen": "選取要開啟的檔案", + "session.files.all": "所有檔案", "session.messages.renderEarlier": "顯示更早的訊息", "session.messages.loadingEarlier": "正在載入更早的訊息...", "session.messages.loadEarlier": "載入更早的訊息", "session.messages.loading": "正在載入訊息...", + "session.messages.jumpToLatest": "跳到最新", "session.context.addToContext": "將 {{selection}} 新增到上下文", "session.new.worktree.main": "主分支", @@ -436,6 +475,8 @@ export const dict = { "terminal.title.numbered": "終端機 {{number}}", "terminal.close": "關閉終端機", + "terminal.connectionLost.title": "連線中斷", + "terminal.connectionLost.description": "終端機連線已中斷。這可能會在伺服器重新啟動時發生。", "common.closeTab": "關閉標籤頁", "common.dismiss": "忽略", "common.requestFailed": "要求失敗", @@ -449,6 +490,8 @@ export const dict = { "common.edit": "編輯", "common.loadMore": "載入更多", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "切換選單", "sidebar.nav.projectsAndSessions": "專案與工作階段", "sidebar.settings": "設定", "sidebar.help": "說明", @@ -460,7 +503,9 @@ export const dict = { "sidebar.project.recentSessions": "最近工作階段", "sidebar.project.viewAllSessions": "查看全部工作階段", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "桌面", + "settings.section.server": "伺服器", "settings.tab.general": "一般", "settings.tab.shortcuts": "快速鍵", @@ -477,6 +522,63 @@ export const dict = { "settings.general.row.font.title": "字型", "settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.iosevka": "Iosevka", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "警報 01", + "sound.option.alert02": "警報 02", + "sound.option.alert03": "警報 03", + "sound.option.alert04": "警報 04", + "sound.option.alert05": "警報 05", + "sound.option.alert06": "警報 06", + "sound.option.alert07": "警報 07", + "sound.option.alert08": "警報 08", + "sound.option.alert09": "警報 09", + "sound.option.alert10": "警報 10", + "sound.option.bipbop01": "嗶啵 01", + "sound.option.bipbop02": "嗶啵 02", + "sound.option.bipbop03": "嗶啵 03", + "sound.option.bipbop04": "嗶啵 04", + "sound.option.bipbop05": "嗶啵 05", + "sound.option.bipbop06": "嗶啵 06", + "sound.option.bipbop07": "嗶啵 07", + "sound.option.bipbop08": "嗶啵 08", + "sound.option.bipbop09": "嗶啵 09", + "sound.option.bipbop10": "嗶啵 10", + "sound.option.staplebops01": "斯泰普博普斯 01", + "sound.option.staplebops02": "斯泰普博普斯 02", + "sound.option.staplebops03": "斯泰普博普斯 03", + "sound.option.staplebops04": "斯泰普博普斯 04", + "sound.option.staplebops05": "斯泰普博普斯 05", + "sound.option.staplebops06": "斯泰普博普斯 06", + "sound.option.staplebops07": "斯泰普博普斯 07", + "sound.option.nope01": "否 01", + "sound.option.nope02": "否 02", + "sound.option.nope03": "否 03", + "sound.option.nope04": "否 04", + "sound.option.nope05": "否 05", + "sound.option.nope06": "否 06", + "sound.option.nope07": "否 07", + "sound.option.nope08": "否 08", + "sound.option.nope09": "否 09", + "sound.option.nope10": "否 10", + "sound.option.nope11": "否 11", + "sound.option.nope12": "否 12", + "sound.option.yup01": "是 01", + "sound.option.yup02": "是 02", + "sound.option.yup03": "是 03", + "sound.option.yup04": "是 04", + "sound.option.yup05": "是 05", + "sound.option.yup06": "是 06", "settings.general.notifications.agent.title": "代理程式", "settings.general.notifications.agent.description": "當代理程式完成或需要注意時顯示系統通知", "settings.general.notifications.permissions.title": "權限", @@ -511,6 +613,13 @@ export const dict = { "settings.providers.title": "提供者", "settings.providers.description": "提供者設定將在此處可設定。", + "settings.providers.section.connected": "已連線的提供商", + "settings.providers.connected.empty": "沒有已連線的提供商", + "settings.providers.section.popular": "熱門提供商", + "settings.providers.tag.environment": "環境", + "settings.providers.tag.config": "配置", + "settings.providers.tag.custom": "自訂", + "settings.providers.tag.other": "其他", "settings.models.title": "模型", "settings.models.description": "模型設定將在此處可設定。", "settings.agents.title": "代理程式", @@ -577,6 +686,7 @@ export const dict = { "workspace.reset.failed.title": "重設工作區失敗", "workspace.reset.success.title": "工作區已重設", "workspace.reset.success.description": "工作區已與預設分支保持一致。", + "workspace.error.stillPreparing": "工作區仍在準備中", "workspace.status.checking": "正在檢查未合併的變更...", "workspace.status.error": "無法驗證 git 狀態。", "workspace.status.clean": "未偵測到未合併的變更。", diff --git a/packages/ui/src/i18n/ar.ts b/packages/ui/src/i18n/ar.ts index 27bd3a75dac..a4179a41449 100644 --- a/packages/ui/src/i18n/ar.ts +++ b/packages/ui/src/i18n/ar.ts @@ -5,6 +5,14 @@ export const dict = { "ui.sessionReview.expandAll": "توسيع الكل", "ui.sessionReview.collapseAll": "طي الكل", + "ui.sessionReview.change.added": "مضاف", + "ui.sessionReview.change.removed": "محذوف", + "ui.lineComment.label.prefix": "تعليق على ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "جارٍ التعليق على ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "أضف تعليقًا", + "ui.lineComment.submit": "تعليق", "ui.sessionTurn.steps.show": "إظهار الخطوات", "ui.sessionTurn.steps.hide": "إخفاء الخطوات", "ui.sessionTurn.summary.response": "استجابة", diff --git a/packages/ui/src/i18n/br.ts b/packages/ui/src/i18n/br.ts index d8e987a06b8..b8e6fdbe539 100644 --- a/packages/ui/src/i18n/br.ts +++ b/packages/ui/src/i18n/br.ts @@ -5,6 +5,14 @@ export const dict = { "ui.sessionReview.expandAll": "Expandir tudo", "ui.sessionReview.collapseAll": "Recolher tudo", + "ui.sessionReview.change.added": "Adicionado", + "ui.sessionReview.change.removed": "Removido", + "ui.lineComment.label.prefix": "Comentar em ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "Comentando em ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "Adicionar comentário", + "ui.lineComment.submit": "Comentar", "ui.sessionTurn.steps.show": "Mostrar passos", "ui.sessionTurn.steps.hide": "Ocultar passos", "ui.sessionTurn.summary.response": "Resposta", diff --git a/packages/ui/src/i18n/da.ts b/packages/ui/src/i18n/da.ts index dc9dab5a8f8..6e8f6b46e2c 100644 --- a/packages/ui/src/i18n/da.ts +++ b/packages/ui/src/i18n/da.ts @@ -5,6 +5,14 @@ export const dict = { "ui.sessionReview.expandAll": "Udvid alle", "ui.sessionReview.collapseAll": "Skjul alle", + "ui.sessionReview.change.added": "Tilføjet", + "ui.sessionReview.change.removed": "Fjernet", + "ui.lineComment.label.prefix": "Kommenter på ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "Kommenterer på ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "Tilføj kommentar", + "ui.lineComment.submit": "Kommenter", "ui.sessionTurn.steps.show": "Vis trin", "ui.sessionTurn.steps.hide": "Skjul trin", "ui.sessionTurn.summary.response": "Svar", diff --git a/packages/ui/src/i18n/de.ts b/packages/ui/src/i18n/de.ts index 05c024fc26a..6d208602327 100644 --- a/packages/ui/src/i18n/de.ts +++ b/packages/ui/src/i18n/de.ts @@ -9,6 +9,14 @@ export const dict = { "ui.sessionReview.expandAll": "Alle erweitern", "ui.sessionReview.collapseAll": "Alle reduzieren", + "ui.sessionReview.change.added": "Hinzugefügt", + "ui.sessionReview.change.removed": "Entfernt", + "ui.lineComment.label.prefix": "Kommentar zu ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "Kommentiere ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "Kommentar hinzufügen", + "ui.lineComment.submit": "Kommentieren", "ui.sessionTurn.steps.show": "Schritte anzeigen", "ui.sessionTurn.steps.hide": "Schritte ausblenden", "ui.sessionTurn.summary.response": "Antwort", diff --git a/packages/ui/src/i18n/es.ts b/packages/ui/src/i18n/es.ts index f8361596161..19df81a6b9e 100644 --- a/packages/ui/src/i18n/es.ts +++ b/packages/ui/src/i18n/es.ts @@ -5,6 +5,14 @@ export const dict = { "ui.sessionReview.expandAll": "Expandir todo", "ui.sessionReview.collapseAll": "Colapsar todo", + "ui.sessionReview.change.added": "Añadido", + "ui.sessionReview.change.removed": "Eliminado", + "ui.lineComment.label.prefix": "Comentar en ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "Comentando en ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "Añadir comentario", + "ui.lineComment.submit": "Comentar", "ui.sessionTurn.steps.show": "Mostrar pasos", "ui.sessionTurn.steps.hide": "Ocultar pasos", "ui.sessionTurn.summary.response": "Respuesta", diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts index c479411995f..b563f9a0284 100644 --- a/packages/ui/src/i18n/fr.ts +++ b/packages/ui/src/i18n/fr.ts @@ -5,6 +5,14 @@ export const dict = { "ui.sessionReview.expandAll": "Tout développer", "ui.sessionReview.collapseAll": "Tout réduire", + "ui.sessionReview.change.added": "Ajouté", + "ui.sessionReview.change.removed": "Supprimé", + "ui.lineComment.label.prefix": "Commenter sur ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "Commentaire sur ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "Ajouter un commentaire", + "ui.lineComment.submit": "Commenter", "ui.sessionTurn.steps.show": "Afficher les étapes", "ui.sessionTurn.steps.hide": "Masquer les étapes", "ui.sessionTurn.summary.response": "Réponse", diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts index 13cf0e3cb13..7bd4d8e4015 100644 --- a/packages/ui/src/i18n/ja.ts +++ b/packages/ui/src/i18n/ja.ts @@ -5,6 +5,14 @@ export const dict = { "ui.sessionReview.expandAll": "すべて展開", "ui.sessionReview.collapseAll": "すべて折りたたむ", + "ui.sessionReview.change.added": "追加", + "ui.sessionReview.change.removed": "削除", + "ui.lineComment.label.prefix": "", + "ui.lineComment.label.suffix": "へのコメント", + "ui.lineComment.editorLabel.prefix": "", + "ui.lineComment.editorLabel.suffix": "へのコメントを作成中", + "ui.lineComment.placeholder": "コメントを追加", + "ui.lineComment.submit": "コメント", "ui.sessionTurn.steps.show": "ステップを表示", "ui.sessionTurn.steps.hide": "ステップを隠す", "ui.sessionTurn.summary.response": "応答", diff --git a/packages/ui/src/i18n/ko.ts b/packages/ui/src/i18n/ko.ts index 9c3fbaa3578..b83d7ef375b 100644 --- a/packages/ui/src/i18n/ko.ts +++ b/packages/ui/src/i18n/ko.ts @@ -5,6 +5,14 @@ export const dict = { "ui.sessionReview.expandAll": "모두 펼치기", "ui.sessionReview.collapseAll": "모두 접기", + "ui.sessionReview.change.added": "추가됨", + "ui.sessionReview.change.removed": "삭제됨", + "ui.lineComment.label.prefix": "", + "ui.lineComment.label.suffix": "에 댓글 달기", + "ui.lineComment.editorLabel.prefix": "", + "ui.lineComment.editorLabel.suffix": "에 댓글 작성 중", + "ui.lineComment.placeholder": "댓글 추가", + "ui.lineComment.submit": "댓글", "ui.sessionTurn.steps.show": "단계 표시", "ui.sessionTurn.steps.hide": "단계 숨기기", "ui.sessionTurn.summary.response": "응답", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index eefaef2a46b..c9a7481c942 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -8,6 +8,14 @@ export const dict: Record = { "ui.sessionReview.expandAll": "Utvid alle", "ui.sessionReview.collapseAll": "Fold sammen alle", + "ui.sessionReview.change.added": "Lagt til", + "ui.sessionReview.change.removed": "Fjernet", + "ui.lineComment.label.prefix": "Kommenter på ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "Kommenterer på ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "Legg til kommentar", + "ui.lineComment.submit": "Kommenter", "ui.sessionTurn.steps.show": "Vis trinn", "ui.sessionTurn.steps.hide": "Skjul trinn", "ui.sessionTurn.summary.response": "Svar", diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts index f42955d7f4b..5531a7473ce 100644 --- a/packages/ui/src/i18n/pl.ts +++ b/packages/ui/src/i18n/pl.ts @@ -5,6 +5,14 @@ export const dict = { "ui.sessionReview.expandAll": "Rozwiń wszystko", "ui.sessionReview.collapseAll": "Zwiń wszystko", + "ui.sessionReview.change.added": "Dodano", + "ui.sessionReview.change.removed": "Usunięto", + "ui.lineComment.label.prefix": "Komentarz do ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "Komentowanie: ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "Dodaj komentarz", + "ui.lineComment.submit": "Skomentuj", "ui.sessionTurn.steps.show": "Pokaż kroki", "ui.sessionTurn.steps.hide": "Ukryj kroki", "ui.sessionTurn.summary.response": "Odpowiedź", diff --git a/packages/ui/src/i18n/ru.ts b/packages/ui/src/i18n/ru.ts index aae3df401fc..8af9e05a5ea 100644 --- a/packages/ui/src/i18n/ru.ts +++ b/packages/ui/src/i18n/ru.ts @@ -5,6 +5,14 @@ export const dict = { "ui.sessionReview.expandAll": "Развернуть всё", "ui.sessionReview.collapseAll": "Свернуть всё", + "ui.sessionReview.change.added": "Добавлено", + "ui.sessionReview.change.removed": "Удалено", + "ui.lineComment.label.prefix": "Комментарий к ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "Комментирование: ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "Добавить комментарий", + "ui.lineComment.submit": "Комментировать", "ui.sessionTurn.steps.show": "Показать шаги", "ui.sessionTurn.steps.hide": "Скрыть шаги", "ui.sessionTurn.summary.response": "Ответ", diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 4986a5b8411..c81f4725bd4 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -9,6 +9,14 @@ export const dict = { "ui.sessionReview.expandAll": "全部展开", "ui.sessionReview.collapseAll": "全部收起", + "ui.sessionReview.change.added": "已添加", + "ui.sessionReview.change.removed": "已移除", + "ui.lineComment.label.prefix": "评论 ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "正在评论 ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "添加评论", + "ui.lineComment.submit": "评论", "ui.sessionTurn.steps.show": "显示步骤", "ui.sessionTurn.steps.hide": "隐藏步骤", "ui.sessionTurn.summary.response": "回复", diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts index 37bf0c804c8..906f602f98f 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -9,6 +9,14 @@ export const dict = { "ui.sessionReview.expandAll": "全部展開", "ui.sessionReview.collapseAll": "全部收合", + "ui.sessionReview.change.added": "已新增", + "ui.sessionReview.change.removed": "已移除", + "ui.lineComment.label.prefix": "評論 ", + "ui.lineComment.label.suffix": "", + "ui.lineComment.editorLabel.prefix": "正在評論 ", + "ui.lineComment.editorLabel.suffix": "", + "ui.lineComment.placeholder": "新增評論", + "ui.lineComment.submit": "評論", "ui.sessionTurn.steps.show": "顯示步驟", "ui.sessionTurn.steps.hide": "隱藏步驟", "ui.sessionTurn.summary.response": "回覆", From d4e3acf17e3c275bb6cd27d41eb35cf7a7fc8767 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:36:36 -0600 Subject: [PATCH 081/232] fix(app): session sync issue --- packages/app/src/context/sync.tsx | 43 +++++++-------------- packages/ui/src/components/session-turn.tsx | 1 + 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index 244e5e07bbf..5c8e140c396 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -16,7 +16,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const sdk = useSDK() type Child = ReturnType<(typeof globalSync)["child"]> - type Store = Child[0] type Setter = Child[1] const current = createMemo(() => globalSync.child(sdk.directory)) @@ -43,18 +42,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ return Math.ceil(count / chunk) * chunk } - const hydrateMessages = (directory: string, store: Store, sessionID: string) => { - const key = keyFor(directory, sessionID) - if (meta.limit[key] !== undefined) return - - const messages = store.message[sessionID] - if (!messages) return - - const limit = limitFor(messages.length) - setMeta("limit", key, limit) - setMeta("complete", key, messages.length < limit) - } - const loadMessages = async (input: { directory: string client: typeof sdk.client @@ -150,21 +137,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const directory = sdk.directory const client = sdk.client const [store, setStore] = globalSync.child(directory) + const key = keyFor(directory, sessionID) const hasSession = (() => { const match = Binary.search(store.session, sessionID, (s) => s.id) return match.found })() - hydrateMessages(directory, store, sessionID) - const hasMessages = store.message[sessionID] !== undefined - if (hasSession && hasMessages) return - - const key = keyFor(directory, sessionID) + const hydrated = meta.limit[key] !== undefined + if (hasSession && hasMessages && hydrated) return const pending = inflight.get(key) if (pending) return pending - const limit = meta.limit[key] ?? chunk + const count = store.message[sessionID]?.length ?? 0 + const limit = hydrated ? (meta.limit[key] ?? chunk) : limitFor(count) const sessionReq = hasSession ? Promise.resolve() @@ -184,15 +170,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ ) }) - const messagesReq = hasMessages - ? Promise.resolve() - : loadMessages({ - directory, - client, - setStore, - sessionID, - limit, - }) + const messagesReq = + hasMessages && hydrated + ? Promise.resolve() + : loadMessages({ + directory, + client, + setStore, + sessionID, + limit, + }) const promise = Promise.all([sessionReq, messagesReq]) .then(() => {}) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 4f8ce37fbbc..5aa79e0b939 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -283,6 +283,7 @@ export function SessionTurn( const shellModePart = createMemo(() => { const p = parts() + if (p.length === 0) return if (!p.every((part) => part?.type === "text" && part?.synthetic)) return const msgs = assistantMessages() From 36b832880d54066648af08618dfd3f5bf70a532a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 26 Jan 2026 19:37:41 +0000 Subject: [PATCH 082/232] chore: generate --- packages/app/src/i18n/de.ts | 6 ++++-- packages/app/src/i18n/es.ts | 6 ++++-- packages/app/src/i18n/fr.ts | 6 ++++-- packages/app/src/i18n/ja.ts | 6 ++++-- packages/app/src/i18n/ko.ts | 3 ++- packages/app/src/i18n/no.ts | 2 +- packages/app/src/i18n/pl.ts | 3 ++- packages/app/src/i18n/ru.ts | 3 ++- 8 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index a54f19d180d..3f40a9a7966 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -286,7 +286,8 @@ export const dict = { "dialog.project.edit.color.select": "{{color}}-Farbe auswählen", "dialog.project.edit.worktree.startup": "Startup-Skript für Arbeitsbereich", - "dialog.project.edit.worktree.startup.description": "Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.", + "dialog.project.edit.worktree.startup.description": + "Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.", "dialog.project.edit.worktree.startup.placeholder": "z. B. bun install", "context.breakdown.title": "Kontext-Aufschlüsselung", "context.breakdown.note": @@ -488,7 +489,8 @@ export const dict = { "terminal.close": "Terminal schließen", "terminal.connectionLost.title": "Verbindung verloren", - "terminal.connectionLost.description": "Die Terminalverbindung wurde unterbrochen. Das kann passieren, wenn der Server neu startet.", + "terminal.connectionLost.description": + "Die Terminalverbindung wurde unterbrochen. Das kann passieren, wenn der Server neu startet.", "common.closeTab": "Tab schließen", "common.dismiss": "Verwerfen", "common.requestFailed": "Anfrage fehlgeschlagen", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 06f92e07f04..542143b40ab 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -281,7 +281,8 @@ export const dict = { "dialog.project.edit.color.select": "Seleccionar color {{color}}", "dialog.project.edit.worktree.startup": "Script de inicio del espacio de trabajo", - "dialog.project.edit.worktree.startup.description": "Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).", + "dialog.project.edit.worktree.startup.description": + "Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).", "dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install", "context.breakdown.title": "Desglose de Contexto", "context.breakdown.note": @@ -482,7 +483,8 @@ export const dict = { "terminal.close": "Cerrar terminal", "terminal.connectionLost.title": "Conexión perdida", - "terminal.connectionLost.description": "La conexión del terminal se interrumpió. Esto puede ocurrir cuando el servidor se reinicia.", + "terminal.connectionLost.description": + "La conexión del terminal se interrumpió. Esto puede ocurrir cuando el servidor se reinicia.", "common.closeTab": "Cerrar pestaña", "common.dismiss": "Descartar", "common.requestFailed": "Solicitud fallida", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 3a3017f080b..217e4539184 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -281,7 +281,8 @@ export const dict = { "dialog.project.edit.color.select": "Sélectionner la couleur {{color}}", "dialog.project.edit.worktree.startup": "Script de démarrage de l'espace de travail", - "dialog.project.edit.worktree.startup.description": "S'exécute après la création d'un nouvel espace de travail (arbre de travail).", + "dialog.project.edit.worktree.startup.description": + "S'exécute après la création d'un nouvel espace de travail (arbre de travail).", "dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install", "context.breakdown.title": "Répartition du contexte", "context.breakdown.note": @@ -487,7 +488,8 @@ export const dict = { "terminal.close": "Fermer le terminal", "terminal.connectionLost.title": "Connexion perdue", - "terminal.connectionLost.description": "La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.", + "terminal.connectionLost.description": + "La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.", "common.closeTab": "Fermer l'onglet", "common.dismiss": "Ignorer", "common.requestFailed": "La demande a échoué", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 7c629c3a01c..02a787c026f 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -280,7 +280,8 @@ export const dict = { "dialog.project.edit.color.select": "{{color}}の色を選択", "dialog.project.edit.worktree.startup": "ワークスペース起動スクリプト", - "dialog.project.edit.worktree.startup.description": "新しいワークスペース (ワークツリー) を作成した後に実行されます。", + "dialog.project.edit.worktree.startup.description": + "新しいワークスペース (ワークツリー) を作成した後に実行されます。", "dialog.project.edit.worktree.startup.placeholder": "例: bun install", "context.breakdown.title": "コンテキストの内訳", "context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。', @@ -479,7 +480,8 @@ export const dict = { "terminal.close": "ターミナルを閉じる", "terminal.connectionLost.title": "接続が失われました", - "terminal.connectionLost.description": "ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。", + "terminal.connectionLost.description": + "ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。", "common.closeTab": "タブを閉じる", "common.dismiss": "閉じる", "common.requestFailed": "リクエスト失敗", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 0a04d62511c..8360b46ab09 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -481,7 +481,8 @@ export const dict = { "terminal.close": "터미널 닫기", "terminal.connectionLost.title": "연결 끊김", - "terminal.connectionLost.description": "터미널 연결이 중단되었습니다. 서버가 재시작하면 이런 일이 발생할 수 있습니다.", + "terminal.connectionLost.description": + "터미널 연결이 중단되었습니다. 서버가 재시작하면 이런 일이 발생할 수 있습니다.", "common.closeTab": "탭 닫기", "common.dismiss": "닫기", "common.requestFailed": "요청 실패", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index c2992d57a69..3d3f382a6b1 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -683,7 +683,7 @@ export const dict = { "session.delete.failed.title": "Kunne ikke slette sesjon", "session.delete.title": "Slett sesjon", - "session.delete.confirm": "Slette sesjonen \"{{name}}\"?", + "session.delete.confirm": 'Slette sesjonen "{{name}}"?', "session.delete.button": "Slett sesjon", "workspace.new": "Nytt arbeidsområde", "workspace.type.local": "lokal", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 75b461487b6..5b7bec06bcc 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -281,7 +281,8 @@ export const dict = { "dialog.project.edit.color.select": "Wybierz kolor {{color}}", "dialog.project.edit.worktree.startup": "Skrypt uruchamiania przestrzeni roboczej", - "dialog.project.edit.worktree.startup.description": "Uruchamiany po utworzeniu nowej przestrzeni roboczej (drzewa roboczego).", + "dialog.project.edit.worktree.startup.description": + "Uruchamiany po utworzeniu nowej przestrzeni roboczej (drzewa roboczego).", "dialog.project.edit.worktree.startup.placeholder": "np. bun install", "context.breakdown.title": "Podział kontekstu", "context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.', diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 766c253e3b6..ab579c683c0 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -281,7 +281,8 @@ export const dict = { "dialog.project.edit.color.select": "Выбрать цвет {{color}}", "dialog.project.edit.worktree.startup": "Скрипт запуска рабочего пространства", - "dialog.project.edit.worktree.startup.description": "Запускается после создания нового рабочего пространства (worktree).", + "dialog.project.edit.worktree.startup.description": + "Запускается после создания нового рабочего пространства (worktree).", "dialog.project.edit.worktree.startup.placeholder": "например, bun install", "context.breakdown.title": "Разбивка контекста", "context.breakdown.note": From 810bc012b6ba5adfb42d8c41032491d2f659b1c5 Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 26 Jan 2026 15:45:57 +0000 Subject: [PATCH 083/232] fix(ui): update button styles and disconnect button size --- packages/app/src/components/settings-providers.tsx | 2 +- packages/ui/src/components/button.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index aec6d6c4f06..2b307a04b49 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -91,7 +91,7 @@ export const SettingsProviders: Component = () => { {type(item)}
- diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css index 225e64c775f..933c7ce5f24 100644 --- a/packages/ui/src/components/button.css +++ b/packages/ui/src/components/button.css @@ -148,7 +148,7 @@ padding: 0 12px 0 8px; } - gap: 8px; + gap: 4px; /* text-14-medium */ font-family: var(--font-family-sans); From e0e97e9d93c2dc19b41222e72b5b5965ad125e4a Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 26 Jan 2026 16:00:30 +0000 Subject: [PATCH 084/232] fix(app): set provider row height to 56px --- packages/app/src/components/settings-providers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 2b307a04b49..a1819678eb3 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -84,7 +84,7 @@ export const SettingsProviders: Component = () => { > {(item) => ( -
+
{item.name} @@ -107,7 +107,7 @@ export const SettingsProviders: Component = () => {
{(item) => ( -
+
{item.name} From 9346c1ae3ffa9781521bbc81a24141d3227ad41b Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 26 Jan 2026 16:09:49 +0000 Subject: [PATCH 085/232] fix(app): add hover text for env-connected providers --- packages/app/src/components/settings-providers.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index a1819678eb3..3f62199ccf9 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -84,13 +84,20 @@ export const SettingsProviders: Component = () => { > {(item) => ( -
+
{item.name} {type(item)}
- + + Connected from your environment variables + + } + > From 7c96d704d306e72d5e806fd6db58204f0af421b2 Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 26 Jan 2026 16:17:35 +0000 Subject: [PATCH 086/232] fix(app): use default cursor for env provider text --- packages/app/src/components/settings-providers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 3f62199ccf9..7783c142d92 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -93,7 +93,7 @@ export const SettingsProviders: Component = () => { + Connected from your environment variables } From 6f3d4134722981adee00716eab77b961737d263d Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 26 Jan 2026 16:20:01 +0000 Subject: [PATCH 087/232] feat(ui): add providers icon and use in settings --- packages/app/src/components/dialog-settings.tsx | 2 +- packages/ui/src/components/icon.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index dbae37c9d85..db26d4ca0c3 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -38,7 +38,7 @@ export const DialogSettings: Component = () => { {language.t("settings.section.server")}
- + {language.t("settings.providers.title")} diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index f082605b491..03285532872 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -73,6 +73,7 @@ const icons = { selector: ``, "arrow-down-to-line": ``, link: ``, + providers: ``, } export interface IconProps extends ComponentProps<"svg"> { From ecd04a118a42fd558ee2e0442bcbdee260b4a10d Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 26 Jan 2026 16:23:48 +0000 Subject: [PATCH 088/232] feat(ui): add models icon and use in settings --- packages/app/src/components/dialog-settings.tsx | 2 +- packages/ui/src/components/icon.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index db26d4ca0c3..d77004d9140 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -42,7 +42,7 @@ export const DialogSettings: Component = () => { {language.t("settings.providers.title")} - + {language.t("settings.models.title")}
diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 03285532872..ddedddb36e5 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -74,6 +74,7 @@ const icons = { "arrow-down-to-line": ``, link: ``, providers: ``, + models: ``, } export interface IconProps extends ComponentProps<"svg"> { From 0a572afd46ecdaaee3d411fe95f0ff9c26b8e1ab Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 26 Jan 2026 16:29:24 +0000 Subject: [PATCH 089/232] fix(app): style view all button with interactive color and margin --- packages/app/src/components/settings-providers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 7783c142d92..eec0ac7b9bf 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -148,7 +148,7 @@ export const SettingsProviders: Component = () => {
- +
)} From 5a16d99b601a08deedf8e687c7cb70b605a6b02c Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 26 Jan 2026 19:53:05 +0000 Subject: [PATCH 107/232] fix(app): disable tooltips in filetree tabs --- packages/app/src/pages/session.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index d717cc3d8cb..9fcf68a87a6 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -2672,7 +2672,12 @@ export default function Page() { - openTab(file.tab(node.path))} /> + openTab(file.tab(node.path))} + />
From ae815cca3a2f39fb21ed63c92c3ab78c42f53389 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:32:49 -0600 Subject: [PATCH 108/232] test(app): fix e2e test --- packages/app/e2e/fixtures.ts | 51 +++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/fixtures.ts index 721d60049ce..c5315ff194d 100644 --- a/packages/app/e2e/fixtures.ts +++ b/packages/app/e2e/fixtures.ts @@ -1,5 +1,5 @@ import { test as base, expect } from "@playwright/test" -import { createSdk, dirSlug, getWorktree, promptSelector, sessionPath } from "./utils" +import { createSdk, dirSlug, getWorktree, promptSelector, serverUrl, sessionPath } from "./utils" type TestFixtures = { sdk: ReturnType @@ -29,6 +29,55 @@ export const test = base.extend({ await use(createSdk(directory)) }, gotoSession: async ({ page, directory }, use) => { + await page.addInitScript( + (input: { directory: string; serverUrl: string }) => { + const key = "opencode.global.dat:server" + const raw = localStorage.getItem(key) + const parsed = (() => { + if (!raw) return undefined + try { + return JSON.parse(raw) as unknown + } catch { + return undefined + } + })() + + const store = parsed && typeof parsed === "object" ? (parsed as Record) : {} + const list = Array.isArray(store.list) ? store.list : [] + const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {} + const projects = store.projects && typeof store.projects === "object" ? store.projects : {} + const nextProjects = { ...(projects as Record) } + + const add = (origin: string) => { + const current = nextProjects[origin] + const items = Array.isArray(current) ? current : [] + const existing = items.filter( + (p): p is { worktree: string; expanded?: boolean } => + !!p && + typeof p === "object" && + "worktree" in p && + typeof (p as { worktree?: unknown }).worktree === "string", + ) + + if (existing.some((p) => p.worktree === input.directory)) return + nextProjects[origin] = [{ worktree: input.directory, expanded: true }, ...existing] + } + + add("local") + add(input.serverUrl) + + localStorage.setItem( + key, + JSON.stringify({ + list, + projects: nextProjects, + lastProject, + }), + ) + }, + { directory, serverUrl }, + ) + const gotoSession = async (sessionID?: string) => { await page.goto(sessionPath(directory, sessionID)) await expect(page.locator(promptSelector)).toBeVisible() From c700b928e4c42dbb7f5807cd4de81ea85c44aca4 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 26 Jan 2026 15:38:34 -0500 Subject: [PATCH 109/232] ci: add stale pr job --- .github/workflows/close-stale-prs.yml | 78 +++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/close-stale-prs.yml diff --git a/.github/workflows/close-stale-prs.yml b/.github/workflows/close-stale-prs.yml new file mode 100644 index 00000000000..a9a0502ea5c --- /dev/null +++ b/.github/workflows/close-stale-prs.yml @@ -0,0 +1,78 @@ +name: Close stale PRs + +on: + workflow_dispatch: + schedule: + - cron: "0 6 * * *" + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + close-stale-prs: + runs-on: ubuntu-latest + steps: + - name: Close inactive PRs + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const DAYS_INACTIVE = 60 + const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000) + const { owner, repo } = context.repo + const dryRun = false + const stalePrs = [] + + core.info(`Dry run mode: ${dryRun}`) + + const prs = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: "open", + per_page: 100, + sort: "updated", + direction: "asc", + }) + + for (const pr of prs) { + const lastUpdated = new Date(pr.updated_at) + if (lastUpdated > cutoff) { + core.info(`PR ${pr.number} is fresh`) + continue + } + + stalePrs.push(pr) + } + + if (!stalePrs.length) { + core.info("No stale pull requests found.") + return + } + + for (const pr of stalePrs) { + const issue_number = pr.number + const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.` + + if (dryRun) { + core.info(`[dry-run] Would close PR #${issue_number} from ${pr.user.login}`) + continue + } + + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: closeComment, + }) + + await github.rest.pulls.update({ + owner, + repo, + pull_number: issue_number, + state: "closed", + }) + + core.info(`Closed PR #${issue_number} from ${pr.user.login}`) + } From 6a62b44593b7be108af2245730175868ec967c1e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 26 Jan 2026 15:42:50 -0500 Subject: [PATCH 110/232] ci: add dry-run option to stale PR closer workflow Allows testing stale PR closure without actually closing PRs --- .github/workflows/close-stale-prs.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/close-stale-prs.yml b/.github/workflows/close-stale-prs.yml index a9a0502ea5c..787ee02e62b 100644 --- a/.github/workflows/close-stale-prs.yml +++ b/.github/workflows/close-stale-prs.yml @@ -2,6 +2,11 @@ name: Close stale PRs on: workflow_dispatch: + inputs: + dryRun: + description: "Log actions without closing PRs" + type: boolean + default: false schedule: - cron: "0 6 * * *" @@ -22,7 +27,7 @@ jobs: const DAYS_INACTIVE = 60 const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000) const { owner, repo } = context.repo - const dryRun = false + const dryRun = context.payload.inputs?.dryRun === "true" const stalePrs = [] core.info(`Dry run mode: ${dryRun}`) From 8b5dde5536b5725e68eb43933d23dae22c298b35 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 26 Jan 2026 16:12:41 -0500 Subject: [PATCH 111/232] tweak: retry logic to catch certain provider problems --- packages/opencode/src/session/retry.ts | 49 ++++++++++++-------- packages/opencode/test/session/retry.test.ts | 27 +++++++++++ 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index dd0fe238022..8d3f72844fa 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -1,5 +1,6 @@ import type { NamedError } from "@opencode-ai/util/error" import { MessageV2 } from "./message-v2" +import { iife } from "@/util/iife" export namespace SessionRetry { export const RETRY_INITIAL_DELAY = 2000 @@ -63,28 +64,36 @@ export namespace SessionRetry { return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message } - if (typeof error.data?.message === "string") { + const json = iife(() => { try { - const json = JSON.parse(error.data.message) - if (json.type === "error" && json.error?.type === "too_many_requests") { - return "Too Many Requests" + if (typeof error.data?.message === "string") { + const parsed = JSON.parse(error.data.message) + return parsed } - if (json.code.includes("exhausted") || json.code.includes("unavailable")) { - return "Provider is overloaded" - } - if (json.type === "error" && json.error?.code?.includes("rate_limit")) { - return "Rate Limited" - } - if ( - json.error?.message?.includes("no_kv_space") || - (json.type === "error" && json.error?.type === "server_error") || - !!json.error - ) { - return "Provider Server Error" - } - } catch {} - } - return undefined + return JSON.parse(error.data.message) + } catch { + return undefined + } + }) + if (!json || typeof json !== "object") return undefined + const code = typeof json.code === "string" ? json.code : "" + + if (json.type === "error" && json.error?.type === "too_many_requests") { + return "Too Many Requests" + } + if (code.includes("exhausted") || code.includes("unavailable")) { + return "Provider is overloaded" + } + if (json.type === "error" && json.error?.code?.includes("rate_limit")) { + return "Rate Limited" + } + if ( + json.error?.message?.includes("no_kv_space") || + (json.type === "error" && json.error?.type === "server_error") || + !!json.error + ) { + return "Provider Server Error" + } } } diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 22ffb0cb117..6866b03a155 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test" +import type { NamedError } from "@opencode-ai/util/error" import { APICallError } from "ai" import { SessionRetry } from "../../src/session/retry" import { MessageV2 } from "../../src/session/message-v2" @@ -11,6 +12,10 @@ function apiError(headers?: Record): MessageV2.APIError { }).toObject() as MessageV2.APIError } +function wrap(message: unknown): ReturnType { + return { data: { message } } as ReturnType +} + describe("session.retry.delay", () => { test("caps delay at 30 seconds when headers missing", () => { const error = apiError() @@ -81,6 +86,28 @@ describe("session.retry.delay", () => { }) }) +describe("session.retry.retryable", () => { + test("maps too_many_requests json messages", () => { + const error = wrap(JSON.stringify({ type: "error", error: { type: "too_many_requests" } })) + expect(SessionRetry.retryable(error)).toBe("Too Many Requests") + }) + + test("maps overloaded provider codes", () => { + const error = wrap(JSON.stringify({ code: "resource_exhausted" })) + expect(SessionRetry.retryable(error)).toBe("Provider is overloaded") + }) + + test("handles json messages without code", () => { + const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } })) + expect(SessionRetry.retryable(error)).toBe("Provider Server Error") + }) + + test("returns undefined for non-json message", () => { + const error = wrap("not-json") + expect(SessionRetry.retryable(error)).toBeUndefined() + }) +}) + describe("session.message-v2.fromError", () => { test.concurrent( "converts ECONNRESET socket errors to retryable APIError", From cbe8f265b96dce46b187d27978cc1728974a2bfb Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:08:25 -0600 Subject: [PATCH 112/232] fix(app): disconnect zen provider --- packages/app/src/components/settings-providers.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index dbf2ffc9087..e39e8a8176f 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -20,7 +20,10 @@ export const SettingsProviders: Component = () => { const globalSDK = useGlobalSDK() const providers = useProviders() - const connected = createMemo(() => providers.connected()) + const connected = createMemo(() => { + const paid = providers.paid().length > 0 + return providers.connected().filter((p) => p.id !== "opencode" || paid) + }) const popular = createMemo(() => { const connectedIDs = new Set(connected().map((p) => p.id)) const items = providers From d82e94c209c5a8c25d9646144ff92a95e739f065 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:23:20 -0600 Subject: [PATCH 113/232] fix(app): zen disconnect not working --- packages/app/src/components/settings-providers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index e39e8a8176f..104663c20a4 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -21,9 +21,9 @@ export const SettingsProviders: Component = () => { const providers = useProviders() const connected = createMemo(() => { - const paid = providers.paid().length > 0 - return providers.connected().filter((p) => p.id !== "opencode" || paid) + return providers.connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)) }) + const popular = createMemo(() => { const connectedIDs = new Set(connected().map((p) => p.id)) const items = providers From b21f82f5b0b8bfce977f91c7bfc3eb9443e29250 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 26 Jan 2026 21:24:09 +0000 Subject: [PATCH 114/232] chore: generate --- packages/app/src/components/settings-providers.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 104663c20a4..d098db279d9 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -21,7 +21,9 @@ export const SettingsProviders: Component = () => { const providers = useProviders() const connected = createMemo(() => { - return providers.connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)) + return providers + .connected() + .filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)) }) const popular = createMemo(() => { From 7b3d5f1d68c570335d8111ca735168904e10d5d0 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:29:54 -0600 Subject: [PATCH 115/232] chore: cleanup --- packages/app/src/components/session/session-header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 1e18d2915aa..4070b371e02 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -315,7 +315,7 @@ export function SessionHeader() { + ) : ( + + )} +
+ + {total > 1 && ( +
+ {release.features.map((_, i) => ( + + ))} +
+ )} +
+
+ + {/* Right side - Media content (edge to edge) */} + {feature().media && ( +
+ {feature().media!.type === "image" ? ( + {feature().media!.alt + ) : ( +
+ )} + + ) +} diff --git a/packages/app/src/components/release-notes-handler.tsx b/packages/app/src/components/release-notes-handler.tsx new file mode 100644 index 00000000000..45237b57791 --- /dev/null +++ b/packages/app/src/components/release-notes-handler.tsx @@ -0,0 +1,31 @@ +import { onMount } from "solid-js" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { DialogReleaseNotes } from "./dialog-release-notes" +import { shouldShowReleaseNotes, markReleaseNotesSeen } from "@/lib/release-notes" + +/** + * Component that handles showing release notes modal on app startup. + * Shows the modal if: + * - DEV_ALWAYS_SHOW_RELEASE_NOTES is true in lib/release-notes.ts + * - OR the user hasn't seen the current version's release notes yet + * + * To disable the dev mode behavior, set DEV_ALWAYS_SHOW_RELEASE_NOTES to false + * in packages/app/src/lib/release-notes.ts + */ +export function ReleaseNotesHandler() { + const dialog = useDialog() + + onMount(() => { + // Small delay to ensure app is fully loaded before showing modal + setTimeout(() => { + if (shouldShowReleaseNotes()) { + dialog.show( + () => , + () => markReleaseNotesSeen(), + ) + } + }, 500) + }) + + return null +} diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 3d7b9db7af9..c0c7da8585f 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -55,3 +55,30 @@ scrollbar-width: thin !important; scrollbar-color: var(--border-weak-base) transparent !important; } + +/* Wider dialog variant for release notes modal */ +[data-component="dialog"]:has(.dialog-release-notes) { + padding: 20px; + box-sizing: border-box; + + [data-slot="dialog-container"] { + width: min(100%, 720px); + height: min(100%, 400px); + margin-top: -80px; + + [data-slot="dialog-content"] { + min-height: auto; + overflow: hidden; + height: 100%; + border: none; + box-shadow: var(--shadow-lg-border-base); + } + + [data-slot="dialog-body"] { + overflow: hidden; + height: 100%; + display: flex; + flex-direction: row; + } + } +} diff --git a/packages/app/src/lib/release-notes.ts b/packages/app/src/lib/release-notes.ts new file mode 100644 index 00000000000..a28843acdfe --- /dev/null +++ b/packages/app/src/lib/release-notes.ts @@ -0,0 +1,53 @@ +import { CURRENT_RELEASE } from "@/components/dialog-release-notes" + +const STORAGE_KEY = "opencode:last-seen-version" + +// ============================================================================ +// DEV MODE: Set this to true to always show the release notes modal on startup +// Set to false for production behavior (only shows after updates) +// ============================================================================ +const DEV_ALWAYS_SHOW_RELEASE_NOTES = true + +/** + * Check if release notes should be shown + * Returns true if: + * - DEV_ALWAYS_SHOW_RELEASE_NOTES is true (for development) + * - OR the current version is newer than the last seen version + */ +export function shouldShowReleaseNotes(): boolean { + if (DEV_ALWAYS_SHOW_RELEASE_NOTES) { + console.log("[ReleaseNotes] DEV mode: always showing release notes") + return true + } + + const lastSeen = localStorage.getItem(STORAGE_KEY) + if (!lastSeen) { + // First time user - show release notes + return true + } + + // Compare versions - show if current is newer + return CURRENT_RELEASE.version !== lastSeen +} + +/** + * Mark the current release notes as seen + * Call this when the user closes the release notes modal + */ +export function markReleaseNotesSeen(): void { + localStorage.setItem(STORAGE_KEY, CURRENT_RELEASE.version) +} + +/** + * Get the current version + */ +export function getCurrentVersion(): string { + return CURRENT_RELEASE.version +} + +/** + * Reset the seen status (useful for testing) + */ +export function resetReleaseNotesSeen(): void { + localStorage.removeItem(STORAGE_KEY) +} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b13cb1ac34e..601a240675a 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -68,6 +68,7 @@ import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { navStart } from "@/utils/perf" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogEditProject } from "@/components/dialog-edit-project" +import { ReleaseNotesHandler } from "@/components/release-notes-handler" import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" import { useLanguage, type Locale } from "@/context/language" @@ -1443,6 +1444,11 @@ export default function Layout(props: ParentProps) { ), ) + createEffect(() => { + const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48 + document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`) + }) + createEffect(() => { const project = currentProject() if (!project) return @@ -2791,6 +2797,7 @@ export default function Layout(props: ParentProps) {
+
) } From c1e840b9b229889c1a85b479b3cd601d6f313c87 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 05:58:38 -0600 Subject: [PATCH 118/232] chore: cleanup --- .../src/components/dialog-release-notes.tsx | 141 +++++++++++++++++- 1 file changed, 133 insertions(+), 8 deletions(-) diff --git a/packages/app/src/components/dialog-release-notes.tsx b/packages/app/src/components/dialog-release-notes.tsx index c5a1e15c13b..d6375dbbc49 100644 --- a/packages/app/src/components/dialog-release-notes.tsx +++ b/packages/app/src/components/dialog-release-notes.tsx @@ -4,6 +4,107 @@ import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { markReleaseNotesSeen } from "@/lib/release-notes" +const CHANGELOG_URL = "https://opencode.ai/changelog.json" + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function getText(value: unknown): string | undefined { + if (typeof value === "string") { + const text = value.trim() + return text.length > 0 ? text : undefined + } + + if (!Array.isArray(value)) return + const parts = value.map((item) => (typeof item === "string" ? item.trim() : "")).filter((item) => item.length > 0) + if (parts.length === 0) return + return parts.join(" ") +} + +function normalizeRemoteUrl(url: string): string { + if (url.startsWith("https://") || url.startsWith("http://")) return url + if (url.startsWith("/")) return `https://opencode.ai${url}` + return `https://opencode.ai/${url}` +} + +function parseMedia(value: unknown): ReleaseFeature["media"] | undefined { + if (!isRecord(value)) return + + const type = getText(value.type)?.toLowerCase() + const src = getText(value.src) + if (!src) return + if (type !== "image" && type !== "video") return + + return { + type, + src: normalizeRemoteUrl(src), + alt: getText(value.alt), + } +} + +function parseFeature(value: unknown): ReleaseFeature | undefined { + if (!isRecord(value)) return + + const title = getText(value.title) ?? getText(value.name) ?? getText(value.heading) + const description = getText(value.description) ?? getText(value.body) ?? getText(value.text) + + if (!title) return + if (!description) return + + const tag = getText(value.tag) ?? getText(value.label) ?? "New" + + const media = (() => { + const parsed = parseMedia(value.media) + if (parsed) return parsed + + const alt = getText(value.alt) + const image = getText(value.image) + if (image) return { type: "image" as const, src: normalizeRemoteUrl(image), alt } + + const video = getText(value.video) + if (video) return { type: "video" as const, src: normalizeRemoteUrl(video), alt } + })() + + return { title, description, tag, media } +} + +function parseChangelog(value: unknown): ReleaseNote | undefined { + const releases = (() => { + if (Array.isArray(value)) return value + if (!isRecord(value)) return + if (Array.isArray(value.releases)) return value.releases + if (Array.isArray(value.versions)) return value.versions + if (Array.isArray(value.changelog)) return value.changelog + })() + + if (!releases) { + if (!isRecord(value)) return + if (!Array.isArray(value.highlights)) return + const features = value.highlights.map(parseFeature).filter((item): item is ReleaseFeature => item !== undefined) + if (features.length === 0) return + return { version: CURRENT_RELEASE.version, features: features.slice(0, 3) } + } + + const version = (() => { + const head = releases[0] + if (!isRecord(head)) return + return getText(head.version) ?? getText(head.tag_name) ?? getText(head.tag) ?? getText(head.name) + })() + + const features = releases + .flatMap((item) => { + if (!isRecord(item)) return [] + const highlights = item.highlights + if (!Array.isArray(highlights)) return [] + return highlights.map(parseFeature).filter((feature): feature is ReleaseFeature => feature !== undefined) + }) + .slice(0, 3) + + if (features.length === 0) return + return { version: version ?? CURRENT_RELEASE.version, features } +} + export interface ReleaseFeature { title: string description: string @@ -59,13 +160,13 @@ export const CURRENT_RELEASE: ReleaseNote = { export function DialogReleaseNotes(props: { release?: ReleaseNote }) { const dialog = useDialog() - const release = props.release ?? CURRENT_RELEASE + const [note, setNote] = createSignal(props.release ?? CURRENT_RELEASE) const [index, setIndex] = createSignal(0) - const feature = () => release.features[index()] - const total = release.features.length + const feature = () => note().features[index()] ?? note().features[0] ?? CURRENT_RELEASE.features[0]! + const total = () => note().features.length const isFirst = () => index() === 0 - const isLast = () => index() === total - 1 + const isLast = () => index() === total() - 1 function handleNext() { if (!isLast()) setIndex(index() + 1) @@ -97,6 +198,26 @@ export function DialogReleaseNotes(props: { release?: ReleaseNote }) { focusTrap?.focus() document.addEventListener("keydown", handleKeyDown) onCleanup(() => document.removeEventListener("keydown", handleKeyDown)) + + const controller = new AbortController() + fetch(CHANGELOG_URL, { + signal: controller.signal, + headers: { Accept: "application/json" }, + }) + .then((response) => (response.ok ? (response.json() as Promise) : undefined)) + .then((json) => { + if (!json) return + const parsed = parseChangelog(json) + if (!parsed) return + setNote({ + version: parsed.version, + features: parsed.features, + }) + setIndex(0) + }) + .catch(() => undefined) + + onCleanup(() => controller.abort()) }) // Refocus the trap when index changes to ensure escape always works @@ -144,16 +265,20 @@ export function DialogReleaseNotes(props: { release?: ReleaseNote }) { )}
- {total > 1 && ( + {total() > 1 && (
- {release.features.map((_, i) => ( + {note().features.map((_, i) => (
-
) } From 53ac394c685780985d776c4f861c283714615999 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:25:25 -0600 Subject: [PATCH 120/232] wip: highlights --- packages/app/src/context/highlights.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/app/src/context/highlights.tsx b/packages/app/src/context/highlights.tsx index 4c2e8c838d8..2d20660d72f 100644 --- a/packages/app/src/context/highlights.tsx +++ b/packages/app/src/context/highlights.tsx @@ -6,7 +6,7 @@ import { usePlatform } from "@/context/platform" import { persisted } from "@/utils/persist" import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes" -const CHANGELOG_URL = "https://opencode.ai/changelog.json" +const CHANGELOG_URL = "https://dev.opencode.ai/changelog.json" type Store = { version?: string @@ -81,6 +81,10 @@ function parseRelease(value: unknown): ParsedRelease | undefined { } function parseChangelog(value: unknown): ParsedRelease[] | undefined { + if (Array.isArray(value)) { + return value.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined) + } + if (!isRecord(value)) return if (!Array.isArray(value.releases)) return @@ -163,6 +167,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple if (!json) return const releases = parseChangelog(json) if (!releases) return + if (releases.length === 0) return const highlights = sliceHighlights({ releases, current: platform.version, From ccc7aa49c33dc2247ceee0a51e7816ea5803c404 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:34:59 -0600 Subject: [PATCH 121/232] wip: highlights --- .../src/components/dialog-release-notes.tsx | 30 ++++++----- .../app/src/components/settings-general.tsx | 17 ++++++ packages/app/src/context/highlights.tsx | 54 +++++++++++++------ packages/app/src/context/settings.tsx | 6 +++ packages/app/src/i18n/en.ts | 4 ++ 5 files changed, 82 insertions(+), 29 deletions(-) diff --git a/packages/app/src/components/dialog-release-notes.tsx b/packages/app/src/components/dialog-release-notes.tsx index c62cbc18837..d3ee7e201a7 100644 --- a/packages/app/src/components/dialog-release-notes.tsx +++ b/packages/app/src/components/dialog-release-notes.tsx @@ -2,11 +2,11 @@ import { createSignal, createEffect, onMount, onCleanup } from "solid-js" import { Dialog } from "@opencode-ai/ui/dialog" import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useSettings } from "@/context/settings" export type Highlight = { title: string description: string - tag?: string media?: { type: "image" | "video" src: string @@ -16,6 +16,7 @@ export type Highlight = { export function DialogReleaseNotes(props: { highlights: Highlight[] }) { const dialog = useDialog() + const settings = useSettings() const [index, setIndex] = createSignal(0) const total = () => props.highlights.length @@ -34,9 +35,20 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) { dialog.close() } + function handleDisable() { + settings.general.setReleaseNotes(false) + handleClose() + } + let focusTrap: HTMLDivElement | undefined function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") { + e.preventDefault() + handleClose() + return + } + if (!paged()) return if (e.key === "ArrowLeft" && !isFirst()) { e.preventDefault() @@ -50,8 +62,6 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) { onMount(() => { focusTrap?.focus() - - if (!paged()) return document.addEventListener("keydown", handleKeyDown) onCleanup(() => document.removeEventListener("keydown", handleKeyDown)) }) @@ -72,14 +82,6 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {

{feature()?.title ?? ""}

- {feature()?.tag && ( - - {feature()!.tag} - - )}

{feature()?.description ?? ""}

@@ -89,7 +91,7 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) { {/* Bottom section - buttons and indicators (fixed position) */}
-
+
{isLast() ? ( )} + +
{paged() && ( diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index cfb8a998d33..e7e5b67f3a8 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -214,6 +214,23 @@ export const SettingsGeneral: Component = () => {
+ {/* Updates Section */} +
+

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

+ +
+ + settings.general.setReleaseNotes(checked)} + /> + +
+
+ {/* Sound effects Section */}

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

diff --git a/packages/app/src/context/highlights.tsx b/packages/app/src/context/highlights.tsx index 2d20660d72f..e55bca675d5 100644 --- a/packages/app/src/context/highlights.tsx +++ b/packages/app/src/context/highlights.tsx @@ -3,10 +3,11 @@ import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" import { usePlatform } from "@/context/platform" +import { useSettings } from "@/context/settings" import { persisted } from "@/utils/persist" import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes" -const CHANGELOG_URL = "https://dev.opencode.ai/changelog.json" +const CHANGELOG_URL = "https://opencode.ai/changelog.json" type Store = { version?: string @@ -18,7 +19,7 @@ type ParsedRelease = { } function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null + return typeof value === "object" && value !== null && !Array.isArray(value) } function getText(value: unknown): string | undefined { @@ -40,14 +41,14 @@ function normalizeVersion(value: string | undefined) { function parseMedia(value: unknown, alt: string): Highlight["media"] | undefined { if (!isRecord(value)) return const type = getText(value.type)?.toLowerCase() - const src = getText(value.src) + const src = getText(value.src) ?? getText(value.url) if (!src) return if (type !== "image" && type !== "video") return return { type, src, alt } } -function parseHighlight(value: unknown, tag: string | undefined): Highlight | undefined { +function parseHighlight(value: unknown): Highlight | undefined { if (!isRecord(value)) return const title = getText(value.title) @@ -57,7 +58,7 @@ function parseHighlight(value: unknown, tag: string | undefined): Highlight | un if (!description) return const media = parseMedia(value.media, title) - return { title, description, tag, media } + return { title, description, media } } function parseRelease(value: unknown): ParsedRelease | undefined { @@ -70,11 +71,18 @@ function parseRelease(value: unknown): ParsedRelease | undefined { const highlights = value.highlights.flatMap((group) => { if (!isRecord(group)) return [] - if (!Array.isArray(group.items)) return [] + const source = getText(group.source) - return group.items - .map((item) => parseHighlight(item, source)) - .filter((item): item is Highlight => item !== undefined) + if (!source) return [] + if (!source.toLowerCase().includes("desktop")) return [] + + if (Array.isArray(group.items)) { + return group.items.map((item) => parseHighlight(item)).filter((item): item is Highlight => item !== undefined) + } + + const item = parseHighlight(group) + if (!item) return [] + return [item] }) return { tag, highlights } @@ -108,10 +116,17 @@ function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; p return index === -1 ? releases.length : index })() - return releases - .slice(start, end) - .flatMap((release) => release.highlights) - .slice(0, 3) + const highlights = releases.slice(start, end).flatMap((release) => release.highlights) + const seen = new Set() + const unique = highlights.filter((highlight) => { + const key = [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join( + "\n", + ) + if (seen.has(key)) return false + seen.add(key) + return true + }) + return unique.slice(0, 3) } export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({ @@ -120,6 +135,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple init: () => { const platform = usePlatform() const dialog = useDialog() + const settings = useSettings() const [store, setStore, _, ready] = persisted("highlights.v1", createStore({ version: undefined })) const [from, setFrom] = createSignal(undefined) @@ -135,6 +151,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple createEffect(() => { if (state.started) return if (!ready()) return + if (!settings.ready()) return if (!platform.version) return state.started = true @@ -149,6 +166,11 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple setFrom(previous) setTo(platform.version) + if (!settings.general.releaseNotes()) { + markSeen() + return + } + const fetcher = platform.fetch ?? fetch const controller = new AbortController() onCleanup(() => { @@ -182,10 +204,8 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple } const timer = setTimeout(() => { - dialog.show( - () => , - () => markSeen(), - ) + markSeen() + dialog.show(() => ) }, 500) setTimer(timer) }) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index d976cbc4960..67e907a6364 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -18,6 +18,7 @@ export interface SoundSettings { export interface Settings { general: { autoSave: boolean + releaseNotes: boolean } appearance: { fontSize: number @@ -34,6 +35,7 @@ export interface Settings { const defaultSettings: Settings = { general: { autoSave: true, + releaseNotes: true, }, appearance: { fontSize: 14, @@ -97,6 +99,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setAutoSave(value: boolean) { setStore("general", "autoSave", value) }, + releaseNotes: createMemo(() => store.general?.releaseNotes ?? defaultSettings.general.releaseNotes), + setReleaseNotes(value: boolean) { + setStore("general", "releaseNotes", value) + }, }, appearance: { fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize), diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index d368fff334d..bca08275f7c 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -525,6 +525,7 @@ export const dict = { "settings.general.section.appearance": "Appearance", "settings.general.section.notifications": "System notifications", + "settings.general.section.updates": "Updates", "settings.general.section.sounds": "Sound effects", "settings.general.row.language.title": "Language", @@ -535,6 +536,9 @@ export const dict = { "settings.general.row.theme.description": "Customise how OpenCode is themed.", "settings.general.row.font.title": "Font", "settings.general.row.font.description": "Customise the mono font used in code blocks", + + "settings.general.row.releaseNotes.title": "Release notes", + "settings.general.row.releaseNotes.description": "Show What's New popups after updates", "font.option.ibmPlexMono": "IBM Plex Mono", "font.option.cascadiaCode": "Cascadia Code", "font.option.firaCode": "Fira Code", From 45b09c14658d36f2107f16ad0c8bd31002a02cf0 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 26 Jan 2026 17:17:37 -0500 Subject: [PATCH 122/232] tweak: when using messages api for copilot, attach anthropic beta headers --- packages/opencode/src/plugin/copilot.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/opencode/src/plugin/copilot.ts b/packages/opencode/src/plugin/copilot.ts index 659090dd3a1..0be1345871f 100644 --- a/packages/opencode/src/plugin/copilot.ts +++ b/packages/opencode/src/plugin/copilot.ts @@ -277,6 +277,11 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { }, "chat.headers": async (input, output) => { if (!input.model.providerID.includes("github-copilot")) return + + if (input.model.api.npm === "@ai-sdk/anthropic") { + output.headers["anthropic-beta"] = "interleaved-thinking-2025-05-14" + } + const session = await sdk.session .get({ path: { From 77f11dfabe8fbe1bd696e03b17fbfa459384666b Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:30:15 -0600 Subject: [PATCH 123/232] chore: don't flip github draft release automatically --- script/publish-complete.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/script/publish-complete.ts b/script/publish-complete.ts index a3bdceae07d..4c7c7ac31c3 100755 --- a/script/publish-complete.ts +++ b/script/publish-complete.ts @@ -1,11 +1,11 @@ #!/usr/bin/env bun -import { Script } from "@opencode-ai/script" +// import { Script } from "@opencode-ai/script" import { $ } from "bun" -if (!Script.preview) { - await $`gh release edit v${Script.version} --draft=false` -} +// if (!Script.preview) { +// await $`gh release edit v${Script.version} --draft=false` +// } await $`bun install` From b07d7cdb71301b9b0381f1a94b5670f954a56d4c Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:40:57 -0600 Subject: [PATCH 124/232] fix(app): file tree performance --- packages/app/src/components/file-tree.tsx | 59 +++++++++++++++++------ 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index b7f8316d037..64988af5327 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -17,6 +17,11 @@ import { import { Dynamic } from "solid-js/web" import type { FileNode } from "@opencode-ai/sdk/v2" +type Filter = { + files: Set + dirs: Set +} + export default function FileTree(props: { path: string class?: string @@ -27,26 +32,19 @@ export default function FileTree(props: { draggable?: boolean tooltip?: boolean onFileClick?: (file: FileNode) => void + + _filter?: Filter + _marks?: Set + _deeps?: Map }) { const file = useFile() const level = props.level ?? 0 const draggable = () => props.draggable ?? true const tooltip = () => props.tooltip ?? true - const maxOpen = (dir: string, lvl: number): number => { - const expanded = file.tree.state(dir)?.expanded ?? false - if (!expanded) return -1 - - const nodes = file.tree.children(dir) - const child = nodes.reduce((max, node) => { - if (node.type !== "directory") return max - return Math.max(max, maxOpen(node.path, lvl + 1)) - }, -1) - - return Math.max(lvl, child) - } - const filter = createMemo(() => { + if (props._filter) return props._filter + const allowed = props.allowed if (!allowed) return @@ -66,11 +64,38 @@ export default function FileTree(props: { }) const marks = createMemo(() => { + if (props._marks) return props._marks + const modified = props.modified if (!modified || modified.length === 0) return return new Set(modified) }) + const deeps = createMemo(() => { + if (props._deeps) return props._deeps + + const out = new Map() + + const visit = (dir: string, lvl: number): number => { + const expanded = file.tree.state(dir)?.expanded ?? false + if (!expanded) return -1 + + const nodes = file.tree.children(dir) + const max = nodes.reduce((max, node) => { + if (node.type !== "directory") return max + const open = file.tree.state(node.path)?.expanded ?? false + if (!open) return max + return Math.max(max, visit(node.path, lvl + 1)) + }, lvl) + + out.set(dir, max) + return max + } + + visit(props.path, level - 1) + return out + }) + createEffect(() => { const current = filter() if (!current) return @@ -84,7 +109,8 @@ export default function FileTree(props: { }) createEffect(() => { - void file.tree.list(props.path) + const path = props.path + untrack(() => void file.tree.list(path)) }) const nodes = createMemo(() => { @@ -165,7 +191,7 @@ export default function FileTree(props: { {(node) => { const expanded = () => file.tree.state(node.path)?.expanded ?? false - const deep = createMemo(() => (node.type === "directory" ? maxOpen(node.path, level) : -1)) + const deep = () => deeps().get(node.path) ?? -1 const Wrapper = (p: ParentProps) => { if (!tooltip()) return p.children return ( @@ -212,6 +238,9 @@ export default function FileTree(props: { draggable={props.draggable} tooltip={props.tooltip} onFileClick={props.onFileClick} + _filter={filter()} + _marks={marks()} + _deeps={deeps()} /> From 021d9d105e1dc8d096390b1b392de642c033901c Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:46:01 -0600 Subject: [PATCH 125/232] fix(app): reactive file tree --- packages/app/src/context/file.tsx | 12 ++++++++++++ packages/opencode/src/tool/apply_patch.ts | 15 ++++++++------- packages/opencode/src/tool/edit.ts | 10 ++++++++++ packages/opencode/src/tool/write.ts | 5 +++++ 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 306bce922b6..805936cd830 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -571,6 +571,18 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ } const kind = event.properties.event + if (kind === "change") { + const dir = (() => { + if (path === "") return "" + const node = tree.node[path] + if (node?.type !== "directory") return + return path + })() + if (dir === undefined) return + if (!tree.dir[dir]?.loaded) return + listDir(dir, { force: true }) + return + } if (kind !== "add" && kind !== "unlink") return const parent = path.split("/").slice(0, -1).join("/") diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index 8028ee8de8c..1344467c719 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -185,7 +185,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", { }) // Apply the changes - const changedFiles: string[] = [] + const updates: Array<{ file: string; event: "add" | "change" | "unlink" }> = [] for (const change of fileChanges) { const edited = change.type === "delete" ? undefined : (change.movePath ?? change.filePath) @@ -194,12 +194,12 @@ export const ApplyPatchTool = Tool.define("apply_patch", { // Create parent directories (recursive: true is safe on existing/root dirs) await fs.mkdir(path.dirname(change.filePath), { recursive: true }) await fs.writeFile(change.filePath, change.newContent, "utf-8") - changedFiles.push(change.filePath) + updates.push({ file: change.filePath, event: "add" }) break case "update": await fs.writeFile(change.filePath, change.newContent, "utf-8") - changedFiles.push(change.filePath) + updates.push({ file: change.filePath, event: "change" }) break case "move": @@ -208,13 +208,14 @@ export const ApplyPatchTool = Tool.define("apply_patch", { await fs.mkdir(path.dirname(change.movePath), { recursive: true }) await fs.writeFile(change.movePath, change.newContent, "utf-8") await fs.unlink(change.filePath) - changedFiles.push(change.movePath) + updates.push({ file: change.filePath, event: "unlink" }) + updates.push({ file: change.movePath, event: "add" }) } break case "delete": await fs.unlink(change.filePath) - changedFiles.push(change.filePath) + updates.push({ file: change.filePath, event: "unlink" }) break } @@ -226,8 +227,8 @@ export const ApplyPatchTool = Tool.define("apply_patch", { } // Publish file change events - for (const filePath of changedFiles) { - await Bus.publish(FileWatcher.Event.Updated, { file: filePath, event: "change" }) + for (const update of updates) { + await Bus.publish(FileWatcher.Event.Updated, update) } // Notify LSP of file changes and collect diagnostics diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 26db5b22836..0bf1d6792bc 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -10,6 +10,7 @@ import { LSP } from "../lsp" import { createTwoFilesPatch, diffLines } from "diff" import DESCRIPTION from "./edit.txt" import { File } from "../file" +import { FileWatcher } from "../file/watcher" import { Bus } from "../bus" import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" @@ -48,6 +49,7 @@ export const EditTool = Tool.define("edit", { let contentNew = "" await FileTime.withLock(filePath, async () => { if (params.oldString === "") { + const existed = await Bun.file(filePath).exists() contentNew = params.newString diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) await ctx.ask({ @@ -63,6 +65,10 @@ export const EditTool = Tool.define("edit", { await Bus.publish(File.Event.Edited, { file: filePath, }) + await Bus.publish(FileWatcher.Event.Updated, { + file: filePath, + event: existed ? "change" : "add", + }) FileTime.read(ctx.sessionID, filePath) return } @@ -92,6 +98,10 @@ export const EditTool = Tool.define("edit", { await Bus.publish(File.Event.Edited, { file: filePath, }) + await Bus.publish(FileWatcher.Event.Updated, { + file: filePath, + event: "change", + }) contentNew = await file.text() diff = trimDiff( createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)), diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index cfcf6a0dab7..eca64d30374 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -6,6 +6,7 @@ import { createTwoFilesPatch } from "diff" import DESCRIPTION from "./write.txt" import { Bus } from "../bus" import { File } from "../file" +import { FileWatcher } from "../file/watcher" import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" @@ -45,6 +46,10 @@ export const WriteTool = Tool.define("write", { await Bus.publish(File.Event.Edited, { file: filepath, }) + await Bus.publish(FileWatcher.Event.Updated, { + file: filepath, + event: exists ? "change" : "add", + }) FileTime.read(ctx.sessionID, filepath) let output = "Wrote file successfully." From 4075f9e1ab9c806f4a5f2a7503547169645f45ac Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 26 Jan 2026 22:49:23 +0000 Subject: [PATCH 126/232] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 18 +++--- packages/sdk/openapi.json | 74 ++++++++++++------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9258bc0cde6..2a63d721215 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -627,6 +627,14 @@ export type EventSessionCompacted = { } } +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + export type Todo = { /** * Brief description of the task @@ -654,14 +662,6 @@ export type EventTodoUpdated = { } } -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - export type EventTuiPromptAppend = { type: "tui.prompt.append" properties: { @@ -903,8 +903,8 @@ export type Event = | EventQuestionReplied | EventQuestionRejected | EventSessionCompacted - | EventTodoUpdated | EventFileWatcherUpdated + | EventTodoUpdated | EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 8808bcf7d8c..cf2f29d8589 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7528,6 +7528,41 @@ }, "required": ["type", "properties"] }, + "Event.file.watcher.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file.watcher.updated" + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "event": { + "anyOf": [ + { + "type": "string", + "const": "add" + }, + { + "type": "string", + "const": "change" + }, + { + "type": "string", + "const": "unlink" + } + ] + } + }, + "required": ["file", "event"] + } + }, + "required": ["type", "properties"] + }, "Todo": { "type": "object", "properties": { @@ -7575,41 +7610,6 @@ }, "required": ["type", "properties"] }, - "Event.file.watcher.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file.watcher.updated" - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "anyOf": [ - { - "type": "string", - "const": "add" - }, - { - "type": "string", - "const": "change" - }, - { - "type": "string", - "const": "unlink" - } - ] - } - }, - "required": ["file", "event"] - } - }, - "required": ["type", "properties"] - }, "Event.tui.prompt.append": { "type": "object", "properties": { @@ -8276,10 +8276,10 @@ "$ref": "#/components/schemas/Event.session.compacted" }, { - "$ref": "#/components/schemas/Event.todo.updated" + "$ref": "#/components/schemas/Event.file.watcher.updated" }, { - "$ref": "#/components/schemas/Event.file.watcher.updated" + "$ref": "#/components/schemas/Event.todo.updated" }, { "$ref": "#/components/schemas/Event.tui.prompt.append" From bb178e93525b6ff1a69d00375571ba3b8cd86b84 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:56:35 -0600 Subject: [PATCH 127/232] chore: cleanup --- packages/app/src/context/file.tsx | 1 + packages/app/src/context/local.tsx | 244 +---------------------------- 2 files changed, 3 insertions(+), 242 deletions(-) diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 805936cd830..962af19f0ab 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -561,6 +561,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ const stop = sdk.event.listen((e) => { const event = e.details + console.log(event) if (event.type !== "file.watcher.updated") return const path = normalize(event.properties.file) if (!path) return diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 4b27e6d37ab..5f5d51dbdd9 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,5 +1,5 @@ -import { createStore, produce, reconcile } from "solid-js/store" -import { batch, createEffect, createMemo, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" +import { batch, createMemo } from "solid-js" import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "@opencode-ai/ui/context" import { useSDK } from "./sdk" @@ -7,8 +7,6 @@ import { useSync } from "./sync" import { base64Encode } from "@opencode-ai/util/encode" import { useProviders } from "@/hooks/use-providers" import { useModels } from "@/context/models" -import { showToast } from "@opencode-ai/ui/toast" -import { useLanguage } from "@/context/language" export type LocalFile = FileNode & Partial<{ @@ -41,7 +39,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const sdk = useSDK() const sync = useSync() const providers = useProviders() - const language = useLanguage() function isModelValid(model: ModelKey) { const provider = providers.all().find((x) => x.id === model.providerID) @@ -246,247 +243,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } })() - const file = (() => { - const [store, setStore] = createStore<{ - node: Record - }>({ - node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])), - }) - - const scope = createMemo(() => sdk.directory) - createEffect(() => { - scope() - setStore("node", {}) - }) - - // const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path))) - // const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b))) - - // createEffect((prev: FileStatus[]) => { - // const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path)) - // for (const p of removed) { - // setStore( - // "node", - // p.path, - // produce((draft) => { - // draft.status = undefined - // draft.view = "raw" - // }), - // ) - // load(p.path) - // } - // for (const p of sync.data.changes) { - // if (store.node[p.path] === undefined) { - // fetch(p.path).then(() => { - // if (store.node[p.path] === undefined) return - // setStore("node", p.path, "status", p) - // }) - // } else { - // setStore("node", p.path, "status", p) - // } - // } - // return sync.data.changes - // }, sync.data.changes) - - // const changed = (path: string) => { - // const node = store.node[path] - // if (node?.status) return true - // const set = changeset() - // if (set.has(path)) return true - // for (const p of set) { - // if (p.startsWith(path ? path + "/" : "")) return true - // } - // return false - // } - - // const resetNode = (path: string) => { - // setStore("node", path, { - // loaded: undefined, - // pinned: undefined, - // content: undefined, - // selection: undefined, - // scrollTop: undefined, - // folded: undefined, - // view: undefined, - // selectedChange: undefined, - // }) - // } - - const relative = (path: string) => path.replace(sync.data.path.directory + "/", "") - - const load = async (path: string) => { - const directory = scope() - const client = sdk.client - const relativePath = relative(path) - await client.file - .read({ path: relativePath }) - .then((x) => { - if (scope() !== directory) return - if (!store.node[relativePath]) return - setStore( - "node", - relativePath, - produce((draft) => { - draft.loaded = true - draft.content = x.data - }), - ) - }) - .catch((e) => { - if (scope() !== directory) return - showToast({ - variant: "error", - title: language.t("toast.file.loadFailed.title"), - description: e.message, - }) - }) - } - - const fetch = async (path: string) => { - const relativePath = relative(path) - const parent = relativePath.split("/").slice(0, -1).join("/") - if (parent) { - await list(parent) - } - } - - const init = async (path: string) => { - const relativePath = relative(path) - if (!store.node[relativePath]) await fetch(path) - if (store.node[relativePath]?.loaded) return - return load(relativePath) - } - - const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => { - const relativePath = relative(path) - if (!store.node[relativePath]) await fetch(path) - // setStore("opened", (x) => { - // if (x.includes(relativePath)) return x - // return [ - // ...opened() - // .filter((x) => x.pinned) - // .map((x) => x.path), - // relativePath, - // ] - // }) - // setStore("active", relativePath) - // context.addActive() - if (options?.pinned) setStore("node", path, "pinned", true) - if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view) - if (store.node[relativePath]?.loaded) return - return load(relativePath) - } - - const list = async (path: string) => { - const directory = scope() - const client = sdk.client - return client.file - .list({ path: path + "/" }) - .then((x) => { - if (scope() !== directory) return - setStore( - "node", - produce((draft) => { - x.data!.forEach((node) => { - if (node.path in draft) return - draft[node.path] = node - }) - }), - ) - }) - .catch(() => {}) - } - - const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!) - const searchFilesAndDirectories = (query: string) => - sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!) - - const unsub = sdk.event.listen((e) => { - const event = e.details - switch (event.type) { - case "file.watcher.updated": - const relativePath = relative(event.properties.file) - if (relativePath.startsWith(".git/")) return - if (store.node[relativePath]) load(relativePath) - break - } - }) - onCleanup(unsub) - - return { - node: async (path: string) => { - if (!store.node[path] || !store.node[path].loaded) { - await init(path) - } - return store.node[path] - }, - update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)), - open, - load, - init, - expand(path: string) { - setStore("node", path, "expanded", true) - if (store.node[path]?.loaded) return - setStore("node", path, "loaded", true) - list(path) - }, - collapse(path: string) { - setStore("node", path, "expanded", false) - }, - select(path: string, selection: TextSelection | undefined) { - setStore("node", path, "selection", selection) - }, - scroll(path: string, scrollTop: number) { - setStore("node", path, "scrollTop", scrollTop) - }, - view(path: string): View { - const n = store.node[path] - return n && n.view ? n.view : "raw" - }, - setView(path: string, view: View) { - setStore("node", path, "view", view) - }, - unfold(path: string, key: string) { - setStore("node", path, "folded", (xs) => { - const a = xs ?? [] - if (a.includes(key)) return a - return [...a, key] - }) - }, - fold(path: string, key: string) { - setStore("node", path, "folded", (xs) => (xs ?? []).filter((k) => k !== key)) - }, - folded(path: string) { - const n = store.node[path] - return n && n.folded ? n.folded : [] - }, - changeIndex(path: string) { - return store.node[path]?.selectedChange - }, - setChangeIndex(path: string, index: number | undefined) { - setStore("node", path, "selectedChange", index) - }, - // changes, - // changed, - children(path: string) { - return Object.values(store.node).filter( - (x) => - x.path.startsWith(path) && - x.path !== path && - !x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"), - ) - }, - searchFiles, - searchFilesAndDirectories, - relative, - } - })() - const result = { slug: createMemo(() => base64Encode(sdk.directory)), model, agent, - file, } return result }, From 36577479c57e0a3dd30f0e8e90f0c596f79550f1 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:57:36 -0600 Subject: [PATCH 128/232] fix(app): enable file watcher --- packages/desktop/src-tauri/src/cli.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index b019d66b5c8..f64beed6a1a 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -157,6 +157,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command { .unwrap() .args(args.split_whitespace()) .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") + .env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true") .env("OPENCODE_CLIENT", "desktop") .env("XDG_STATE_HOME", &state_dir); @@ -174,6 +175,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command { app.shell() .command(&shell) .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") + .env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true") .env("OPENCODE_CLIENT", "desktop") .env("XDG_STATE_HOME", &state_dir) .args(["-il", "-c", &cmd]) From 8371ba5aecd995f04653f21f02aff81178b16557 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:01:04 -0600 Subject: [PATCH 129/232] chore: cleanup --- packages/app/src/context/local.tsx | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 5f5d51dbdd9..f51bb693092 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,6 +1,5 @@ import { createStore } from "solid-js/store" import { batch, createMemo } from "solid-js" -import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "@opencode-ai/ui/context" import { useSDK } from "./sdk" import { useSync } from "./sync" @@ -8,31 +7,8 @@ import { base64Encode } from "@opencode-ai/util/encode" import { useProviders } from "@/hooks/use-providers" import { useModels } from "@/context/models" -export type LocalFile = FileNode & - Partial<{ - loaded: boolean - pinned: boolean - expanded: boolean - content: FileContent - selection: { startLine: number; startChar: number; endLine: number; endChar: number } - scrollTop: number - view: "raw" | "diff-unified" | "diff-split" - folded: string[] - selectedChange: number - status: FileStatus - }> -export type TextSelection = LocalFile["selection"] -export type View = LocalFile["view"] - -export type LocalModel = Omit & { - provider: Provider - latest?: boolean -} export type ModelKey = { providerID: string; modelID: string } -export type FileContext = { type: "file"; path: string; selection?: TextSelection } -export type ContextItem = FileContext - export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", init: () => { From 6897bb7d02a67c516687a046c354c7faea983cc2 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:02:56 -0600 Subject: [PATCH 130/232] chore: cleanup --- packages/app/src/context/file.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 962af19f0ab..805936cd830 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -561,7 +561,6 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ const stop = sdk.event.listen((e) => { const event = e.details - console.log(event) if (event.type !== "file.watcher.updated") return const path = normalize(event.properties.file) if (!path) return From b24fd90fe8a3433333646f61be6ca882b55d1155 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:42:23 -0600 Subject: [PATCH 131/232] test(app): file tree spec --- packages/app/e2e/file-tree.spec.ts | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 packages/app/e2e/file-tree.spec.ts diff --git a/packages/app/e2e/file-tree.spec.ts b/packages/app/e2e/file-tree.spec.ts new file mode 100644 index 00000000000..12ea7a081fc --- /dev/null +++ b/packages/app/e2e/file-tree.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from "./fixtures" + +test("file tree can expand folders and open a file", async ({ page, gotoSession }) => { + await gotoSession() + + await page.getByRole("button", { name: "Toggle file tree" }).click() + + const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]') + await expect(treeTabs).toBeVisible() + + await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click() + + const node = (name: string) => treeTabs.getByRole("button", { name, exact: true }) + + await expect(node("packages")).toBeVisible() + await node("packages").click() + + await expect(node("app")).toBeVisible() + await node("app").click() + + await expect(node("src")).toBeVisible() + await node("src").click() + + await expect(node("components")).toBeVisible() + await node("components").click() + + await expect(node("file-tree.tsx")).toBeVisible() + await node("file-tree.tsx").click() + + const tab = page.getByRole("tab", { name: "file-tree.tsx" }) + await expect(tab).toBeVisible() + await tab.click() + + const code = page.locator('[data-component="code"]').first() + await expect(code.getByText("export default function FileTree")).toBeVisible() +}) From bf463aee04bee55a1bf616be1ab1d14ed48f105f Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Mon, 26 Jan 2026 19:37:54 -0500 Subject: [PATCH 132/232] feat(release): add highlights template to draft releases (#10745) --- script/publish-start.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/script/publish-start.ts b/script/publish-start.ts index 5d1b2fb6abf..644790f9dc7 100755 --- a/script/publish-start.ts +++ b/script/publish-start.ts @@ -4,6 +4,35 @@ import { $ } from "bun" import { Script } from "@opencode-ai/script" import { buildNotes, getLatestRelease } from "./changelog" +const highlightsTemplate = `## Highlights + + + + + +` + let notes: string[] = [] console.log("=== publishing ===\n") @@ -11,6 +40,7 @@ console.log("=== publishing ===\n") if (!Script.preview) { const previous = await getLatestRelease() notes = await buildNotes(previous, "HEAD") + notes.unshift(highlightsTemplate) } const pkgjsons = await Array.fromAsync( From 64a3661a325e8331f767145971c1cce9f7257b00 Mon Sep 17 00:00:00 2001 From: Fabio Martino Date: Tue, 27 Jan 2026 01:45:05 +0100 Subject: [PATCH 133/232] docs: add Italian README (#10732) --- README.ar.md | 1 + README.br.md | 1 + README.da.md | 1 + README.de.md | 1 + README.es.md | 1 + README.fr.md | 1 + README.it.md | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.ja.md | 1 + README.ko.md | 1 + README.md | 1 + README.no.md | 1 + README.pl.md | 1 + README.ru.md | 1 + README.zh.md | 1 + README.zht.md | 1 + 15 files changed, 148 insertions(+) create mode 100644 README.it.md diff --git a/README.ar.md b/README.ar.md index 9ce00c67aa8..2abceb300d2 100644 --- a/README.ar.md +++ b/README.ar.md @@ -22,6 +22,7 @@
Deutsch |
Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.br.md b/README.br.md index efa7a9ea743..6a58241c98e 100644 --- a/README.br.md +++ b/README.br.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.da.md b/README.da.md index a3cc7c4146d..7e7dda42a84 100644 --- a/README.da.md +++ b/README.da.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.de.md b/README.de.md index 189171ea3c4..c949dd00f42 100644 --- a/README.de.md +++ b/README.de.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.es.md b/README.es.md index d6530b1dd08..3e3797ed301 100644 --- a/README.es.md +++ b/README.es.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.fr.md b/README.fr.md index 520ed3a061c..00133b1e9fe 100644 --- a/README.fr.md +++ b/README.fr.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.it.md b/README.it.md new file mode 100644 index 00000000000..f3c92c25495 --- /dev/null +++ b/README.it.md @@ -0,0 +1,134 @@ +

+ + + + + Logo OpenCode + + +

+

L’agente di coding AI open source.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + العربية | + Norsk | + Português (Brasil) +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installazione + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Package manager +npm i -g opencode-ai@latest # oppure bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS e Linux (consigliato, sempre aggiornato) +brew install opencode # macOS e Linux (formula brew ufficiale, aggiornata meno spesso) +paru -S opencode-bin # Arch Linux +mise use -g opencode # Qualsiasi OS +nix run nixpkgs#opencode # oppure github:anomalyco/opencode per l’ultima branch di sviluppo +``` + +> [!TIP] +> Rimuovi le versioni precedenti alla 0.1.x prima di installare. + +### App Desktop (BETA) + +OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla direttamente dalla [pagina delle release](https://github.com/anomalyco/opencode/releases) oppure da [opencode.ai/download](https://opencode.ai/download). + +| Piattaforma | Download | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, oppure AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Directory di installazione + +Lo script di installazione rispetta il seguente ordine di priorità per il percorso di installazione: + +1. `$OPENCODE_INSTALL_DIR` – Directory di installazione personalizzata +2. `$XDG_BIN_DIR` – Percorso conforme alla XDG Base Directory Specification +3. `$HOME/bin` – Directory binaria standard dell’utente (se esiste o può essere creata) +4. `$HOME/.opencode/bin` – Fallback predefinito + +```bash +# Esempi +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agenti + +OpenCode include due agenti integrati tra cui puoi passare usando il tasto `Tab`. + +- **build** – Predefinito, agente con accesso completo per il lavoro di sviluppo +- **plan** – Agente in sola lettura per analisi ed esplorazione del codice + + - Nega le modifiche ai file per impostazione predefinita + - Chiede il permesso prima di eseguire comandi bash + - Ideale per esplorare codebase sconosciute o pianificare modifiche + +È inoltre incluso un sotto-agente **general** per ricerche complesse e attività multi-step. +Viene utilizzato internamente e può essere invocato usando `@general` nei messaggi. + +Scopri di più sugli [agenti](https://opencode.ai/docs/agents). + +### Documentazione + +Per maggiori informazioni su come configurare OpenCode, [**consulta la nostra documentazione**](https://opencode.ai/docs). + +### Contribuire + +Se sei interessato a contribuire a OpenCode, leggi la nostra [guida alla contribuzione](./CONTRIBUTING.md) prima di inviare una pull request. + +### Costruire su OpenCode + +Se stai lavorando a un progetto correlato a OpenCode e che utilizza “opencode” come parte del nome (ad esempio “opencode-dashboard” o “opencode-mobile”), aggiungi una nota nel tuo README per chiarire che non è sviluppato dal team OpenCode e che non è affiliato in alcun modo con noi. + +### FAQ + +#### In cosa è diverso da Claude Code? + +È molto simile a Claude Code in termini di funzionalità. Ecco le principali differenze: + +- 100% open source +- Non è legato a nessun provider. Anche se consigliamo i modelli forniti tramite [OpenCode Zen](https://opencode.ai/zen), OpenCode può essere utilizzato con Claude, OpenAI, Google o persino modelli locali. Con l’evoluzione dei modelli, le differenze tra di essi si ridurranno e i prezzi scenderanno, quindi essere indipendenti dal provider è importante. +- Supporto LSP pronto all’uso +- Forte attenzione alla TUI. OpenCode è sviluppato da utenti neovim e dai creatori di [terminal.shop](https://terminal.shop); spingeremo al limite ciò che è possibile fare nel terminale. +- Architettura client/server. Questo, ad esempio, permette a OpenCode di girare sul tuo computer mentre lo controlli da remoto tramite un’app mobile. La frontend TUI è quindi solo uno dei possibili client. + +--- + +**Unisciti alla nostra community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.ja.md b/README.ja.md index 271bcc4e1f2..5f3a9e189ec 100644 --- a/README.ja.md +++ b/README.ja.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.ko.md b/README.ko.md index a5e299400f4..213f46bfe7c 100644 --- a/README.ko.md +++ b/README.ko.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.md b/README.md index 1ee5f26975e..d0acb758d9f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.no.md b/README.no.md index 43f23b88947..44371df5ed8 100644 --- a/README.no.md +++ b/README.no.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.pl.md b/README.pl.md index 6465879b15d..b183cd62455 100644 --- a/README.pl.md +++ b/README.pl.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.ru.md b/README.ru.md index 5ace29b849e..c192036b54d 100644 --- a/README.ru.md +++ b/README.ru.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.zh.md b/README.zh.md index 9908e8ffb18..9ebbe8ce934 100644 --- a/README.zh.md +++ b/README.zh.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | diff --git a/README.zht.md b/README.zht.md index e06d681aa80..298b5b35acf 100644 --- a/README.zht.md +++ b/README.zht.md @@ -22,6 +22,7 @@ Deutsch | Español | Français | + Italiano | Dansk | 日本語 | Polski | From e7c6267323816d93c149a541dfe4f8b1e968ca51 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 27 Jan 2026 00:45:43 +0000 Subject: [PATCH 134/232] chore: generate --- README.it.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.it.md b/README.it.md index f3c92c25495..89692a36687 100644 --- a/README.it.md +++ b/README.it.md @@ -95,7 +95,6 @@ OpenCode include due agenti integrati tra cui puoi passare usando il tasto `Tab` - **build** – Predefinito, agente con accesso completo per il lavoro di sviluppo - **plan** – Agente in sola lettura per analisi ed esplorazione del codice - - Nega le modifiche ai file per impostazione predefinita - Chiede il permesso prima di eseguire comandi bash - Ideale per esplorare codebase sconosciute o pianificare modifiche From 7655f51e101b318efe0e6a23e05052b344348a90 Mon Sep 17 00:00:00 2001 From: Rahul A Mistry <149420892+ProdigyRahul@users.noreply.github.com> Date: Tue, 27 Jan 2026 06:16:04 +0530 Subject: [PATCH 135/232] fix(app): add connect provier in model selector (#10706) --- .../src/components/dialog-select-model.tsx | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 2ee1d9db1a4..c5a930a435a 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -110,6 +110,11 @@ export function ModelSelectorPopover(props: { setStore("open", false) dialog.show(() => ) } + + const handleConnectProvider = () => { + setStore("open", false) + dialog.show(() => ) + } const language = useLanguage() createEffect(() => { @@ -207,15 +212,26 @@ export function ModelSelectorPopover(props: { onSelect={() => setStore("open", false)} class="p-1" action={ - +
+ + +
} /> From d9e8b2b65d9fba4b54cb6a7f7d677162335c8e3f Mon Sep 17 00:00:00 2001 From: Chris Yang <18487241+ysm-dev@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:50:51 -0700 Subject: [PATCH 136/232] fix(desktop): disable magnification gestures on macOS (#10605) --- packages/desktop/src-tauri/Cargo.lock | 2 ++ packages/desktop/src-tauri/Cargo.toml | 4 ++++ packages/desktop/src-tauri/src/window_customizer.rs | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index a41739a697b..294d7ad6ce5 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -3028,6 +3028,8 @@ dependencies = [ "futures", "gtk", "listeners", + "objc2 0.6.3", + "objc2-web-kit", "reqwest", "semver", "serde", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 6296b8325fb..b875f928b0b 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -47,6 +47,10 @@ comrak = { version = "0.50", default-features = false } gtk = "0.18.2" webkit2gtk = "=2.0.1" +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6" +objc2-web-kit = "0.3" + [target.'cfg(windows)'.dependencies] windows = { version = "0.61", features = [ "Win32_Foundation", diff --git a/packages/desktop/src-tauri/src/window_customizer.rs b/packages/desktop/src-tauri/src/window_customizer.rs index cd42fd0299c..682f57f2471 100644 --- a/packages/desktop/src-tauri/src/window_customizer.rs +++ b/packages/desktop/src-tauri/src/window_customizer.rs @@ -29,6 +29,18 @@ impl Plugin for PinchZoomDisablePlugin { gobject_ffi::g_signal_handlers_destroy(data.as_ptr().cast()); } } + + #[cfg(target_os = "macos")] + unsafe { + use objc2::rc::Retained; + use objc2_web_kit::WKWebView; + + // Get the WKWebView pointer and disable magnification gestures + // This prevents Cmd+Ctrl+scroll and pinch-to-zoom from changing the zoom level + let wk_webview: Retained = + Retained::retain(_webview.inner().cast()).unwrap(); + wk_webview.setAllowsMagnification(false); + } }); } } From b59aec6f043144963994809f667d034496e164e4 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Mon, 26 Jan 2026 20:50:44 -0500 Subject: [PATCH 137/232] feat: add /learn command to extract session learnings to scoped AGENTS.md files (#10717) --- .opencode/command/learn.md | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .opencode/command/learn.md diff --git a/.opencode/command/learn.md b/.opencode/command/learn.md new file mode 100644 index 00000000000..fe4965a5887 --- /dev/null +++ b/.opencode/command/learn.md @@ -0,0 +1,42 @@ +--- +description: Extract non-obvious learnings from session to AGENTS.md files to build codebase understanding +--- + +Analyze this session and extract non-obvious learnings to add to AGENTS.md files. + +AGENTS.md files can exist at any directory level, not just the project root. When an agent reads a file, any AGENTS.md in parent directories are automatically loaded into the context of the tool read. Place learnings as close to the relevant code as possible: + +- Project-wide learnings → root AGENTS.md +- Package/module-specific → packages/foo/AGENTS.md +- Feature-specific → src/auth/AGENTS.md + +What counts as a learning (non-obvious discoveries only): + +- Hidden relationships between files or modules +- Execution paths that differ from how code appears +- Non-obvious configuration, env vars, or flags +- Debugging breakthroughs when error messages were misleading +- API/tool quirks and workarounds +- Build/test commands not in README +- Architectural decisions and constraints +- Files that must change together + +What NOT to include: + +- Obvious facts from documentation +- Standard language/framework behavior +- Things already in an AGENTS.md +- Verbose explanations +- Session-specific details + +Process: + +1. Review session for discoveries, errors that took multiple attempts, unexpected connections +2. Determine scope - what directory does each learning apply to? +3. Read existing AGENTS.md files at relevant levels +4. Create or update AGENTS.md at the appropriate level +5. Keep entries to 1-3 lines per insight + +After updating, summarize which AGENTS.md files were created/updated and how many learnings per file. + +$ARGUMENTS From 6cf2c3e3db629757bcace9528b0c84f107bf73a7 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 26 Jan 2026 20:57:42 -0500 Subject: [PATCH 138/232] fix: use Instance.directory instead of process.cwd() in read tool --- packages/opencode/bunfig.toml | 2 -- packages/opencode/src/tool/read.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/bunfig.toml b/packages/opencode/bunfig.toml index c227328d5ae..db64a09a988 100644 --- a/packages/opencode/bunfig.toml +++ b/packages/opencode/bunfig.toml @@ -3,5 +3,3 @@ preload = ["@opentui/solid/preload"] [test] preload = ["./test/preload.ts"] timeout = 10000 # 10 seconds (default is 5000ms) -# Enable code coverage -coverage = true diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 028a007ccc0..93c1b751ffc 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -24,7 +24,7 @@ export const ReadTool = Tool.define("read", { async execute(params, ctx) { let filepath = params.filePath if (!path.isAbsolute(filepath)) { - filepath = path.join(process.cwd(), filepath) + filepath = path.join(Instance.directory, filepath) } const title = path.relative(Instance.worktree, filepath) From a8c18dba8205d7707a46ec0859db672370be8963 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:57:34 -0600 Subject: [PATCH 139/232] fix(core): expose Instance.directory to custom tools --- packages/opencode/src/tool/read.ts | 2 +- packages/opencode/src/tool/registry.ts | 5 +++-- packages/plugin/src/tool.ts | 5 +++++ packages/web/src/content/docs/custom-tools.mdx | 10 ++++++---- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 93c1b751ffc..746e0b173c8 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -24,7 +24,7 @@ export const ReadTool = Tool.define("read", { async execute(params, ctx) { let filepath = params.filePath if (!path.isAbsolute(filepath)) { - filepath = path.join(Instance.directory, filepath) + filepath = path.resolve(Instance.directory, filepath) } const title = path.relative(Instance.worktree, filepath) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index faa5f72bcce..2c862dfd886 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -16,7 +16,7 @@ import { Tool } from "./tool" import { Instance } from "../project/instance" import { Config } from "../config/config" import path from "path" -import { type ToolDefinition } from "@opencode-ai/plugin" +import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin" import z from "zod" import { Plugin } from "../plugin" import { WebSearchTool } from "./websearch" @@ -67,7 +67,8 @@ export namespace ToolRegistry { parameters: z.object(def.args), description: def.description, execute: async (args, ctx) => { - const result = await def.execute(args as any, ctx) + const pluginCtx = { ...ctx, directory: Instance.directory } as unknown as PluginToolContext + const result = await def.execute(args as any, pluginCtx) const out = await Truncate.output(result, {}, initCtx?.agent) return { title: "", diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index f759c07d2b5..6b4f7f1d1ef 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -4,6 +4,11 @@ export type ToolContext = { sessionID: string messageID: string agent: string + /** + * Current project directory for this session. + * Prefer this over process.cwd() when resolving relative paths. + */ + directory: string abort: AbortSignal metadata(input: { title?: string; metadata?: { [key: string]: any } }): void ask(input: AskInput): Promise diff --git a/packages/web/src/content/docs/custom-tools.mdx b/packages/web/src/content/docs/custom-tools.mdx index e089a035b4b..ef93a64e2ca 100644 --- a/packages/web/src/content/docs/custom-tools.mdx +++ b/packages/web/src/content/docs/custom-tools.mdx @@ -120,8 +120,8 @@ export default tool({ args: {}, async execute(args, context) { // Access context information - const { agent, sessionID, messageID } = context - return `Agent: ${agent}, Session: ${sessionID}, Message: ${messageID}` + const { agent, sessionID, messageID, directory } = context + return `Agent: ${agent}, Session: ${sessionID}, Message: ${messageID}, Directory: ${directory}` }, }) ``` @@ -148,6 +148,7 @@ Then create the tool definition that invokes it: ```ts title=".opencode/tools/python-add.ts" {10} import { tool } from "@opencode-ai/plugin" +import path from "path" export default tool({ description: "Add two numbers using Python", @@ -155,8 +156,9 @@ export default tool({ a: tool.schema.number().describe("First number"), b: tool.schema.number().describe("Second number"), }, - async execute(args) { - const result = await Bun.$`python3 .opencode/tools/add.py ${args.a} ${args.b}`.text() + async execute(args, context) { + const script = path.join(context.directory, ".opencode/tools/add.py") + const result = await Bun.$`python3 ${script} ${args.a} ${args.b}`.text() return result.trim() }, }) From 213c0e18ab6620cff4671e409b634b46a35e9471 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:01:16 -0600 Subject: [PATCH 140/232] fix(app): only show files in select dialog when clicking + tab --- .../app/src/components/dialog-select-file.tsx | 62 ++++++++++++++++--- packages/app/src/pages/session.tsx | 2 +- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 8ba1ce61829..83baa7a5e25 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -24,13 +24,16 @@ type Entry = { path?: string } -export function DialogSelectFile() { +type DialogSelectFileMode = "all" | "files" + +export function DialogSelectFile(props: { mode?: DialogSelectFileMode }) { const command = useCommand() const language = useLanguage() const layout = useLayout() const file = useFile() const dialog = useDialog() const params = useParams() + const filesOnly = () => props.mode === "files" const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) const view = createMemo(() => layout.view(sessionKey)) @@ -46,11 +49,12 @@ export function DialogSelectFile() { ] const limit = 5 - const allowed = createMemo(() => - command.options.filter( + const allowed = createMemo(() => { + if (filesOnly()) return [] + return command.options.filter( (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open", - ), - ) + ) + }) const commandItem = (option: CommandOption): Entry => ({ id: "command:" + option.id, @@ -99,10 +103,50 @@ export function DialogSelectFile() { return items.slice(0, limit) }) - const items = async (filter: string) => { - const query = filter.trim() + const root = createMemo(() => { + const nodes = file.tree.children("") + const paths = nodes + .filter((node) => node.type === "file") + .map((node) => node.path) + .sort((a, b) => a.localeCompare(b)) + return paths.slice(0, limit).map(fileItem) + }) + + const unique = (items: Entry[]) => { + const seen = new Set() + const out: Entry[] = [] + for (const item of items) { + if (seen.has(item.id)) continue + seen.add(item.id) + out.push(item) + } + return out + } + + const items = async (text: string) => { + const query = text.trim() setGrouped(query.length > 0) + + if (!query && filesOnly()) { + const loaded = file.tree.state("")?.loaded + const pending = loaded ? Promise.resolve() : file.tree.list("") + const next = unique([...recent(), ...root()]) + + if (loaded || next.length > 0) { + void pending + return next + } + + await pending + return unique([...recent(), ...root()]) + } + if (!query) return [...picks(), ...recent()] + + if (filesOnly()) { + const files = await file.searchFiles(query) + return files.map(fileItem) + } const files = await file.searchFiles(query) const entries = files.map(fileItem) return [...list(), ...entries] @@ -146,7 +190,9 @@ export function DialogSelectFile() { dialog.show(() => )} + onClick={() => dialog.show(() => )} aria-label={language.t("command.file.open")} /> From dd1624e30e00c7931e03d337b473bc16429f34a5 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 27 Jan 2026 17:48:43 +0800 Subject: [PATCH 141/232] desktop: deduplicate tauri configs --- .../desktop/src-tauri/tauri.prod.conf.json | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/desktop/src-tauri/tauri.prod.conf.json b/packages/desktop/src-tauri/tauri.prod.conf.json index 282db5b26c6..7ce4c78420e 100644 --- a/packages/desktop/src-tauri/tauri.prod.conf.json +++ b/packages/desktop/src-tauri/tauri.prod.conf.json @@ -2,27 +2,6 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCode", "identifier": "ai.opencode.desktop", - "app": { - "windows": [ - { - "label": "main", - "create": false, - "title": "OpenCode", - "url": "/", - "decorations": true, - "dragDropEnabled": false, - "zoomHotkeysEnabled": true, - "titleBarStyle": "Overlay", - "hiddenTitle": true, - "trafficLightPosition": { "x": 12.0, "y": 18.0 } - } - ], - "withGlobalTauri": true, - "security": { - "csp": null - }, - "macOSPrivateApi": true - }, "bundle": { "createUpdaterArtifacts": true, "icon": [ From ddffb34b99da90a40e2fdd28bff8221a2016b682 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 27 Jan 2026 12:05:48 +0000 Subject: [PATCH 142/232] ignore: update download stats 2026-01-27 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 4aa9a37b827..f00ebd72a3a 100644 --- a/STATS.md +++ b/STATS.md @@ -212,3 +212,4 @@ | 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) | | 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) | | 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) | +| 2026-01-27 | 7,208,093 (+266,473) | 2,280,762 (+48,647) | 9,488,855 (+315,120) | From b6565c606e4c3044fcbf98d1cce1b15537b035c0 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:30:49 -0600 Subject: [PATCH 143/232] fix(app): auto-scroll button sometimes sticks --- packages/app/src/pages/session.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 871d3b753d3..39f5b057ea0 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1703,9 +1703,9 @@ export default function Page() { markScrollGesture(e.target) }} onScroll={(e) => { + autoScroll.handleScroll() if (!hasScrollGesture()) return markScrollGesture(e.target) - autoScroll.handleScroll() if (isDesktop()) scheduleScrollSpy(e.currentTarget) }} onClick={autoScroll.handleInteraction} From c0a5f853497de6778f1430b691e393caa54c59a7 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:59:32 -0600 Subject: [PATCH 144/232] chore(app): missing tooltips --- .../src/components/dialog-select-model.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index c5a930a435a..588527386ee 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -213,24 +213,26 @@ export function ModelSelectorPopover(props: { class="p-1" action={
- - + + + + + +
} /> From 58b9b54600f12ba4ec1d80a2d1b7dee3879a0479 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:58:29 -0600 Subject: [PATCH 145/232] feat(app): forward and back buttons --- packages/app/src/components/titlebar.tsx | 135 ++++++++++++++++++----- packages/app/src/context/platform.tsx | 6 + packages/app/src/entry.tsx | 6 + packages/app/src/i18n/en.ts | 1 + packages/desktop/src/index.tsx | 8 ++ packages/ui/src/components/icon.tsx | 1 + 6 files changed, 130 insertions(+), 27 deletions(-) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 159216a4f72..1c825606912 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -1,8 +1,10 @@ -import { createEffect, createMemo, Show } from "solid-js" +import { createEffect, createMemo, Show, untrack } from "solid-js" +import { createStore } from "solid-js/store" +import { useLocation, useNavigate } from "@solidjs/router" import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon } from "@opencode-ai/ui/icon" import { Button } from "@opencode-ai/ui/button" -import { TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { useTheme } from "@opencode-ai/ui/theme" import { useLayout } from "@/context/layout" @@ -16,11 +18,68 @@ export function Titlebar() { const command = useCommand() const language = useLanguage() const theme = useTheme() + const navigate = useNavigate() + const location = useLocation() const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows") const web = createMemo(() => platform.platform === "web") + const [history, setHistory] = createStore({ + stack: [] as string[], + index: 0, + action: undefined as "back" | "forward" | undefined, + }) + + const path = () => `${location.pathname}${location.search}${location.hash}` + + createEffect(() => { + const current = path() + + untrack(() => { + if (!history.stack.length) { + const stack = current === "/" ? ["/"] : ["/", current] + setHistory({ stack, index: stack.length - 1 }) + return + } + + const active = history.stack[history.index] + if (current === active) { + if (history.action) setHistory("action", undefined) + return + } + + if (history.action) { + setHistory("action", undefined) + return + } + + const next = history.stack.slice(0, history.index + 1).concat(current) + setHistory({ stack: next, index: next.length - 1 }) + }) + }) + + const canBack = createMemo(() => history.index > 0) + const canForward = createMemo(() => history.index < history.stack.length - 1) + + const back = () => { + if (!canBack()) return + const index = history.index - 1 + const to = history.stack[index] + if (!to) return + setHistory({ index, action: "back" }) + navigate(to) + } + + const forward = () => { + if (!canForward()) return + const index = history.index + 1 + const to = history.stack[index] + if (!to) return + setHistory({ index, action: "forward" }) + navigate(to) + } + const getWin = () => { if (platform.platform !== "desktop") return @@ -106,34 +165,56 @@ export function Titlebar() { />
- - + + - - + +
+
+ /** Navigate back in history */ + back(): void + + /** Navigate forward in history */ + forward(): void + /** Send a system notification (optional deep link) */ notify(title: string, description?: string, href?: string): Promise diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 7fe03bb6af2..aa52fa1e7cb 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -31,6 +31,12 @@ const platform: Platform = { openLink(url: string) { window.open(url, "_blank") }, + back() { + window.history.back() + }, + forward() { + window.history.forward() + }, restart: async () => { window.location.reload() }, diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index bca08275f7c..abbe497dcfe 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -167,6 +167,7 @@ export const dict = { "common.search.placeholder": "Search", "common.goBack": "Go back", + "common.goForward": "Go forward", "common.loading": "Loading", "common.loading.ellipsis": "...", "common.cancel": "Cancel", diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index fe9e3f92e24..b19adfeda5a 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -80,6 +80,14 @@ const createPlatform = (password: Accessor): Platform => ({ void shellOpen(url).catch(() => undefined) }, + back() { + window.history.back() + }, + + forward() { + window.history.forward() + }, + storage: (() => { type StoreLike = { get(key: string): Promise diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index ddedddb36e5..544c6abdd21 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -4,6 +4,7 @@ const icons = { "align-right": ``, "arrow-up": ``, "arrow-left": ``, + "arrow-right": ``, archive: ``, "bubble-5": ``, brain: ``, From 2180be2f3f6f414e0b40f983a0727e8d610f05ea Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:00:36 -0600 Subject: [PATCH 146/232] chore: cleanup --- packages/app/src/components/titlebar.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 1c825606912..7a08456c7b7 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -196,18 +196,20 @@ export function Titlebar() {