From ef205c366062fbf89ec49c9fc7f2a4b4c5223614 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 14 Feb 2026 12:29:01 -0600 Subject: [PATCH 01/18] bump vertex ai packages (#13625) --- bun.lock | 10 +++++++--- packages/opencode/package.json | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 061e36e143..2e5d13dd82 100644 --- a/bun.lock +++ b/bun.lock @@ -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", @@ -594,7 +594,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 +4208,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/packages/opencode/package.json b/packages/opencode/package.json index bf372379bd..bb1ce546b5 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -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", From 759ec104b6e537235afd3177acd28b6c9694e496 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 14 Feb 2026 12:32:29 -0600 Subject: [PATCH 02/18] fix vercel gateway variants (#13541) Co-authored-by: Benjamin Woodruff " --- packages/opencode/src/provider/transform.ts | 97 +++++- .../opencode/test/provider/transform.test.ts | 293 ++++++++++++++++++ 2 files changed, 380 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8091f731f0..b4eb14a2ff 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 @@ -363,8 +364,50 @@ export namespace ProviderTransform { if (!model.id.includes("gpt") && !model.id.includes("gemini-3")) 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,12 @@ export namespace ProviderTransform { result["promptCacheKey"] = input.sessionID } + if (input.model.api.npm === "@ai-sdk/gateway") { + result["gateway"] = { + caching: "auto", + } + } + return result } @@ -754,6 +803,34 @@ export namespace ProviderTransform { } 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 + // - ``: provider-specific model options (anthropic/openai/...) + // We keep `gateway` as-is and route every other top-level option under the + // model-derived upstream slug so variants/options can stay flat internally. + const i = model.api.id.indexOf("/") + const slug = i > 0 ? model.api.id.slice(0, i) : 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) { + 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/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 02bb5278fc..99acfcc817 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -175,6 +175,174 @@ 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" }, + }) + }) +}) + describe("ProviderTransform.schema - gemini array items", () => { test("adds missing items for array properties", () => { const geminiModel = { @@ -1232,6 +1400,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 +1675,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", From 306fc77076fa3ac0930efefc842e2f61cd5ddd19 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 14 Feb 2026 18:38:18 +0000 Subject: [PATCH 03/18] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index ef0b63897d..9abd070a8c 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-3pSRWyuQUn2s889IHKcL58BfO3y1dgZMOrqYgHgeHqc=", + "aarch64-linux": "sha256-AuBO4b9JLxOL+GJTK4dLwImZUx+Kpv4JdaX0FqKyLuI=", + "aarch64-darwin": "sha256-9toJpgkLovUYCoDa7mdGHa3ElVg7s6s+8rQSO3LH58w=", + "x86_64-darwin": "sha256-Zjt2gFBuXXeSY4dGZQQNBs7cDFlcQ9yiIVMc52lxb1Y=" } } From 68bb8ce1da922229e6ab4dde4207b431cf9d76a8 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 14 Feb 2026 13:40:49 -0500 Subject: [PATCH 04/18] core: filter sessions at database level to improve session list loading performance --- .../opencode/src/server/routes/session.ts | 14 +++---- packages/opencode/src/session/index.ts | 38 ++++++++++++++----- 2 files changed, 35 insertions(+), 17 deletions(-) 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) { From 8631d6c01d8c8f5e8c616e09e85e5a27791d1a56 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 14 Feb 2026 13:43:41 -0500 Subject: [PATCH 05/18] core: add comprehensive test coverage for Session.list() filters Adds test cases for filtering sessions by directory, root sessions only, start time, search terms, and result limits to ensure the listing functionality works correctly for all filter combinations. --- .../opencode/test/server/session-list.test.ts | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) 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) + }, + }) + }) }) From 3b6b3e6fc8a8a4da5798c9f00027e954263a483e Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 14 Feb 2026 19:08:58 +0000 Subject: [PATCH 06/18] release: v1.2.2 --- 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 2e5d13dd82..2b4c4993aa 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.1", + "version": "1.2.2", "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.2", "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.2", "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.2", "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.2", "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.2", "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.2", "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.2", "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.2", "bin": { "opencode": "./bin/opencode", }, @@ -369,7 +369,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.1", + "version": "1.2.2", "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.2", "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.2", "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.2", "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.2", "dependencies": { "zod": "catalog:", }, @@ -466,7 +466,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.1", + "version": "1.2.2", "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 e8c24c08ba..b2f2f23246 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.2", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index a6267c73ef..ea16083326 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.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 85f9c200a7..8c71ca77b2 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.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 02aa6f76ef..debba52bab 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.2", "$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..e361cc0e73 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.2", "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..f9ab28cfca 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.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 7aa3bb90d1..173688eb1d 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.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index fdb66d69bf..9498ce1269 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.2" 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.2/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.2/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.2/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.2/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.2/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 242ce353f3..1718b791a6 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.2", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index bb1ce546b5..728ec38385 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.2", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 481404969c..d078c0028e 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.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e141774821..a03d8382db 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.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index d9234899d9..8a6d289ec0 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.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index a0e5c2af25..8771b397ce 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.2", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index bf0cb074be..c610140398 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.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 6bdecea178..79858de586 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.2", "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 d19dcbf467..e91d1a324d 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.2", "publisher": "sst-dev", "repository": { "type": "git", From 933a491adeeed875d3ba4cbc88ed301a60456734 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:18:52 -0600 Subject: [PATCH 07/18] fix: ensure vercel variants pass amazon models under bedrock key (#13631) --- packages/opencode/src/provider/transform.ts | 14 +++++++-- .../opencode/test/provider/transform.test.ts | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index b4eb14a2ff..1c92f528ee 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -802,15 +802,22 @@ 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 + // - `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 so variants/options can stay flat internally. + // model-derived upstream slug. const i = model.api.id.indexOf("/") - const slug = i > 0 ? model.api.id.slice(0, i) : undefined + 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 @@ -820,6 +827,7 @@ export namespace ProviderTransform { 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 } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 99acfcc817..3494cb56fd 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -341,6 +341,36 @@ describe("ProviderTransform.providerOptions", () => { 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", () => { From 575f2cf2a5e2246175a38dbf96bb1fed33186edc Mon Sep 17 00:00:00 2001 From: Alberto Valverde Date: Sat, 14 Feb 2026 20:21:31 +0100 Subject: [PATCH 08/18] chore: bump nixpkgs to get bun 1.3.9 (#13302) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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": { From 67c985ce82b3a0ef3b22bef435f58884a3aab990 Mon Sep 17 00:00:00 2001 From: Dax Date: Sat, 14 Feb 2026 14:33:08 -0500 Subject: [PATCH 09/18] fix: add WAL checkpoint on database open (#13633) --- packages/opencode/src/storage/db.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 50aa76384e..387e93b376 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -74,6 +74,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 }) From 839c5cda12fa978d4c7ba85c7cf51600ec853bc8 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:30:07 -0600 Subject: [PATCH 10/18] fix: ensure anthropic models on OR also have variant support (#13498) --- bun.lock | 1 + package.json | 3 +- packages/opencode/src/provider/transform.ts | 5 +- .../@openrouter%2Fai-sdk-provider@1.5.4.patch | 128 ++++++++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 patches/@openrouter%2Fai-sdk-provider@1.5.4.patch diff --git a/bun.lock b/bun.lock index 2b4c4993aa..c29176596d 100644 --- a/bun.lock +++ b/bun.lock @@ -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": { 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/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 1c92f528ee..853d03c1d8 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -361,7 +361,7 @@ 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 } }])) case "@ai-sdk/gateway": @@ -763,6 +763,9 @@ 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", 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) { From 7911cb62abe424337d934c03e48bc431199401e7 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 14 Feb 2026 20:38:57 +0000 Subject: [PATCH 11/18] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 9abd070a8c..fde268ba83 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-3pSRWyuQUn2s889IHKcL58BfO3y1dgZMOrqYgHgeHqc=", - "aarch64-linux": "sha256-AuBO4b9JLxOL+GJTK4dLwImZUx+Kpv4JdaX0FqKyLuI=", - "aarch64-darwin": "sha256-9toJpgkLovUYCoDa7mdGHa3ElVg7s6s+8rQSO3LH58w=", - "x86_64-darwin": "sha256-Zjt2gFBuXXeSY4dGZQQNBs7cDFlcQ9yiIVMc52lxb1Y=" + "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=" } } From c190f5f611c1520a553facc362749f8aefaa5005 Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 15 Feb 2026 00:34:56 +0000 Subject: [PATCH 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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 18/18] 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",