diff --git a/bun.lock b/bun.lock index 061e36e143..59106e14af 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.1", + "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.1", + "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.1", + "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.1", + "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.1", + "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.1", + "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.1", + "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.1", + "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.1", + "version": "1.2.4", "bin": { "opencode": "./bin/opencode", }, @@ -276,7 +276,7 @@ "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", "@ai-sdk/google": "2.0.52", - "@ai-sdk/google-vertex": "3.0.98", + "@ai-sdk/google-vertex": "3.0.103", "@ai-sdk/groq": "2.0.34", "@ai-sdk/mistral": "2.0.27", "@ai-sdk/openai": "2.0.89", @@ -369,7 +369,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.1", + "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.1", + "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.1", + "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.1", + "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.1", + "version": "1.2.4", "dependencies": { "zod": "catalog:", }, @@ -466,7 +466,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.1", + "version": "1.2.4", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -504,6 +504,7 @@ "tree-sitter-bash", ], "patchedDependencies": { + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", }, "overrides": { @@ -594,7 +595,7 @@ "@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.98", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/google": "2.0.52", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uuv0RHkdJ5vTzeH1+iuBlv7GAjRcOPd2jiqtGLz6IKOUDH+PRQoE3ExrvOysVnKuhhTBMqvawkktDhMDQE6sVQ=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.103", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.63", "@ai-sdk/google": "2.0.53", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MPZRSVOJFxYGHE4s6XjSWaiUPru7u2i/LUUA1Ih2nzNYZaei8c46Z56imOCD/KQjQX3afRA2iZh6P5McsmwhqA=="], "@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="], @@ -4208,7 +4209,11 @@ "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="], + "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA=="], + + "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ=="], + + "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], diff --git a/flake.lock b/flake.lock index 10fa973cfe..9efa1883b1 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1770073757, - "narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=", + "lastModified": 1770812194, + "narHash": "sha256-OH+lkaIKAvPXR3nITO7iYZwew2nW9Y7Xxq0yfM/UcUU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "47472570b1e607482890801aeaf29bfb749884f6", + "rev": "8482c7ded03bae7550f3d69884f1e611e3bd19e8", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index ef0b63897d..fde268ba83 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-hVf8rBEqy3q4xexOqyKDtKmlMydl1hFoDV0JiEvmfgs=", - "aarch64-linux": "sha256-4m3UZllEmfJXB70cOgIoyWRIYMXxGzzenyOfF3kEQKk=", - "aarch64-darwin": "sha256-27xGR9+FVnC0rsUIyepk2tCP1eEUmGvqWUGAZ+rk7IQ=", - "x86_64-darwin": "sha256-+At7bHSeg6QJu6yGawyvzt53Tu/fddDg6Ms+xhaMLhY=" + "x86_64-linux": "sha256-5pgd2xuvIIkTbIOGIdK5MIXo6O9qRpvk1RKQZ1e1R+8=", + "aarch64-linux": "sha256-FZiHwihM4b82ipQ9XfW08X+sd5CvZhx/+pU/8X1zsns=", + "aarch64-darwin": "sha256-iZv0w1NthV53pY5uvuf3JlI14GeKmCu7WHwGSRdEQeM=", + "x86_64-darwin": "sha256-c3Zm3P1goFPgg3vNAZPMFOhHX/gyTmsCN/PKbGO/v0E=" } } diff --git a/package.json b/package.json index c4408e264b..5d93205056 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "@types/node": "catalog:" }, "patchedDependencies": { - "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch" + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch" } } 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/package.json b/packages/app/package.json index e8c24c08ba..31afda6566 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.1", + "version": "1.2.4", "description": "", "type": "module", "exports": { 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`} /> - + ...} + > + + 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} diff --git a/packages/console/app/package.json b/packages/console/app/package.json index a6267c73ef..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.1", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 85f9c200a7..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.1", + "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 02aa6f76ef..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.1", + "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 115d365f7e..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.1", + "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 858dbf5c4d..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.1", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 7aa3bb90d1..ac7060dd10 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.1", + "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 fdb66d69bf..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.1" +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.1/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.1/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.1/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.1/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.1/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 242ce353f3..3a431e9bdf 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.1", + "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 bf372379bd..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.1", + "version": "1.2.4", "name": "opencode", "type": "module", "license": "MIT", @@ -62,7 +62,7 @@ "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", "@ai-sdk/google": "2.0.52", - "@ai-sdk/google-vertex": "3.0.98", + "@ai-sdk/google-vertex": "3.0.103", "@ai-sdk/groq": "2.0.34", "@ai-sdk/mistral": "2.0.27", "@ai-sdk/openai": "2.0.89", 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/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8091f731f0..853d03c1d8 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -171,7 +171,7 @@ export namespace ProviderTransform { return msgs } - function applyCaching(msgs: ModelMessage[], providerID: string): ModelMessage[] { + function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2) @@ -194,7 +194,7 @@ export namespace ProviderTransform { } for (const msg of unique([...system, ...final])) { - const useMessageLevelOptions = providerID === "anthropic" || providerID.includes("bedrock") + const useMessageLevelOptions = model.providerID === "anthropic" || model.providerID.includes("bedrock") const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0 if (shouldUseContentOptions) { @@ -253,14 +253,15 @@ export namespace ProviderTransform { msgs = unsupportedParts(msgs, model) msgs = normalizeMessages(msgs, model, options) if ( - model.providerID === "anthropic" || - model.api.id.includes("anthropic") || - model.api.id.includes("claude") || - model.id.includes("anthropic") || - model.id.includes("claude") || - model.api.npm === "@ai-sdk/anthropic" + (model.providerID === "anthropic" || + model.api.id.includes("anthropic") || + model.api.id.includes("claude") || + model.id.includes("anthropic") || + model.id.includes("claude") || + model.api.npm === "@ai-sdk/anthropic") && + model.api.npm !== "@ai-sdk/gateway" ) { - msgs = applyCaching(msgs, model.providerID) + msgs = applyCaching(msgs, model) } // Remap providerOptions keys from stored providerID to expected SDK key @@ -360,11 +361,53 @@ export namespace ProviderTransform { switch (model.api.npm) { case "@openrouter/ai-sdk-provider": - if (!model.id.includes("gpt") && !model.id.includes("gemini-3")) return {} + if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("claude")) return {} return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }])) - // TODO: YOU CANNOT SET max_tokens if this is set!!! case "@ai-sdk/gateway": + if (model.id.includes("anthropic")) { + return { + high: { + thinking: { + type: "enabled", + budgetTokens: 16000, + }, + }, + max: { + thinking: { + type: "enabled", + budgetTokens: 31999, + }, + }, + } + } + if (model.id.includes("google")) { + if (id.includes("2.5")) { + return { + high: { + thinkingConfig: { + includeThoughts: true, + thinkingBudget: 16000, + }, + }, + max: { + thinkingConfig: { + includeThoughts: true, + thinkingBudget: 24576, + }, + }, + } + } + return Object.fromEntries( + ["low", "high"].map((effort) => [ + effort, + { + includeThoughts: true, + thinkingLevel: effort, + }, + ]), + ) + } return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) case "@ai-sdk/github-copilot": @@ -720,6 +763,15 @@ export namespace ProviderTransform { result["promptCacheKey"] = input.sessionID } + if (input.model.providerID === "openrouter") { + result["prompt_cache_key"] = input.sessionID + } + if (input.model.api.npm === "@ai-sdk/gateway") { + result["gateway"] = { + caching: "auto", + } + } + return result } @@ -753,7 +805,43 @@ export namespace ProviderTransform { return {} } + // Maps model ID prefix to provider slug used in providerOptions. + // Example: "amazon/nova-2-lite" → "bedrock" + const SLUG_OVERRIDES: Record = { + amazon: "bedrock", + } + export function providerOptions(model: Provider.Model, options: { [x: string]: any }) { + if (model.api.npm === "@ai-sdk/gateway") { + // Gateway providerOptions are split across two namespaces: + // - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.) + // - ``: provider-specific model options (anthropic/openai/...) + // We keep `gateway` as-is and route every other top-level option under the + // model-derived upstream slug. + const i = model.api.id.indexOf("/") + const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined + const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined + const gateway = options.gateway + const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway")) + const has = Object.keys(rest).length > 0 + + const result: Record = {} + if (gateway !== undefined) result.gateway = gateway + + if (has) { + if (slug) { + // Route model-specific options under the provider slug + result[slug] = rest + } else if (gateway && typeof gateway === "object" && !Array.isArray(gateway)) { + result.gateway = { ...gateway, ...rest } + } else { + result.gateway = rest + } + } + + return result + } + const key = sdkKey(model.api.npm) ?? model.providerID return { [key]: options } } diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 2cf5473f22..1195529e06 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -53,15 +53,15 @@ export const SessionRoutes = lazy(() => ), async (c) => { const query = c.req.valid("query") - const term = query.search?.toLowerCase() const sessions: Session.Info[] = [] - for await (const session of Session.list()) { - if (query.directory !== undefined && session.directory !== query.directory) continue - if (query.roots && session.parentID) continue - if (query.start !== undefined && session.time.updated < query.start) continue - if (term !== undefined && !session.title.toLowerCase().includes(term)) continue + for await (const session of Session.list({ + directory: query.directory, + roots: query.roots, + start: query.start, + search: query.search, + limit: query.limit, + })) { sessions.push(session) - if (query.limit !== undefined && sessions.length >= query.limit) break } return c.json(sessions) }, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 38007a0a7f..255f4dd460 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -10,7 +10,7 @@ import { Flag } from "../flag/flag" import { Identifier } from "../id/id" import { Installation } from "../installation" -import { Database, NotFoundError, eq, and, or, like } from "../storage/db" +import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like } from "../storage/db" import { SessionTable, MessageTable, PartTable } from "./session.sql" import { Storage } from "@/storage/storage" import { Log } from "../util/log" @@ -505,20 +505,38 @@ export namespace Session { }, ) - export function* list() { + export function* list(input?: { + directory?: string + roots?: boolean + start?: number + search?: string + limit?: number + }) { const project = Instance.project - // const rel = path.relative(Instance.worktree, Instance.directory) - // const suffix = path.sep + rel + const conditions = [eq(SessionTable.project_id, project.id)] + + if (input?.directory) { + conditions.push(eq(SessionTable.directory, input.directory)) + } + if (input?.roots) { + conditions.push(isNull(SessionTable.parent_id)) + } + if (input?.start) { + conditions.push(gte(SessionTable.time_updated, input.start)) + } + if (input?.search) { + conditions.push(like(SessionTable.title, `%${input.search}%`)) + } + + const limit = input?.limit ?? 100 + const rows = Database.use((db) => db .select() .from(SessionTable) - .where( - and( - eq(SessionTable.project_id, project.id), - // or(eq(SessionTable.directory, Instance.directory), like(SessionTable.directory, `%${suffix}`)), - ), - ) + .where(and(...conditions)) + .orderBy(desc(SessionTable.time_updated)) + .limit(limit) .all(), ) for (const row of rows) { diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 50aa76384e..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> @@ -74,6 +75,7 @@ export namespace Database { sqlite.run("PRAGMA busy_timeout = 5000") sqlite.run("PRAGMA cache_size = -64000") sqlite.run("PRAGMA foreign_keys = ON") + sqlite.run("PRAGMA wal_checkpoint(PASSIVE)") const db = drizzle({ client: sqlite, schema }) 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}`) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 02bb5278fc..3494cb56fd 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -175,6 +175,204 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => { }) }) +describe("ProviderTransform.options - gateway", () => { + const sessionID = "test-session-123" + + const createModel = (id: string) => + ({ + id, + providerID: "vercel", + api: { + id, + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + name: id, + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 200_000, + output: 8192, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2024-01-01", + }) as any + + test("puts gateway defaults under gateway key", () => { + const model = createModel("anthropic/claude-sonnet-4") + const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) + expect(result).toEqual({ + gateway: { + caching: "auto", + }, + }) + }) +}) + +describe("ProviderTransform.providerOptions", () => { + const createModel = (overrides: Partial = {}) => + ({ + id: "test/test-model", + providerID: "test", + api: { + id: "test-model", + url: "https://api.test.com", + npm: "@ai-sdk/openai", + }, + name: "Test Model", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 200_000, + output: 64_000, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2024-01-01", + ...overrides, + }) as any + + test("uses sdk key for non-gateway models", () => { + const model = createModel({ + providerID: "my-bedrock", + api: { + id: "anthropic.claude-sonnet-4", + url: "https://bedrock.aws", + npm: "@ai-sdk/amazon-bedrock", + }, + }) + + expect(ProviderTransform.providerOptions(model, { cachePoint: { type: "default" } })).toEqual({ + bedrock: { cachePoint: { type: "default" } }, + }) + }) + + test("uses gateway model provider slug for gateway models", () => { + const model = createModel({ + providerID: "vercel", + api: { + id: "anthropic/claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { thinking: { type: "enabled", budgetTokens: 12_000 } })).toEqual({ + anthropic: { thinking: { type: "enabled", budgetTokens: 12_000 } }, + }) + }) + + test("falls back to gateway key when gateway api id is unscoped", () => { + const model = createModel({ + id: "anthropic/claude-sonnet-4", + providerID: "vercel", + api: { + id: "claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { thinking: { type: "enabled", budgetTokens: 12_000 } })).toEqual({ + gateway: { thinking: { type: "enabled", budgetTokens: 12_000 } }, + }) + }) + + test("splits gateway routing options from provider-specific options", () => { + const model = createModel({ + providerID: "vercel", + api: { + id: "anthropic/claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect( + ProviderTransform.providerOptions(model, { + gateway: { order: ["vertex", "anthropic"] }, + thinking: { type: "enabled", budgetTokens: 12_000 }, + }), + ).toEqual({ + gateway: { order: ["vertex", "anthropic"] }, + anthropic: { thinking: { type: "enabled", budgetTokens: 12_000 } }, + } as any) + }) + + test("falls back to gateway key when model id has no provider slug", () => { + const model = createModel({ + id: "claude-sonnet-4", + providerID: "vercel", + api: { + id: "claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningEffort: "high" })).toEqual({ + gateway: { reasoningEffort: "high" }, + }) + }) + + test("maps amazon slug to bedrock for provider options", () => { + const model = createModel({ + providerID: "vercel", + api: { + id: "amazon/nova-2-lite", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningConfig: { type: "enabled" } })).toEqual({ + bedrock: { reasoningConfig: { type: "enabled" } }, + }) + }) + + test("uses groq slug for groq models", () => { + const model = createModel({ + providerID: "vercel", + api: { + id: "groq/llama-3.3-70b-versatile", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningFormat: "parsed" })).toEqual({ + groq: { reasoningFormat: "parsed" }, + }) + }) +}) + describe("ProviderTransform.schema - gemini array items", () => { test("adds missing items for array properties", () => { const geminiModel = { @@ -1232,6 +1430,105 @@ describe("ProviderTransform.message - claude w/bedrock custom inference profile" }) }) +describe("ProviderTransform.message - cache control on gateway", () => { + const createModel = (overrides: Partial = {}) => + ({ + id: "anthropic/claude-sonnet-4", + providerID: "vercel", + api: { + id: "anthropic/claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + name: "Claude Sonnet 4", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } }, + limit: { context: 200_000, output: 8192 }, + status: "active", + options: {}, + headers: {}, + ...overrides, + }) as any + + test("gateway does not set cache control for anthropic models", () => { + const model = createModel() + const msgs = [ + { + role: "system", + content: [{ type: "text", text: "You are a helpful assistant" }], + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].content[0].providerOptions).toBeUndefined() + expect(result[0].providerOptions).toBeUndefined() + }) + + test("non-gateway anthropic keeps existing cache control behavior", () => { + const model = createModel({ + providerID: "anthropic", + api: { + id: "claude-sonnet-4", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions).toEqual({ + anthropic: { + cacheControl: { + type: "ephemeral", + }, + }, + openrouter: { + cacheControl: { + type: "ephemeral", + }, + }, + bedrock: { + cachePoint: { + type: "default", + }, + }, + openaiCompatible: { + cache_control: { + type: "ephemeral", + }, + }, + copilot: { + copilot_cache_control: { + type: "ephemeral", + }, + }, + }) + }) +}) + describe("ProviderTransform.variants", () => { const createMockModel = (overrides: Partial = {}): any => ({ id: "test/test-model", @@ -1408,6 +1705,32 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/gateway", () => { + test("anthropic models return anthropic thinking options", () => { + const model = createMockModel({ + id: "anthropic/claude-sonnet-4", + providerID: "gateway", + api: { + id: "anthropic/claude-sonnet-4", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["high", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "enabled", + budgetTokens: 16000, + }, + }) + expect(result.max).toEqual({ + thinking: { + type: "enabled", + budgetTokens: 31999, + }, + }) + }) + test("returns OPENAI_EFFORTS with reasoningEffort", () => { const model = createMockModel({ id: "gateway/gateway-model", diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 623c16a811..675a89011f 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -1,20 +1,17 @@ import { describe, expect, test } from "bun:test" import path from "path" import { Instance } from "../../src/project/instance" -import { Server } from "../../src/server/server" import { Session } from "../../src/session" import { Log } from "../../src/util/log" const projectRoot = path.join(__dirname, "../..") Log.init({ print: false }) -describe("session.list", () => { +describe("Session.list", () => { test("filters by directory", async () => { await Instance.provide({ directory: projectRoot, fn: async () => { - const app = Server.App() - const first = await Session.create({}) const otherDir = path.join(projectRoot, "..", "__session_list_other") @@ -23,17 +20,71 @@ describe("session.list", () => { fn: async () => Session.create({}), }) - const response = await app.request(`/session?directory=${encodeURIComponent(projectRoot)}`) - expect(response.status).toBe(200) - - const body = (await response.json()) as unknown[] - const ids = body - .map((s) => (typeof s === "object" && s && "id" in s ? (s as { id: string }).id : undefined)) - .filter((x): x is string => typeof x === "string") + const sessions = [...Session.list({ directory: projectRoot })] + const ids = sessions.map((s) => s.id) expect(ids).toContain(first.id) expect(ids).not.toContain(second.id) }, }) }) + + test("filters root sessions", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const root = await Session.create({ title: "root-session" }) + const child = await Session.create({ title: "child-session", parentID: root.id }) + + const sessions = [...Session.list({ roots: true })] + const ids = sessions.map((s) => s.id) + + expect(ids).toContain(root.id) + expect(ids).not.toContain(child.id) + }, + }) + }) + + test("filters by start time", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const session = await Session.create({ title: "new-session" }) + const futureStart = Date.now() + 86400000 + + const sessions = [...Session.list({ start: futureStart })] + expect(sessions.length).toBe(0) + }, + }) + }) + + test("filters by search term", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + await Session.create({ title: "unique-search-term-abc" }) + await Session.create({ title: "other-session-xyz" }) + + const sessions = [...Session.list({ search: "unique-search" })] + const titles = sessions.map((s) => s.title) + + expect(titles).toContain("unique-search-term-abc") + expect(titles).not.toContain("other-session-xyz") + }, + }) + }) + + test("respects limit parameter", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + await Session.create({ title: "session-1" }) + await Session.create({ title: "session-2" }) + await Session.create({ title: "session-3" }) + + const sessions = [...Session.list({ limit: 2 })] + expect(sessions.length).toBe(2) + }, + }) + }) }) 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) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 481404969c..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.1", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e141774821..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.1", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index d9234899d9..5a5a0e8359 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.1", + "version": "1.2.4", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index a0e5c2af25..684836335c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.1", + "version": "1.2.4", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index bf0cb074be..74393cecee 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.1", + "version": "1.2.4", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 6bdecea178..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.1", + "version": "1.2.4", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/patches/@openrouter%2Fai-sdk-provider@1.5.4.patch b/patches/@openrouter%2Fai-sdk-provider@1.5.4.patch new file mode 100644 index 0000000000..6226bf790c --- /dev/null +++ b/patches/@openrouter%2Fai-sdk-provider@1.5.4.patch @@ -0,0 +1,128 @@ +diff --git a/dist/index.js b/dist/index.js +index f33510a50d11a2cb92a90ea70cc0ac84c89f29b9..e887a60352c0c08ab794b1e6821854dfeefd20cc 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -2110,7 +2110,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted && !textStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + reasoningStarted = false; + } +@@ -2307,7 +2312,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + } + if (textStarted) { +diff --git a/dist/index.mjs b/dist/index.mjs +index 8a688331b88b4af738ee4ca8062b5f24124d3d81..6310cb8b7c8d0a728d86e1eed09906c6b4c91ae2 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -2075,7 +2075,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted && !textStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + reasoningStarted = false; + } +@@ -2272,7 +2277,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + } + if (textStarted) { +diff --git a/dist/internal/index.js b/dist/internal/index.js +index d40fa66125941155ac13a4619503caba24d89f8a..8dd86d1b473f2fa31c1acd9881d72945b294a197 100644 +--- a/dist/internal/index.js ++++ b/dist/internal/index.js +@@ -2064,7 +2064,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted && !textStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + reasoningStarted = false; + } +@@ -2261,7 +2266,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + } + if (textStarted) { +diff --git a/dist/internal/index.mjs b/dist/internal/index.mjs +index b0ed9d113549c5c55ea3b1e08abb3db6f92ae5a7..5695930a8e038facc071d58a4179a369a29be9c7 100644 +--- a/dist/internal/index.mjs ++++ b/dist/internal/index.mjs +@@ -2030,7 +2030,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted && !textStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + reasoningStarted = false; + } +@@ -2227,7 +2232,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + } + if (textStarted) { diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index d19dcbf467..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.1", + "version": "1.2.4", "publisher": "sst-dev", "repository": { "type": "git",