From c190f5f611c1520a553facc362749f8aefaa5005 Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 15 Feb 2026 00:34:56 +0000 Subject: [PATCH 01/13] release: v1.2.3 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index c29176596d..73c7492065 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -73,7 +73,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -107,7 +107,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -134,7 +134,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -158,7 +158,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -182,7 +182,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -215,7 +215,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -244,7 +244,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -260,7 +260,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.2", + "version": "1.2.3", "bin": { "opencode": "./bin/opencode", }, @@ -369,7 +369,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -389,7 +389,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.2", + "version": "1.2.3", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -400,7 +400,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -413,7 +413,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -455,7 +455,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "zod": "catalog:", }, @@ -466,7 +466,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index b2f2f23246..1b140c70f7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.2", + "version": "1.2.3", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index ea16083326..92f82645fd 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.2", + "version": "1.2.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 8c71ca77b2..bb41029f43 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.2", + "version": "1.2.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index debba52bab..66ccb0ad95 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.2", + "version": "1.2.3", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index e361cc0e73..964f4f9829 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.2", + "version": "1.2.3", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index f9ab28cfca..6885891897 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.2", + "version": "1.2.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 173688eb1d..e27762ecb3 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.2", + "version": "1.2.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 9498ce1269..5838fe157d 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.2" +version = "1.2.3" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 1718b791a6..c52ba53c87 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.2", + "version": "1.2.3", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 728ec38385..69742bcbad 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.2", + "version": "1.2.3", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d078c0028e..98ee0323d1 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.2", + "version": "1.2.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index a03d8382db..27580ee522 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.2", + "version": "1.2.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 8a6d289ec0..fe6aea500f 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.2", + "version": "1.2.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 8771b397ce..b4aa7ccf40 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.2", + "version": "1.2.3", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index c610140398..083fd3626f 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.2", + "version": "1.2.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 79858de586..257d117f77 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.2", + "version": "1.2.3", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index e91d1a324d..a04e453e71 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.2", + "version": "1.2.3", "publisher": "sst-dev", "repository": { "type": "git", From 460a87f359cef2cdcd4638ba49b1d7d652ddedd5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sat, 14 Feb 2026 19:24:48 -0600 Subject: [PATCH 02/13] fix(app): stack overflow in filetree (#13667) Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com> --- packages/app/src/components/file-tree.tsx | 94 ++++++++++++++++------- 1 file changed, 66 insertions(+), 28 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 5552cc90b8..758f5a83f5 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -21,6 +21,8 @@ import { import { Dynamic } from "solid-js/web" import type { FileNode } from "@opencode-ai/sdk/v2" +const MAX_DEPTH = 128 + function pathToFileUrl(filepath: string): string { return `file://${encodeFilePath(filepath)}` } @@ -260,12 +262,20 @@ export default function FileTree(props: { _marks?: Set _deeps?: Map _kinds?: ReadonlyMap + _chain?: readonly string[] }) { const file = useFile() const level = props.level ?? 0 const draggable = () => props.draggable ?? true const tooltip = () => props.tooltip ?? true + const key = (p: string) => + file + .normalize(p) + .replace(/[\\/]+$/, "") + .replaceAll("\\", "/") + const chain = props._chain ? [...props._chain, key(props.path)] : [key(props.path)] + const filter = createMemo(() => { if (props._filter) return props._filter @@ -307,23 +317,45 @@ export default function FileTree(props: { const out = new Map() - const visit = (dir: string, lvl: number): number => { - const expanded = file.tree.state(dir)?.expanded ?? false - if (!expanded) return -1 + const root = props.path + if (!(file.tree.state(root)?.expanded ?? false)) return out - 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) + const seen = new Set() + const stack: { dir: string; lvl: number; i: number; kids: string[]; max: number }[] = [] - out.set(dir, max) - return max + const push = (dir: string, lvl: number) => { + const id = key(dir) + if (seen.has(id)) return + seen.add(id) + + const kids = file.tree + .children(dir) + .filter((node) => node.type === "directory" && (file.tree.state(node.path)?.expanded ?? false)) + .map((node) => node.path) + + stack.push({ dir, lvl, i: 0, kids, max: lvl }) + } + + push(root, level - 1) + + while (stack.length > 0) { + const top = stack[stack.length - 1]! + + if (top.i < top.kids.length) { + const next = top.kids[top.i]! + top.i++ + push(next, top.lvl + 1) + continue + } + + out.set(top.dir, top.max) + stack.pop() + + const parent = stack[stack.length - 1] + if (!parent) continue + parent.max = Math.max(parent.max, top.max) } - visit(props.path, level - 1) return out }) @@ -459,21 +491,27 @@ export default function FileTree(props: { }} style={`left: ${Math.max(0, 8 + level * 12 - 4) + 8}px`} /> - + ...} + > + + From 85b5f5b705e8f7852184a4ef147bdc826639d224 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sat, 14 Feb 2026 19:33:22 -0600 Subject: [PATCH 03/13] feat(app): clear notifications action (#13668) Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com> --- packages/app/e2e/selectors.ts | 3 + packages/app/src/i18n/ar.ts | 1 + packages/app/src/i18n/br.ts | 1 + packages/app/src/i18n/bs.ts | 1 + packages/app/src/i18n/da.ts | 1 + packages/app/src/i18n/de.ts | 1 + packages/app/src/i18n/en.ts | 1 + packages/app/src/i18n/es.ts | 1 + packages/app/src/i18n/fr.ts | 1 + packages/app/src/i18n/ja.ts | 1 + packages/app/src/i18n/ko.ts | 1 + packages/app/src/i18n/no.ts | 1 + packages/app/src/i18n/pl.ts | 1 + packages/app/src/i18n/ru.ts | 1 + packages/app/src/i18n/th.ts | 1 + packages/app/src/i18n/zh.ts | 1 + packages/app/src/i18n/zht.ts | 1 + packages/app/src/pages/layout.tsx | 17 ++ .../app/src/pages/layout/sidebar-project.tsx | 155 ++++++++++-------- 19 files changed, 126 insertions(+), 65 deletions(-) diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts index 52c9007ea1..1a0afbab10 100644 --- a/packages/app/e2e/selectors.ts +++ b/packages/app/e2e/selectors.ts @@ -30,6 +30,9 @@ export const projectMenuTriggerSelector = (slug: string) => export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]` +export const projectClearNotificationsSelector = (slug: string) => + `[data-action="project-clear-notifications"][data-project="${slug}"]` + export const projectWorkspacesToggleSelector = (slug: string) => `[data-action="project-workspaces-toggle"][data-project="${slug}"]` diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index e3792a3c3c..81cc92bf6d 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -509,6 +509,7 @@ export const dict = { "sidebar.gettingStarted.line2": "قم بتوصيل أي موفر لاستخدام النماذج، بما في ذلك Claude و GPT و Gemini وما إلى ذلك.", "sidebar.project.recentSessions": "الجلسات الحديثة", "sidebar.project.viewAllSessions": "عرض جميع الجلسات", + "sidebar.project.clearNotifications": "مسح الإشعارات", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "سطح المكتب", "settings.section.server": "الخادم", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 07d6ce467a..9ed3a9fc6f 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -515,6 +515,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Conecte qualquer provedor para usar modelos, incluindo Claude, GPT, Gemini etc.", "sidebar.project.recentSessions": "Sessões recentes", "sidebar.project.viewAllSessions": "Ver todas as sessões", + "sidebar.project.clearNotifications": "Limpar notificações", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", "settings.section.server": "Servidor", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 7d10da6ed8..206aae3729 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -576,6 +576,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Poveži bilo kojeg provajdera da koristiš modele, npr. Claude, GPT, Gemini itd.", "sidebar.project.recentSessions": "Nedavne sesije", "sidebar.project.viewAllSessions": "Prikaži sve sesije", + "sidebar.project.clearNotifications": "Očisti obavijesti", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index ac5c4d494b..6bf67168fb 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -572,6 +572,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Forbind enhver udbyder for at bruge modeller, inkl. Claude, GPT, Gemini osv.", "sidebar.project.recentSessions": "Seneste sessioner", "sidebar.project.viewAllSessions": "Vis alle sessioner", + "sidebar.project.clearNotifications": "Ryd notifikationer", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 99a9506310..4b6b43a57c 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -524,6 +524,7 @@ export const dict = { "Verbinden Sie einen beliebigen Anbieter, um Modelle wie Claude, GPT, Gemini usw. zu nutzen.", "sidebar.project.recentSessions": "Letzte Sitzungen", "sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen", + "sidebar.project.clearNotifications": "Benachrichtigungen löschen", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", "settings.section.server": "Server", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 99513edaa1..fd70f389ec 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -577,6 +577,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc.", "sidebar.project.recentSessions": "Recent sessions", "sidebar.project.viewAllSessions": "View all sessions", + "sidebar.project.clearNotifications": "Clear notifications", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 7a6c4974e0..135a63fef7 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -579,6 +579,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Conecta cualquier proveedor para usar modelos, inc. Claude, GPT, Gemini etc.", "sidebar.project.recentSessions": "Sesiones recientes", "sidebar.project.viewAllSessions": "Ver todas las sesiones", + "sidebar.project.clearNotifications": "Borrar notificaciones", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index fc3bf26679..1ab0c72d53 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -523,6 +523,7 @@ export const dict = { "Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.", "sidebar.project.recentSessions": "Sessions récentes", "sidebar.project.viewAllSessions": "Voir toutes les sessions", + "sidebar.project.clearNotifications": "Effacer les notifications", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Bureau", "settings.section.server": "Serveur", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index b597db02a5..6f092a60f6 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -513,6 +513,7 @@ export const dict = { "sidebar.gettingStarted.line2": "プロバイダーを接続して、Claude、GPT、Geminiなどのモデルを使用できます。", "sidebar.project.recentSessions": "最近のセッション", "sidebar.project.viewAllSessions": "すべてのセッションを表示", + "sidebar.project.clearNotifications": "通知をクリア", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "デスクトップ", "settings.section.server": "サーバー", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 525bd03565..4d814d43d0 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -514,6 +514,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Claude, GPT, Gemini 등을 포함한 모델을 사용하려면 공급자를 연결하세요.", "sidebar.project.recentSessions": "최근 세션", "sidebar.project.viewAllSessions": "모든 세션 보기", + "sidebar.project.clearNotifications": "알림 지우기", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "데스크톱", "settings.section.server": "서버", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 98e79e1896..63bc66acfc 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -579,6 +579,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Koble til en leverandør for å bruke modeller, inkl. Claude, GPT, Gemini osv.", "sidebar.project.recentSessions": "Nylige sesjoner", "sidebar.project.viewAllSessions": "Vis alle sesjoner", + "sidebar.project.clearNotifications": "Fjern varsler", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 983c9c14ac..2a3ea7bfb1 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -514,6 +514,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Połącz dowolnego dostawcę, aby używać modeli, w tym Claude, GPT, Gemini itp.", "sidebar.project.recentSessions": "Ostatnie sesje", "sidebar.project.viewAllSessions": "Zobacz wszystkie sesje", + "sidebar.project.clearNotifications": "Wyczyść powiadomienia", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Pulpit", "settings.section.server": "Serwer", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index f2c87fe0f1..93e5b27425 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -578,6 +578,7 @@ export const dict = { "Подключите любого провайдера для использования моделей, включая Claude, GPT, Gemini и др.", "sidebar.project.recentSessions": "Недавние сессии", "sidebar.project.viewAllSessions": "Посмотреть все сессии", + "sidebar.project.clearNotifications": "Очистить уведомления", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Приложение", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 689e821189..3b3486b5c7 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -571,6 +571,7 @@ export const dict = { "sidebar.gettingStarted.line2": "เชื่อมต่อผู้ให้บริการใด ๆ เพื่อใช้โมเดล รวมถึง Claude, GPT, Gemini ฯลฯ", "sidebar.project.recentSessions": "เซสชันล่าสุด", "sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด", + "sidebar.project.clearNotifications": "ล้างการแจ้งเตือน", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 1b40013b60..6489b70254 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -569,6 +569,7 @@ export const dict = { "sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。", "sidebar.project.recentSessions": "最近会话", "sidebar.project.viewAllSessions": "查看全部会话", + "sidebar.project.clearNotifications": "清除通知", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 34aec01b9c..a01b76c052 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -567,6 +567,7 @@ export const dict = { "sidebar.gettingStarted.line2": "連線任意提供者即可使用更多模型,如 Claude、GPT、Gemini 等。", "sidebar.project.recentSessions": "最近工作階段", "sidebar.project.viewAllSessions": "查看全部工作階段", + "sidebar.project.clearNotifications": "清除通知", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "桌面", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 7eb064f425..7d4a5c0cb8 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1692,6 +1692,13 @@ export default function Layout(props: ParentProps) { }) const projectId = createMemo(() => panelProps.project?.id ?? "") const workspaces = createMemo(() => workspaceIds(panelProps.project)) + const unseenCount = createMemo(() => + workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), + ) + const clearNotifications = () => + workspaces() + .filter((directory) => notification.project.unseenCount(directory) > 0) + .forEach((directory) => notification.project.markViewed(directory)) const workspacesEnabled = createMemo(() => { const project = panelProps.project if (!project) return false @@ -1769,6 +1776,16 @@ export default function Layout(props: ParentProps) { : language.t("sidebar.workspaces.enable")} + + + {language.t("sidebar.project.clearNotifications")} + + active: Accessor overlay: Accessor + dirs: Accessor onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseLeave: (worktree: string) => void onProjectFocus: (worktree: string) => void @@ -70,73 +72,94 @@ const ProjectTile = (props: { setMenu: (value: boolean) => void setOpen: (value: boolean) => void language: ReturnType -}): JSX.Element => ( - { - props.setMenu(value) - if (value) props.setOpen(false) - }} - > - { + const notification = useNotification() + const unseenCount = createMemo(() => + props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), + ) + + const clear = () => + props + .dirs() + .filter((directory) => notification.project.unseenCount(directory) > 0) + .forEach((directory) => notification.project.markViewed(directory)) + + return ( + { + props.setMenu(value) + if (value) props.setOpen(false) }} - onMouseEnter={(event: MouseEvent) => { - if (!props.overlay()) return - props.onProjectMouseEnter(props.project.worktree, event) - }} - onMouseLeave={() => { - if (!props.overlay()) return - props.onProjectMouseLeave(props.project.worktree) - }} - onFocus={() => { - if (!props.overlay()) return - props.onProjectFocus(props.project.worktree) - }} - onClick={() => props.navigateToProject(props.project.worktree)} - onBlur={() => props.setOpen(false)} > - - - - - props.showEditProjectDialog(props.project)}> - {props.language.t("common.edit")} - - props.toggleProjectWorkspaces(props.project)} - > - - {props.workspacesEnabled(props.project) - ? props.language.t("sidebar.workspaces.disable") - : props.language.t("sidebar.workspaces.enable")} - - - - props.closeProject(props.project.worktree)} - > - {props.language.t("common.close")} - - - - -) + { + if (!props.overlay()) return + props.onProjectMouseEnter(props.project.worktree, event) + }} + onMouseLeave={() => { + if (!props.overlay()) return + props.onProjectMouseLeave(props.project.worktree) + }} + onFocus={() => { + if (!props.overlay()) return + props.onProjectFocus(props.project.worktree) + }} + onClick={() => props.navigateToProject(props.project.worktree)} + onBlur={() => props.setOpen(false)} + > + + + + + props.showEditProjectDialog(props.project)}> + {props.language.t("common.edit")} + + props.toggleProjectWorkspaces(props.project)} + > + + {props.workspacesEnabled(props.project) + ? props.language.t("sidebar.workspaces.disable") + : props.language.t("sidebar.workspaces.enable")} + + + + {props.language.t("sidebar.project.clearNotifications")} + + + props.closeProject(props.project.worktree)} + > + {props.language.t("common.close")} + + + + + ) +} const ProjectPreviewPanel = (props: { project: LocalProject @@ -254,6 +277,7 @@ export const SortableProject = (props: { ) const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2)) const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) + const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) const [open, setOpen] = createSignal(false) const [menu, setMenu] = createSignal(false) @@ -304,6 +328,7 @@ export const SortableProject = (props: { selected={selected} active={active} overlay={overlay} + dirs={dirs} onProjectMouseEnter={props.ctx.onProjectMouseEnter} onProjectMouseLeave={props.ctx.onProjectMouseLeave} onProjectFocus={props.ctx.onProjectFocus} From 2bab5e8c39f4ed70dbfe6d971728d8d899b88e4f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 14 Feb 2026 20:35:43 -0500 Subject: [PATCH 04/13] fix: derive all IDs from file paths during json migration Earlier migrations moved data to new directories without updating JSON fields. Now consistently derives all IDs from file paths: - Projects: id from filename - Sessions: id from filename, projectID from parent directory - Messages: id from filename, sessionID from parent directory - Parts: id from filename, messageID from parent directory This ensures migrated data matches the actual file layout regardless of stale values in JSON content. --- .../opencode/src/storage/json-migration.ts | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 89d561188c..e0684ce3c1 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -152,6 +152,7 @@ export namespace JsonMigration { sqlite.exec("BEGIN TRANSACTION") // Migrate projects first (no FK deps) + // Derive all IDs from file paths, not JSON content const projectIds = new Set() const projectValues = [] as any[] for (let i = 0; i < projectFiles.length; i += batchSize) { @@ -161,13 +162,10 @@ export namespace JsonMigration { for (let j = 0; j < batch.length; j++) { const data = batch[j] if (!data) continue - if (!data?.id) { - errs.push(`project missing id: ${projectFiles[i + j]}`) - continue - } - projectIds.add(data.id) + const id = path.basename(projectFiles[i + j], ".json") + projectIds.add(id) projectValues.push({ - id: data.id, + id, worktree: data.worktree ?? "/", vcs: data.vcs, name: data.name ?? undefined, @@ -186,6 +184,9 @@ export namespace JsonMigration { log.info("migrated projects", { count: stats.projects, duration: Math.round(performance.now() - start) }) // Migrate sessions (depends on projects) + // Derive all IDs from directory/file paths, not JSON content, since earlier + // migrations may have moved sessions to new directories without updating the JSON + const sessionProjects = sessionFiles.map((file) => path.basename(path.dirname(file))) const sessionIds = new Set() const sessionValues = [] as any[] for (let i = 0; i < sessionFiles.length; i += batchSize) { @@ -195,18 +196,16 @@ export namespace JsonMigration { for (let j = 0; j < batch.length; j++) { const data = batch[j] if (!data) continue - if (!data?.id || !data?.projectID) { - errs.push(`session missing id or projectID: ${sessionFiles[i + j]}`) - continue - } - if (!projectIds.has(data.projectID)) { + const id = path.basename(sessionFiles[i + j], ".json") + const projectID = sessionProjects[i + j] + if (!projectIds.has(projectID)) { orphans.sessions++ continue } - sessionIds.add(data.id) + sessionIds.add(id) sessionValues.push({ - id: data.id, - project_id: data.projectID, + id, + project_id: projectID, parent_id: data.parentID ?? null, slug: data.slug ?? "", directory: data.directory ?? "", @@ -253,11 +252,7 @@ export namespace JsonMigration { const data = batch[j] if (!data) continue const file = allMessageFiles[i + j] - const id = data.id ?? path.basename(file, ".json") - if (!id) { - errs.push(`message missing id: ${file}`) - continue - } + const id = path.basename(file, ".json") const sessionID = allMessageSessions[i + j] messageSessions.set(id, sessionID) const rest = data @@ -287,12 +282,8 @@ export namespace JsonMigration { const data = batch[j] if (!data) continue const file = partFiles[i + j] - const id = data.id ?? path.basename(file, ".json") - const messageID = data.messageID ?? path.basename(path.dirname(file)) - if (!id || !messageID) { - errs.push(`part missing id/messageID/sessionID: ${file}`) - continue - } + const id = path.basename(file, ".json") + const messageID = path.basename(path.dirname(file)) const sessionID = messageSessions.get(messageID) if (!sessionID) { errs.push(`part missing message session: ${file}`) From b5c8bd3421e4b89cf9dabc6ccf019a82eefc64a5 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 14 Feb 2026 20:35:46 -0500 Subject: [PATCH 05/13] test: add tests for path-derived IDs in json migration Tests verify that file paths are used for IDs even when JSON contains different values - ensuring robustness against stale JSON content. --- .../test/storage/json-migration.test.ts | 167 +++++++++++++++++- 1 file changed, 163 insertions(+), 4 deletions(-) diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index ff05d6d059..b70c9e1ebe 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -128,6 +128,28 @@ describe("JSON to SQLite migration", () => { expect(projects[0].sandboxes).toEqual(["/test/sandbox"]) }) + test("uses filename for project id when JSON has different value", async () => { + await Bun.write( + path.join(storageDir, "project", "proj_filename.json"), + JSON.stringify({ + id: "proj_different_in_json", // Stale! Should be ignored + worktree: "/test/path", + vcs: "git", + name: "Test Project", + sandboxes: [], + }), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.projects).toBe(1) + + const db = drizzle({ client: sqlite }) + const projects = db.select().from(ProjectTable).all() + expect(projects.length).toBe(1) + expect(projects[0].id).toBe("proj_filename") // Uses filename, not JSON id + }) + test("migrates project with commands", async () => { await writeProject(storageDir, { id: "proj_with_commands", @@ -285,6 +307,74 @@ describe("JSON to SQLite migration", () => { expect(parts[0].data).not.toHaveProperty("sessionID") }) + test("uses filename for message id when JSON has different value", async () => { + await writeProject(storageDir, { + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }) + await writeSession(storageDir, "proj_test123abc", { ...fixtures.session }) + await Bun.write( + path.join(storageDir, "message", "ses_test456def", "msg_from_filename.json"), + JSON.stringify({ + id: "msg_different_in_json", // Stale! Should be ignored + sessionID: "ses_test456def", + role: "user", + agent: "default", + time: { created: 1700000000000 }, + }), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.messages).toBe(1) + + const db = drizzle({ client: sqlite }) + const messages = db.select().from(MessageTable).all() + expect(messages.length).toBe(1) + expect(messages[0].id).toBe("msg_from_filename") // Uses filename, not JSON id + expect(messages[0].session_id).toBe("ses_test456def") + }) + + test("uses paths for part id and messageID when JSON has different values", async () => { + await writeProject(storageDir, { + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }) + await writeSession(storageDir, "proj_test123abc", { ...fixtures.session }) + await Bun.write( + path.join(storageDir, "message", "ses_test456def", "msg_realmsgid.json"), + JSON.stringify({ + role: "user", + agent: "default", + time: { created: 1700000000000 }, + }), + ) + await Bun.write( + path.join(storageDir, "part", "msg_realmsgid", "prt_from_filename.json"), + JSON.stringify({ + id: "prt_different_in_json", // Stale! Should be ignored + messageID: "msg_different_in_json", // Stale! Should be ignored + sessionID: "ses_test456def", + type: "text", + text: "Hello", + }), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.parts).toBe(1) + + const db = drizzle({ client: sqlite }) + const parts = db.select().from(PartTable).all() + expect(parts.length).toBe(1) + expect(parts[0].id).toBe("prt_from_filename") // Uses filename, not JSON id + expect(parts[0].message_id).toBe("msg_realmsgid") // Uses parent dir, not JSON messageID + }) + test("skips orphaned sessions (no parent project)", async () => { await Bun.write( path.join(storageDir, "session", "proj_test123abc", "ses_orphan.json"), @@ -304,6 +394,72 @@ describe("JSON to SQLite migration", () => { expect(stats?.sessions).toBe(0) }) + test("uses directory path for projectID when JSON has stale value", async () => { + // Simulates the scenario where earlier migration moved sessions to new + // git-based project directories but didn't update the projectID field + const gitBasedProjectID = "abc123gitcommit" + await writeProject(storageDir, { + id: gitBasedProjectID, + worktree: "/test/path", + vcs: "git", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }) + + // Session is in the git-based directory but JSON still has old projectID + await writeSession(storageDir, gitBasedProjectID, { + id: "ses_migrated", + projectID: "old-project-name", // Stale! Should be ignored + slug: "migrated-session", + directory: "/test/path", + title: "Migrated Session", + version: "1.0.0", + time: { created: 1700000000000, updated: 1700000001000 }, + }) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.sessions).toBe(1) + + const db = drizzle({ client: sqlite }) + const sessions = db.select().from(SessionTable).all() + expect(sessions.length).toBe(1) + expect(sessions[0].id).toBe("ses_migrated") + expect(sessions[0].project_id).toBe(gitBasedProjectID) // Uses directory, not stale JSON + }) + + test("uses filename for session id when JSON has different value", async () => { + await writeProject(storageDir, { + id: "proj_test123abc", + worktree: "/test/path", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }) + + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_from_filename.json"), + JSON.stringify({ + id: "ses_different_in_json", // Stale! Should be ignored + projectID: "proj_test123abc", + slug: "test-session", + directory: "/test/path", + title: "Test Session", + version: "1.0.0", + time: { created: 1700000000000, updated: 1700000001000 }, + }), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.sessions).toBe(1) + + const db = drizzle({ client: sqlite }) + const sessions = db.select().from(SessionTable).all() + expect(sessions.length).toBe(1) + expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id + expect(sessions[0].project_id).toBe("proj_test123abc") + }) + test("is idempotent (running twice doesn't duplicate)", async () => { await writeProject(storageDir, { id: "proj_test123abc", @@ -666,8 +822,11 @@ describe("JSON to SQLite migration", () => { const stats = await JsonMigration.run(sqlite) - expect(stats.projects).toBe(1) - expect(stats.sessions).toBe(1) + // Projects: proj_test123abc (valid), proj_missing_id (now derives id from filename) + // Sessions: ses_test456def (valid), ses_missing_project (now uses dir path), + // ses_orphan (now uses dir path, ignores stale projectID) + expect(stats.projects).toBe(2) + expect(stats.sessions).toBe(3) expect(stats.messages).toBe(1) expect(stats.parts).toBe(1) expect(stats.todos).toBe(1) @@ -676,8 +835,8 @@ describe("JSON to SQLite migration", () => { expect(stats.errors.length).toBeGreaterThanOrEqual(6) const db = drizzle({ client: sqlite }) - expect(db.select().from(ProjectTable).all().length).toBe(1) - expect(db.select().from(SessionTable).all().length).toBe(1) + expect(db.select().from(ProjectTable).all().length).toBe(2) + expect(db.select().from(SessionTable).all().length).toBe(3) expect(db.select().from(MessageTable).all().length).toBe(1) expect(db.select().from(PartTable).all().length).toBe(1) expect(db.select().from(TodoTable).all().length).toBe(1) From 45f0050372a1bc035164a5953b1fdb46df106d4a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 14 Feb 2026 20:36:17 -0500 Subject: [PATCH 06/13] core: add db command for database inspection and querying --- packages/opencode/src/cli/cmd/db.ts | 68 +++++++++++++++++++++++++++++ packages/opencode/src/index.ts | 2 + packages/opencode/src/storage/db.ts | 1 + 3 files changed, 71 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/db.ts diff --git a/packages/opencode/src/cli/cmd/db.ts b/packages/opencode/src/cli/cmd/db.ts new file mode 100644 index 0000000000..0ade4d3c4b --- /dev/null +++ b/packages/opencode/src/cli/cmd/db.ts @@ -0,0 +1,68 @@ +import type { Argv } from "yargs" +import { spawn } from "child_process" +import { Database } from "../../storage/db" +import { Database as BunDatabase } from "bun:sqlite" +import { UI } from "../ui" +import { cmd } from "./cmd" + +const QueryCommand = cmd({ + command: "$0 [query]", + describe: "open an interactive sqlite3 shell or run a query", + builder: (yargs: Argv) => { + return yargs + .positional("query", { + type: "string", + describe: "SQL query to execute", + }) + .option("format", { + type: "string", + choices: ["json", "tsv"], + default: "tsv", + describe: "Output format", + }) + }, + handler: async (args: { query?: string; format: string }) => { + const query = args.query as string | undefined + if (query) { + const db = new BunDatabase(Database.Path, { readonly: true }) + try { + const result = db.query(query).all() as Record[] + if (args.format === "json") { + console.log(JSON.stringify(result, null, 2)) + } else if (result.length > 0) { + const keys = Object.keys(result[0]) + console.log(keys.join("\t")) + for (const row of result) { + console.log(keys.map((k) => row[k]).join("\t")) + } + } + } catch (err) { + UI.error(err instanceof Error ? err.message : String(err)) + process.exit(1) + } + db.close() + return + } + const child = spawn("sqlite3", [Database.Path], { + stdio: "inherit", + }) + await new Promise((resolve) => child.on("close", resolve)) + }, +}) + +const PathCommand = cmd({ + command: "path", + describe: "print the database path", + handler: () => { + console.log(Database.Path) + }, +}) + +export const DbCommand = cmd({ + command: "db", + describe: "database tools", + builder: (yargs: Argv) => { + return yargs.command(QueryCommand).command(PathCommand).demandCommand() + }, + handler: () => {}, +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 420ead5555..0c4fb5d195 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -26,6 +26,7 @@ import { EOL } from "os" import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" +import { DbCommand } from "./cli/cmd/db" import path from "path" import { Global } from "./global" import { JsonMigration } from "./storage/json-migration" @@ -138,6 +139,7 @@ const cli = yargs(hideBin(process.argv)) .command(GithubCommand) .command(PrCommand) .command(SessionCommand) + .command(DbCommand) .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") || diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 387e93b376..0974cbe7be 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -25,6 +25,7 @@ export const NotFoundError = NamedError.create( const log = Log.create({ service: "db" }) export namespace Database { + export const Path = path.join(Global.Path.data, "opencode.db") type Schema = typeof schema export type Transaction = SQLiteTransaction<"sync", void, Schema> From d1482e148399bfaf808674549199f5f4aa69a22d Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 15 Feb 2026 01:55:33 +0000 Subject: [PATCH 07/13] release: v1.2.4 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index 73c7492065..59106e14af 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -73,7 +73,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -107,7 +107,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -134,7 +134,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -158,7 +158,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -182,7 +182,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -215,7 +215,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -244,7 +244,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -260,7 +260,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.3", + "version": "1.2.4", "bin": { "opencode": "./bin/opencode", }, @@ -369,7 +369,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -389,7 +389,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.3", + "version": "1.2.4", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -400,7 +400,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -413,7 +413,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -455,7 +455,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "zod": "catalog:", }, @@ -466,7 +466,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 1b140c70f7..31afda6566 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.3", + "version": "1.2.4", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 92f82645fd..a6b2f5685d 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.3", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index bb41029f43..f81304c920 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.3", + "version": "1.2.4", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 66ccb0ad95..e3864bfad6 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.3", + "version": "1.2.4", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 964f4f9829..261d36bae5 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.3", + "version": "1.2.4", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 6885891897..2901e299c0 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.3", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index e27762ecb3..ac7060dd10 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.3", + "version": "1.2.4", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 5838fe157d..9c10eb9826 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.3" +version = "1.2.4" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.4/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.4/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.4/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.4/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.3/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.4/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index c52ba53c87..3a431e9bdf 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.3", + "version": "1.2.4", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 69742bcbad..a5b3415550 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.3", + "version": "1.2.4", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 98ee0323d1..437fc09170 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.3", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 27580ee522..74c3fdb1ad 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.3", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index fe6aea500f..5a5a0e8359 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.3", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index b4aa7ccf40..684836335c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.3", + "version": "1.2.4", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 083fd3626f..74393cecee 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.3", + "version": "1.2.4", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 257d117f77..e2acd6cf39 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.3", + "version": "1.2.4", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index a04e453e71..e8e41a4f29 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.3", + "version": "1.2.4", "publisher": "sst-dev", "repository": { "type": "git", From eb553f53ac9689ab2056fceea0c7b0504f642101 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 15 Feb 2026 00:41:16 -0600 Subject: [PATCH 08/13] fix: ensure sqlite migration logs to stderr instead of stdout (#13691) --- packages/opencode/src/index.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 0c4fb5d195..39e77782f5 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -82,14 +82,14 @@ const cli = yargs(hideBin(process.argv)) const marker = path.join(Global.Path.data, "opencode.db") if (!(await Bun.file(marker).exists())) { - console.log("Performing one time database migration, may take a few minutes...") - const tty = process.stdout.isTTY + const tty = process.stderr.isTTY + process.stderr.write("Performing one time database migration, may take a few minutes..." + EOL) const width = 36 const orange = "\x1b[38;5;214m" const muted = "\x1b[0;2m" const reset = "\x1b[0m" let last = -1 - if (tty) process.stdout.write("\x1b[?25l") + if (tty) process.stderr.write("\x1b[?25l") try { await JsonMigration.run(Database.Client().$client, { progress: (event) => { @@ -99,22 +99,22 @@ const cli = yargs(hideBin(process.argv)) if (tty) { const fill = Math.round((percent / 100) * width) const bar = `${"■".repeat(fill)}${"・".repeat(width - fill)}` - process.stdout.write( + process.stderr.write( `\r${orange}${bar} ${percent.toString().padStart(3)}%${reset} ${muted}${event.label.padEnd(12)} ${event.current}/${event.total}${reset}`, ) - if (event.current === event.total) process.stdout.write("\n") + if (event.current === event.total) process.stderr.write("\n") } else { - console.log(`sqlite-migration:${percent}`) + process.stderr.write(`sqlite-migration:${percent}${EOL}`) } }, }) } finally { - if (tty) process.stdout.write("\x1b[?25h") + if (tty) process.stderr.write("\x1b[?25h") else { - console.log(`sqlite-migration:done`) + process.stderr.write(`sqlite-migration:done${EOL}`) } } - console.log("Database migration complete.") + process.stderr.write("Database migration complete." + EOL) } }) .usage("\n" + UI.logo()) @@ -190,7 +190,7 @@ try { if (formatted) UI.error(formatted) if (formatted === undefined) { UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL) - console.error(e instanceof Error ? e.message : String(e)) + process.stderr.write((e instanceof Error ? e.message : String(e)) + EOL) } process.exitCode = 1 } finally { From 985c2a3d15c13512b9bb456882b97ebe863cae5f Mon Sep 17 00:00:00 2001 From: Brandon Julio Thenaro Date: Sun, 15 Feb 2026 20:44:21 +0700 Subject: [PATCH 09/13] feat: Add GeistMono Nerd Font to available mono font options (#13720) --- packages/app/src/components/settings-general.tsx | 1 + packages/app/src/context/settings.tsx | 1 + packages/app/src/i18n/ar.ts | 1 + packages/app/src/i18n/br.ts | 1 + packages/app/src/i18n/bs.ts | 1 + packages/app/src/i18n/da.ts | 1 + packages/app/src/i18n/de.ts | 1 + packages/app/src/i18n/en.ts | 1 + packages/app/src/i18n/es.ts | 1 + packages/app/src/i18n/fr.ts | 1 + packages/app/src/i18n/ja.ts | 1 + packages/app/src/i18n/ko.ts | 1 + packages/app/src/i18n/no.ts | 1 + packages/app/src/i18n/pl.ts | 1 + packages/app/src/i18n/ru.ts | 1 + packages/app/src/i18n/th.ts | 1 + packages/app/src/i18n/zh.ts | 1 + packages/app/src/i18n/zht.ts | 1 + packages/ui/src/components/font.tsx | 7 +++++++ 19 files changed, 25 insertions(+) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 439f542bb1..d5a0b813b6 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -128,6 +128,7 @@ export const SettingsGeneral: Component = () => { { value: "roboto-mono", label: "font.option.robotoMono" }, { value: "source-code-pro", label: "font.option.sourceCodePro" }, { value: "ubuntu-mono", label: "font.option.ubuntuMono" }, + { value: "geist-mono", label: "font.option.geistMono" }, ] as const const fontOptionsList = [...fontOptions] diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index d72d4ceb1e..fbcd0a8518 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -85,6 +85,7 @@ const monoFonts: Record = { "roboto-mono": `"Roboto Mono Nerd Font", "RobotoMono Nerd Font", "RobotoMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "source-code-pro": `"Source Code Pro Nerd Font", "SauceCodePro Nerd Font", "SauceCodePro Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "ubuntu-mono": `"Ubuntu Mono Nerd Font", "UbuntuMono Nerd Font", "UbuntuMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "geist-mono": `"GeistMono Nerd Font", "GeistMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, } export function monoFontFamily(font: string | undefined) { diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 81cc92bf6d..3d347c8423 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -557,6 +557,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "تنبيه 01", "sound.option.alert02": "تنبيه 02", "sound.option.alert03": "تنبيه 03", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 9ed3a9fc6f..730c01fdff 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -563,6 +563,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alerta 01", "sound.option.alert02": "Alerta 02", "sound.option.alert03": "Alerta 03", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 206aae3729..d53c261126 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -631,6 +631,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Upozorenje 01", "sound.option.alert02": "Upozorenje 02", "sound.option.alert03": "Upozorenje 03", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 6bf67168fb..9faa14d3da 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -627,6 +627,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alarm 01", "sound.option.alert02": "Alarm 02", "sound.option.alert03": "Alarm 03", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 4b6b43a57c..d350af6cf5 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -572,6 +572,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alarm 01", "sound.option.alert02": "Alarm 02", "sound.option.alert03": "Alarm 03", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index fd70f389ec..cb42b016f1 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -632,6 +632,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 135a63fef7..c4ec378dcd 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -635,6 +635,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alerta 01", "sound.option.alert02": "Alerta 02", "sound.option.alert03": "Alerta 03", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 1ab0c72d53..7069fbd98f 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -571,6 +571,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alerte 01", "sound.option.alert02": "Alerte 02", "sound.option.alert03": "Alerte 03", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 6f092a60f6..e7e24a9bd6 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -561,6 +561,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "アラート 01", "sound.option.alert02": "アラート 02", "sound.option.alert03": "アラート 03", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 4d814d43d0..650b7e662a 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -562,6 +562,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "알림 01", "sound.option.alert02": "알림 02", "sound.option.alert03": "알림 03", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 63bc66acfc..afc162ab17 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -634,6 +634,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Varsel 01", "sound.option.alert02": "Varsel 02", "sound.option.alert03": "Varsel 03", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 2a3ea7bfb1..d8572148a8 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -562,6 +562,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 93e5b27425..86d201cebc 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -632,6 +632,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 3b3486b5c7..83020bf8c0 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -626,6 +626,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "เสียงเตือน 01", "sound.option.alert02": "เสียงเตือน 02", "sound.option.alert03": "เสียงเตือน 03", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 6489b70254..d0bf86cbba 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -623,6 +623,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "警报 01", "sound.option.alert02": "警报 02", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index a01b76c052..349c90b0e1 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -621,6 +621,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "警報 01", "sound.option.alert02": "警報 02", "sound.option.alert03": "警報 03", diff --git a/packages/ui/src/components/font.tsx b/packages/ui/src/components/font.tsx index fa4b6d8c01..bcb8863c89 100644 --- a/packages/ui/src/components/font.tsx +++ b/packages/ui/src/components/font.tsx @@ -26,6 +26,8 @@ import ubuntuMono from "../assets/fonts/ubuntu-mono-nerd-font.woff2" import ubuntuMonoBold from "../assets/fonts/ubuntu-mono-nerd-font-bold.woff2" import iosevka from "../assets/fonts/iosevka-nerd-font.woff2" import iosevkaBold from "../assets/fonts/iosevka-nerd-font-bold.woff2" +import geistMono from "../assets/fonts/GeistMonoNerdFontMono-Regular.woff2" +import geistMonoBold from "../assets/fonts/GeistMonoNerdFontMono-Bold.woff2" type MonoFont = { family: string @@ -89,6 +91,11 @@ export const MONO_NERD_FONTS = [ regular: iosevka, bold: iosevkaBold, }, + { + family: "GeistMono Nerd Font", + regular: geistMono, + bold: geistMonoBold, + }, ] satisfies MonoFont[] const monoNerdCss = MONO_NERD_FONTS.map( From 3aaa34be1efe2e202312fe1312605c4cdac2e115 Mon Sep 17 00:00:00 2001 From: zerone0x Date: Sun, 15 Feb 2026 21:45:34 +0800 Subject: [PATCH 10/13] fix(desktop): focus window after update/relaunch (#13701) --- packages/desktop/src-tauri/src/windows.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/desktop/src-tauri/src/windows.rs b/packages/desktop/src-tauri/src/windows.rs index 2ddcb0506d..056720055b 100644 --- a/packages/desktop/src-tauri/src/windows.rs +++ b/packages/desktop/src-tauri/src/windows.rs @@ -22,6 +22,8 @@ impl MainWindow { pub fn create(app: &AppHandle) -> Result { if let Some(window) = app.get_webview_window(Self::LABEL) { + let _ = window.set_focus(); + let _ = window.unminimize(); return Ok(Self(window)); } @@ -50,6 +52,9 @@ impl MainWindow { let window = window_builder.build()?; + // Ensure window is focused after creation (e.g., after update/relaunch) + let _ = window.set_focus(); + setup_window_state_listener(app, &window); #[cfg(windows)] From 37611217282b81458bcd5a74850bd96787721b06 Mon Sep 17 00:00:00 2001 From: Denys Date: Sun, 15 Feb 2026 14:46:19 +0100 Subject: [PATCH 11/13] docs: add Ukrainian README translation (#13697) --- README.ar.md | 3 +- README.br.md | 3 +- README.bs.md | 3 +- README.da.md | 3 +- README.de.md | 3 +- README.es.md | 3 +- README.fr.md | 3 +- README.it.md | 3 +- README.ja.md | 3 +- README.ko.md | 3 +- README.md | 3 +- README.no.md | 3 +- README.pl.md | 3 +- README.ru.md | 3 +- README.th.md | 3 +- README.tr.md | 3 +- README.uk.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.zh.md | 3 +- README.zht.md | 3 +- 19 files changed, 175 insertions(+), 18 deletions(-) create mode 100644 README.uk.md diff --git a/README.ar.md b/README.ar.md index edac204a28..f24e598d5e 100644 --- a/README.ar.md +++ b/README.ar.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.br.md b/README.br.md index c185603efb..4802c4996f 100644 --- a/README.br.md +++ b/README.br.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.bs.md b/README.bs.md index d64a69c0d7..9ad6852018 100644 --- a/README.bs.md +++ b/README.bs.md @@ -32,7 +32,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.da.md b/README.da.md index 7f3d5aa5dd..4b1302dbc3 100644 --- a/README.da.md +++ b/README.da.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.de.md b/README.de.md index 2aa78657ca..16116dc72f 100644 --- a/README.de.md +++ b/README.de.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.es.md b/README.es.md index 2b80427ab2..5c18ff4aca 100644 --- a/README.es.md +++ b/README.es.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.fr.md b/README.fr.md index bc3fe9e757..0382164bed 100644 --- a/README.fr.md +++ b/README.fr.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.it.md b/README.it.md index 6da7d51fc6..c966ccec49 100644 --- a/README.it.md +++ b/README.it.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ja.md b/README.ja.md index 7a0bbb08f3..11109e7eb4 100644 --- a/README.ja.md +++ b/README.ja.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ko.md b/README.ko.md index 1c931c31f3..23fea76b1e 100644 --- a/README.ko.md +++ b/README.ko.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.md b/README.md index bd01fc94e8..99b4b2c50f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.no.md b/README.no.md index 092316bae1..9b9e90dc38 100644 --- a/README.no.md +++ b/README.no.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.pl.md b/README.pl.md index a225d82539..fced98dfc3 100644 --- a/README.pl.md +++ b/README.pl.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ru.md b/README.ru.md index c13f039d16..a7c590c16b 100644 --- a/README.ru.md +++ b/README.ru.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.th.md b/README.th.md index ba2db8a850..0999167f23 100644 --- a/README.th.md +++ b/README.th.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.tr.md b/README.tr.md index 635a5782fe..67f84e4ddb 100644 --- a/README.tr.md +++ b/README.tr.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

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

+ + + + + OpenCode logo + + +

+

AI-агент для програмування з відкритим кодом.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська +

+ +[![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 (офіційна формула Homebrew, оновлюється рідше) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +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 +``` + +### Агенти + +OpenCode містить два вбудовані агенти, між якими можна перемикатися клавішею `Tab`. + +- **build** - Агент за замовчуванням із повним доступом для завдань розробки +- **plan** - Агент лише для читання для аналізу та дослідження коду + - За замовчуванням забороняє редагування файлів + - Запитує дозвіл перед запуском bash-команд + - Ідеально підходить для дослідження незнайомих кодових баз або планування змін + +Також доступний допоміжний агент **general** для складного пошуку та багатокрокових завдань. +Він використовується всередині системи й може бути викликаний у повідомленнях через `@general`. + +Дізнайтеся більше про [agents](https://opencode.ai/docs/agents). + +### Документація + +Щоб дізнатися більше про налаштування OpenCode, [**перейдіть до нашої документації**](https://opencode.ai/docs). + +### Внесок + +Якщо ви хочете зробити внесок в OpenCode, будь ласка, прочитайте нашу [документацію для контриб'юторів](./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.md b/README.zh.md index b2f288f5ba..113d476b2e 100644 --- a/README.zh.md +++ b/README.zh.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.zht.md b/README.zht.md index be4ef053c0..b518104443 100644 --- a/README.zht.md +++ b/README.zht.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) From 878ddc6a0a9eff4fe990dfc241a8eb1c72f0659d Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:46:56 +0100 Subject: [PATCH 12/13] fix(app): keybind [shift+tab] (#13695) --- packages/app/src/context/command.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 237d718846..03437c9735 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -316,8 +316,10 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const isPalette = palette().has(sig) const option = keymap().get(sig) const modified = event.ctrlKey || event.metaKey || event.altKey + const isTab = event.key === "Tab" - if (isEditableTarget(event.target) && !isPalette && !isAllowedEditableKeybind(option?.id) && !modified) return + if (isEditableTarget(event.target) && !isPalette && !isAllowedEditableKeybind(option?.id) && !modified && !isTab) + return if (isPalette) { event.preventDefault() From 3c85cf4fac596928713685068c6c92f356b848f3 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Sun, 15 Feb 2026 19:17:19 +0530 Subject: [PATCH 13/13] fix(app): only navigate prompt history at input boundaries (#13690) --- packages/app/src/components/prompt-input.tsx | 22 ++----------------- .../components/prompt-input/history.test.ts | 15 ++++++++++--- .../src/components/prompt-input/history.ts | 10 +++++---- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 8e8c3c895b..984888c35d 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -911,31 +911,13 @@ export const PromptInput: Component = (props) => { if (!collapsed) return const cursorPosition = getCursorPosition(editorRef) - const textLength = promptLength(prompt.current()) const textContent = prompt .current() .map((part) => ("content" in part ? part.content : "")) .join("") const direction = event.key === "ArrowUp" ? "up" : "down" - if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition)) return - const isEmpty = textContent.trim() === "" || textLength <= 1 - const hasNewlines = textContent.includes("\n") - const inHistory = store.historyIndex >= 0 - const atStart = cursorPosition <= (isEmpty ? 1 : 0) - const atEnd = cursorPosition >= (isEmpty ? textLength - 1 : textLength) - const allowUp = isEmpty || atStart || (!hasNewlines && !inHistory) || (inHistory && atEnd) - const allowDown = isEmpty || atEnd || (!hasNewlines && !inHistory) || (inHistory && atStart) - - if (direction === "up") { - if (!allowUp) return - if (navigateHistory("up")) { - event.preventDefault() - } - return - } - - if (!allowDown) return - if (navigateHistory("down")) { + if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition, store.historyIndex >= 0)) return + if (navigateHistory(direction)) { event.preventDefault() } return diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts index a37fdad677..b7a4f896b8 100644 --- a/packages/app/src/components/prompt-input/history.test.ts +++ b/packages/app/src/components/prompt-input/history.test.ts @@ -73,7 +73,7 @@ describe("prompt-input history", () => { expect(original[1].selection?.startLine).toBe(1) }) - test("canNavigateHistoryAtCursor only allows multiline boundaries", () => { + test("canNavigateHistoryAtCursor only allows prompt boundaries", () => { const value = "a\nb\nc" expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true) @@ -85,7 +85,16 @@ describe("prompt-input history", () => { expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false) expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true) - expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(true) - expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(true) + expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false) + expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false) + + expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true) + expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true) + expect(canNavigateHistoryAtCursor("down", "abc", 3, true)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 1, true)).toBe(false) + expect(canNavigateHistoryAtCursor("down", "abc", 1, true)).toBe(false) }) }) diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts index f26f808487..c279a3ed56 100644 --- a/packages/app/src/components/prompt-input/history.ts +++ b/packages/app/src/components/prompt-input/history.ts @@ -4,11 +4,13 @@ const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] export const MAX_HISTORY = 100 -export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number) { - if (!text.includes("\n")) return true +export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) { const position = Math.max(0, Math.min(cursor, text.length)) - if (direction === "up") return !text.slice(0, position).includes("\n") - return !text.slice(position).includes("\n") + const atStart = position === 0 + const atEnd = position === text.length + if (inHistory) return atStart || atEnd + if (direction === "up") return position === 0 + return position === text.length } export function clonePromptParts(prompt: Prompt): Prompt {