From 3a669d528b346f57bace0e54eedaf59d42030b1b Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:03:05 -0500 Subject: [PATCH 001/140] fix(provider): enable sonnet 5 adaptive thinking (#34673) --- packages/opencode/src/provider/transform.ts | 10 ++- .../opencode/test/provider/transform.test.ts | 77 +++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index e459380faae4..4bb81b753b52 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -605,8 +605,14 @@ function anthropicOpus47OrLater(apiId: string) { return major > 4 || (major === 4 && minor >= 7) } +function anthropicSonnet5OrLater(apiId: string) { + const version = /sonnet-(\d+)(?:[.@-]|$)|claude-(\d+)-sonnet(?:[.@-]|$)/i.exec(apiId) + if (!version) return false + return Number(version[1] ?? version[2]) >= 5 +} + function anthropicAdaptiveEfforts(apiId: string): string[] | null { - if (anthropicOpus47OrLater(apiId) || apiId.includes("fable-5")) { + if (anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")) { return ["low", "medium", "high", "xhigh", "max"] } if ( @@ -620,7 +626,7 @@ function anthropicAdaptiveEfforts(apiId: string): string[] | null { } function anthropicOmitsThinking(apiId: string) { - return anthropicOpus47OrLater(apiId) || apiId.includes("fable-5") + return anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5") } function googleThinkingLevelEfforts(apiId: string) { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index aa1b8eee1b1a..dcbaf8171609 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3290,6 +3290,27 @@ describe("ProviderTransform.variants", () => { }) }) + test("anthropic sonnet 5 returns adaptive thinking options with xhigh", () => { + const model = createMockModel({ + id: "anthropic/claude-sonnet-5", + providerID: "gateway", + api: { + id: "anthropic/claude-sonnet-5", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "adaptive", + display: "summarized", + }, + effort: "high", + }) + }) + test("anthropic opus 4.6 omits display so it keeps the summarized default", () => { const model = createMockModel({ id: "anthropic/claude-opus-4-6", @@ -3877,6 +3898,12 @@ describe("ProviderTransform.variants", () => { efforts: ["low", "medium", "high", "xhigh", "max"], expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, }, + { + name: "sonnet 5", + apiIds: ["claude-sonnet-5", "claude-sonnet-5-20260630"], + efforts: ["low", "medium", "high", "xhigh", "max"], + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, + }, { name: "fable 5", apiIds: ["claude-fable-5"], @@ -3974,6 +4001,28 @@ describe("ProviderTransform.variants", () => { effort: "high", }) }) + + test("sonnet 5 uses adaptive reasoning for Vertex model IDs", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "google-vertex-anthropic/claude-sonnet-5@default", + providerID: "google-vertex-anthropic", + api: { + id: "claude-sonnet-5@default", + url: "https://us-central1-aiplatform.googleapis.com", + npm: "@ai-sdk/google-vertex/anthropic", + }, + }), + ) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "adaptive", + display: "summarized", + }, + effort: "high", + }) + }) }) describe("@ai-sdk/amazon-bedrock", () => { @@ -4047,6 +4096,28 @@ describe("ProviderTransform.variants", () => { }) }) + test("anthropic sonnet 5 returns adaptive reasoning options with xhigh", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "bedrock/anthropic-claude-sonnet-5", + providerID: "bedrock", + api: { + id: "anthropic.claude-sonnet-5", + url: "https://bedrock.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + }), + ) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + reasoningConfig: { + type: "adaptive", + maxReasoningEffort: "high", + display: "summarized", + }, + }) + }) + test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => { const model = createMockModel({ id: "bedrock/llama-4", @@ -4229,6 +4300,12 @@ describe("ProviderTransform.variants", () => { efforts: ["low", "medium", "high", "xhigh", "max"], thinking: { type: "adaptive", display: "summarized" }, }, + { + name: "sonnet 5", + apiIds: ["anthropic--claude-sonnet-5", "anthropic--claude-5-sonnet"], + efforts: ["low", "medium", "high", "xhigh", "max"], + thinking: { type: "adaptive", display: "summarized" }, + }, ]) { for (const apiId of testCase.apiIds) { test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => { From afff74eb2c9fc3808a9795f365707f32853099e9 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 30 Jun 2026 19:48:01 +0000 Subject: [PATCH 002/140] sync release versions for v1.17.12 --- bun.lock | 54 ++++++++++----------- packages/app/package.json | 2 +- packages/cli/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/console/support/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/session-ui/package.json | 2 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 29 files changed, 55 insertions(+), 55 deletions(-) diff --git a/bun.lock b/bun.lock index aabc44e32fbe..c72fe6a2806d 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@dnd-kit/abstract": "0.5.0", "@dnd-kit/dom": "0.5.0", @@ -92,7 +92,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.17.11", + "version": "1.17.12", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -140,7 +140,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -176,7 +176,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -203,7 +203,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -225,7 +225,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -249,7 +249,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -269,7 +269,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.17.11", + "version": "1.17.12", "bin": { "opencode": "./bin/opencode", }, @@ -363,7 +363,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -417,7 +417,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -431,7 +431,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "effect": "catalog:", }, @@ -443,7 +443,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -475,7 +475,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -491,7 +491,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -522,7 +522,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -541,7 +541,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.17.11", + "version": "1.17.12", "bin": { "opencode": "./bin/opencode", }, @@ -671,7 +671,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -747,7 +747,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "cross-spawn": "catalog:", }, @@ -762,7 +762,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -777,7 +777,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -821,7 +821,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -834,7 +834,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@ibm/plex": "6.4.1", "@opencode-ai/stats-core": "workspace:*", @@ -867,7 +867,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -886,7 +886,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -927,7 +927,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -954,7 +954,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1005,7 +1005,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.17.11", + "version": "1.17.12", "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 52420afedc1e..d9317f39bca7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.17.11", + "version": "1.17.12", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 1ea11f0864c6..03667a524a6b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.17.11", + "version": "1.17.12", "type": "module", "license": "MIT", "bin": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 1011a5a59af8..5a1aeab61d7e 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.17.11", + "version": "1.17.12", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index bc7c31cc0b65..239f78d1ed79 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.17.11", + "version": "1.17.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 3cb120ed6976..ca0020fea238 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.17.11", + "version": "1.17.12", "$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 da8c06a6110a..53ef65e162fe 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.17.11", + "version": "1.17.12", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index 5c081f7f4d49..6d4a3066fb9c 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.17.11", + "version": "1.17.12", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index c540e442927a..61e73adcd85c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.11", + "version": "1.17.12", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 5a239863a8ef..2d040ea5c399 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.17.11", + "version": "1.17.12", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index 0e66036f0f94..fcd1d80dbf37 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.11", + "version": "1.17.12", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 0bf7116aa64a..8954d9cbdf14 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.11", + "version": "1.17.12", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 03183d33a53c..2c0c92caa6b5 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.17.11", + "version": "1.17.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index ba0867556735..dd2763d345c0 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.17.11", + "version": "1.17.12", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 09cf673afc4c..2777962469d6 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.11", + "version": "1.17.12", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index d5b370c7d46c..a80f74a36424 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.11", + "version": "1.17.12", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 09667e590722..124fc98a4412 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.11", + "version": "1.17.12", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 6e3c59bea3b8..574fcfa275fb 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.17.11", + "version": "1.17.12", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index de6c62bc2662..63b5609171df 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.17.11", + "version": "1.17.12", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index b0c4d948cb31..4d254218426f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.17.11", + "version": "1.17.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index f6c70cf5ae7d..f6da78f1b09c 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.17.11", + "version": "1.17.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index a815936e5d5e..030525dffc36 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.17.11", + "version": "1.17.12", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index d3b83ac4b32f..5021abc6666e 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.17.11", + "version": "1.17.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index 3d93cba551fd..cdffcae183db 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.17.11", + "version": "1.17.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 86ad57f337eb..35c572dedf8d 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.17.11", + "version": "1.17.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/package.json b/packages/tui/package.json index ee3972a2c602..182d266bf5ce 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.17.11", + "version": "1.17.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index 6312702331af..bd306af535ce 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.17.11", + "version": "1.17.12", "type": "module", "license": "MIT", "repository": { diff --git a/packages/web/package.json b/packages/web/package.json index 4c924ea504b0..22db2b0ad451 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.17.11", + "version": "1.17.12", "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 8a151aae1e19..c5006bfef67e 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.17.11", + "version": "1.17.12", "publisher": "sst-dev", "repository": { "type": "git", From 928c37bc023e9b0705355f970c6fa27545a84f0f Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 30 Jun 2026 19:56:25 -0400 Subject: [PATCH 003/140] zen: kk2.7, mm3, sonnet5 --- packages/web/src/content/docs/ar/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/bs/zen.mdx | 29 ++++++----- packages/web/src/content/docs/da/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/de/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/es/zen.mdx | 29 ++++++----- packages/web/src/content/docs/fr/zen.mdx | 29 ++++++----- packages/web/src/content/docs/it/zen.mdx | 29 ++++++----- packages/web/src/content/docs/ja/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/ko/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/nb/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/pl/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/pt-br/zen.mdx | 29 ++++++----- packages/web/src/content/docs/ru/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/th/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/tr/zen.mdx | 29 ++++++----- packages/web/src/content/docs/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/zh-cn/zen.mdx | 55 +++++++++++---------- packages/web/src/content/docs/zh-tw/zen.mdx | 55 +++++++++++---------- 18 files changed, 444 insertions(+), 390 deletions(-) diff --git a/packages/web/src/content/docs/ar/zen.mdx b/packages/web/src/content/docs/ar/zen.mdx index 63489b1c2f91..c0f75fb9e997 100644 --- a/packages/web/src/content/docs/ar/zen.mdx +++ b/packages/web/src/content/docs/ar/zen.mdx @@ -77,12 +77,10 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -92,6 +90,7 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -99,6 +98,7 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -131,13 +131,15 @@ https://opencode.ai/zen/v1/models | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -150,12 +152,10 @@ https://opencode.ai/zen/v1/models | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -217,23 +217,26 @@ https://opencode.ai/zen/v1/models ### النماذج المهملة -| النموذج | تاريخ الإيقاف | -| ------------------ | -------------- | -| GPT 5.2 Codex | 23 يوليو 2026 | -| GPT 5.1 Codex | 23 يوليو 2026 | -| GPT 5.1 Codex Max | 23 يوليو 2026 | -| GPT 5.1 Codex Mini | 23 يوليو 2026 | -| GPT 5 Codex | 23 يوليو 2026 | -| Claude Sonnet 4 | 15 يونيو 2026 | -| GLM 5 | 14 مايو 2026 | -| MiniMax M2.1 | 15 مارس 2026 | -| GLM 4.7 | 15 مارس 2026 | -| GLM 4.6 | 15 مارس 2026 | -| Gemini 3 Pro | 9 مارس 2026 | -| Kimi K2 Thinking | 6 مارس 2026 | -| Kimi K2 | 6 مارس 2026 | -| Claude Haiku 3.5 | 16 فبراير 2026 | -| Qwen3 Coder 480B | 6 فبراير 2026 | +| النموذج | تاريخ الإيقاف | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/bs/zen.mdx b/packages/web/src/content/docs/bs/zen.mdx index c07afb37652d..d19e13c6e958 100644 --- a/packages/web/src/content/docs/bs/zen.mdx +++ b/packages/web/src/content/docs/bs/zen.mdx @@ -82,12 +82,10 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa. | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -97,6 +95,7 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa. | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,6 +103,7 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa. | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -138,13 +138,15 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -157,12 +159,10 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -235,16 +235,19 @@ vam ipak može naplatiti više od $20 ako vam stanje padne ispod $5. | GPT 5.1 Codex Max | July 23, 2026 | | GPT 5.1 Codex Mini | July 23, 2026 | | GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | | Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | | MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | | GLM 4.7 | March 15, 2026 | | GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | +| Kimi K2.5 | August 5, 2026 | | Kimi K2 Thinking | March 6, 2026 | | Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/da/zen.mdx b/packages/web/src/content/docs/da/zen.mdx index 198796b4515f..346af2b51b03 100644 --- a/packages/web/src/content/docs/da/zen.mdx +++ b/packages/web/src/content/docs/da/zen.mdx @@ -82,12 +82,10 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints. | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -97,6 +95,7 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints. | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,6 +103,7 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints. | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -138,13 +138,15 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -157,12 +159,10 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -227,23 +227,26 @@ at opkræve dig mere end $20, hvis din saldo kommer under $5. ### Udfasede modeller -| Model | Udfasningsdato | -| ------------------ | -------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Model | Udfasningsdato | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/de/zen.mdx b/packages/web/src/content/docs/de/zen.mdx index 4a5a079abd1d..7a2fa0bfc1e0 100644 --- a/packages/web/src/content/docs/de/zen.mdx +++ b/packages/web/src/content/docs/de/zen.mdx @@ -73,12 +73,10 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen. | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -88,6 +86,7 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen. | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -95,6 +94,7 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen. | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -127,13 +127,15 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -146,12 +148,10 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -213,23 +213,26 @@ Angenommen, du setzt ein monatliches Nutzungslimit von $20, dann wird Zen in ein ### Veraltete Modelle -| Model | Deprecation date | -| ------------------ | ---------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Model | Deprecation date | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/es/zen.mdx b/packages/web/src/content/docs/es/zen.mdx index 7b96155c90e6..e5e4452c6be5 100644 --- a/packages/web/src/content/docs/es/zen.mdx +++ b/packages/web/src/content/docs/es/zen.mdx @@ -82,12 +82,10 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -97,6 +95,7 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,6 +103,7 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -138,13 +138,15 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -157,12 +159,10 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -234,16 +234,19 @@ cobrándote más de $20 si tu saldo baja de $5. | GPT 5.1 Codex Max | July 23, 2026 | | GPT 5.1 Codex Mini | July 23, 2026 | | GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | | Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | | MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | | GLM 4.7 | March 15, 2026 | | GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | +| Kimi K2.5 | August 5, 2026 | | Kimi K2 Thinking | March 6, 2026 | | Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/fr/zen.mdx b/packages/web/src/content/docs/fr/zen.mdx index f59181ddf480..8342f76b8992 100644 --- a/packages/web/src/content/docs/fr/zen.mdx +++ b/packages/web/src/content/docs/fr/zen.mdx @@ -73,12 +73,10 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -88,6 +86,7 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -95,6 +94,7 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -127,13 +127,15 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -146,12 +148,10 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -220,16 +220,19 @@ Par exemple, si vous définissez une limite d'utilisation mensuelle à $20, Zen | GPT 5.1 Codex Max | July 23, 2026 | | GPT 5.1 Codex Mini | July 23, 2026 | | GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | | Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | | MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | | GLM 4.7 | March 15, 2026 | | GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | +| Kimi K2.5 | August 5, 2026 | | Kimi K2 Thinking | March 6, 2026 | | Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/it/zen.mdx b/packages/web/src/content/docs/it/zen.mdx index 76f47118aa5d..77f019d43a92 100644 --- a/packages/web/src/content/docs/it/zen.mdx +++ b/packages/web/src/content/docs/it/zen.mdx @@ -82,12 +82,10 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API. | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -97,6 +95,7 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API. | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,6 +103,7 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API. | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -138,13 +138,15 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -157,12 +159,10 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -234,16 +234,19 @@ per addebitarti più di $20 se il tuo saldo scende sotto $5. | GPT 5.1 Codex Max | July 23, 2026 | | GPT 5.1 Codex Mini | July 23, 2026 | | GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | | Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | | MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | | GLM 4.7 | March 15, 2026 | | GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | +| Kimi K2.5 | August 5, 2026 | | Kimi K2 Thinking | March 6, 2026 | | Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/ja/zen.mdx b/packages/web/src/content/docs/ja/zen.mdx index 0c92aa1e3164..72df27be4c00 100644 --- a/packages/web/src/content/docs/ja/zen.mdx +++ b/packages/web/src/content/docs/ja/zen.mdx @@ -73,12 +73,10 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動 | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -88,6 +86,7 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動 | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -95,6 +94,7 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動 | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -127,13 +127,15 @@ https://opencode.ai/zen/v1/models | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -146,12 +148,10 @@ https://opencode.ai/zen/v1/models | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -213,23 +213,26 @@ https://opencode.ai/zen/v1/models ### 非推奨モデル -| Model | Deprecation date | -| ------------------ | ---------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Model | Deprecation date | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/ko/zen.mdx b/packages/web/src/content/docs/ko/zen.mdx index b21af9452561..31b07f0a71f2 100644 --- a/packages/web/src/content/docs/ko/zen.mdx +++ b/packages/web/src/content/docs/ko/zen.mdx @@ -73,12 +73,10 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다. | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -88,6 +86,7 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다. | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -95,6 +94,7 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다. | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -127,13 +127,15 @@ https://opencode.ai/zen/v1/models | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -146,12 +148,10 @@ https://opencode.ai/zen/v1/models | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -213,23 +213,26 @@ https://opencode.ai/zen/v1/models ### 지원 중단 모델 -| 모델 | 지원 중단 날짜 | -| ------------------ | -------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| 모델 | 지원 중단 날짜 | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/nb/zen.mdx b/packages/web/src/content/docs/nb/zen.mdx index 6121817a47fb..b08ee2e5f172 100644 --- a/packages/web/src/content/docs/nb/zen.mdx +++ b/packages/web/src/content/docs/nb/zen.mdx @@ -82,12 +82,10 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter. | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -97,6 +95,7 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter. | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,6 +103,7 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter. | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -138,13 +138,15 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -157,12 +159,10 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -227,23 +227,26 @@ med å belaste deg mer enn $20 hvis saldoen din går under $5. ### Utfasede modeller -| Modell | Utfasingsdato | -| ------------------ | -------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Modell | Utfasingsdato | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/pl/zen.mdx b/packages/web/src/content/docs/pl/zen.mdx index 41b1b66341c0..14f77c843e6f 100644 --- a/packages/web/src/content/docs/pl/zen.mdx +++ b/packages/web/src/content/docs/pl/zen.mdx @@ -82,12 +82,10 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API. | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -97,6 +95,7 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API. | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,6 +103,7 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API. | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -138,13 +138,15 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -157,12 +159,10 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -228,23 +228,26 @@ ostatecznie obciążyć Cię kwotą wyższą niż $20, jeśli Twoje saldo spadni ### Wycofane modele -| Model | Data wycofania | -| ------------------ | -------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Model | Data wycofania | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/pt-br/zen.mdx b/packages/web/src/content/docs/pt-br/zen.mdx index 4672c1f41e8c..493b5eb322e5 100644 --- a/packages/web/src/content/docs/pt-br/zen.mdx +++ b/packages/web/src/content/docs/pt-br/zen.mdx @@ -73,12 +73,10 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API. | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -88,6 +86,7 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API. | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -95,6 +94,7 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API. | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -127,13 +127,15 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -146,12 +148,10 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -220,16 +220,19 @@ Por exemplo, digamos que você defina um limite mensal de uso de $20; o Zen não | GPT 5.1 Codex Max | July 23, 2026 | | GPT 5.1 Codex Mini | July 23, 2026 | | GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | | Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | | MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | | GLM 4.7 | March 15, 2026 | | GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | +| Kimi K2.5 | August 5, 2026 | | Kimi K2 Thinking | March 6, 2026 | | Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/ru/zen.mdx b/packages/web/src/content/docs/ru/zen.mdx index bfd4d380e834..218d4c029562 100644 --- a/packages/web/src/content/docs/ru/zen.mdx +++ b/packages/web/src/content/docs/ru/zen.mdx @@ -82,12 +82,10 @@ OpenCode Zen работает как любой другой провайдер | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -97,6 +95,7 @@ OpenCode Zen работает как любой другой провайдер | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,6 +103,7 @@ OpenCode Zen работает как любой другой провайдер | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -138,13 +138,15 @@ https://opencode.ai/zen/v1/models | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -157,12 +159,10 @@ https://opencode.ai/zen/v1/models | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -227,23 +227,26 @@ https://opencode.ai/zen/v1/models ### Устаревшие модели -| Модель | Дата устаревания | -| ------------------ | ---------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Модель | Дата устаревания | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/th/zen.mdx b/packages/web/src/content/docs/th/zen.mdx index d4f679b9e99e..4f2c3aebd452 100644 --- a/packages/web/src/content/docs/th/zen.mdx +++ b/packages/web/src/content/docs/th/zen.mdx @@ -75,12 +75,10 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -90,6 +88,7 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -97,6 +96,7 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -129,13 +129,15 @@ https://opencode.ai/zen/v1/models | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -148,12 +150,10 @@ https://opencode.ai/zen/v1/models | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -215,23 +215,26 @@ https://opencode.ai/zen/v1/models ### โมเดลที่ถูกยกเลิก -| Model | Deprecation date | -| ------------------ | ---------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Model | Deprecation date | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/tr/zen.mdx b/packages/web/src/content/docs/tr/zen.mdx index b9529f20124b..8655d23412c0 100644 --- a/packages/web/src/content/docs/tr/zen.mdx +++ b/packages/web/src/content/docs/tr/zen.mdx @@ -73,12 +73,10 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -88,6 +86,7 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -95,6 +94,7 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -127,13 +127,15 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -146,12 +148,10 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -220,16 +220,19 @@ Ayrıca tüm çalışma alanı için ve ekibinizdeki her üye için aylık kulla | GPT 5.1 Codex Max | July 23, 2026 | | GPT 5.1 Codex Mini | July 23, 2026 | | GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | | Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | | MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | | GLM 4.7 | March 15, 2026 | | GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | +| Kimi K2.5 | August 5, 2026 | | Kimi K2 Thinking | March 6, 2026 | | Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 1f9be8ce92b3..49ca11afe35b 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -82,12 +82,10 @@ You can also access our models through the following API endpoints. | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -97,6 +95,7 @@ You can also access our models through the following API endpoints. | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,6 +103,7 @@ You can also access our models through the following API endpoints. | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -138,13 +138,15 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -157,12 +159,10 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -227,23 +227,26 @@ charging you more than $20 if your balance goes below $5. ### Deprecated models -| Model | Deprecation date | -| ------------------ | ---------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| Model | Deprecation date | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/zh-cn/zen.mdx b/packages/web/src/content/docs/zh-cn/zen.mdx index c8f67de852a1..55a55308df1a 100644 --- a/packages/web/src/content/docs/zh-cn/zen.mdx +++ b/packages/web/src/content/docs/zh-cn/zen.mdx @@ -73,12 +73,10 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -88,6 +86,7 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -95,6 +94,7 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -127,13 +127,15 @@ https://opencode.ai/zen/v1/models | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -146,12 +148,10 @@ https://opencode.ai/zen/v1/models | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -213,23 +213,26 @@ https://opencode.ai/zen/v1/models ### 已弃用模型 -| 模型 | 弃用日期 | -| ------------------ | -------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| 模型 | 弃用日期 | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- diff --git a/packages/web/src/content/docs/zh-tw/zen.mdx b/packages/web/src/content/docs/zh-tw/zen.mdx index 57fe1da1fc6b..778df663bf51 100644 --- a/packages/web/src/content/docs/zh-tw/zen.mdx +++ b/packages/web/src/content/docs/zh-tw/zen.mdx @@ -77,12 +77,10 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。 | Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | @@ -92,6 +90,7 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。 | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -99,6 +98,7 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。 | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -132,13 +132,15 @@ https://opencode.ai/zen/v1/models | MiMo-V2.5 Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | -| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M3 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | GLM 5.2 | $1.40 | $4.40 | $0.26 | - | | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | @@ -151,12 +153,10 @@ https://opencode.ai/zen/v1/models | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | | Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | @@ -220,23 +220,26 @@ https://opencode.ai/zen/v1/models ### 已棄用模型 -| 模型 | 棄用日期 | -| ------------------ | -------------- | -| GPT 5.2 Codex | July 23, 2026 | -| GPT 5.1 Codex | July 23, 2026 | -| GPT 5.1 Codex Max | July 23, 2026 | -| GPT 5.1 Codex Mini | July 23, 2026 | -| GPT 5 Codex | July 23, 2026 | -| Claude Sonnet 4 | June 15, 2026 | -| GLM 5 | May 14, 2026 | -| MiniMax M2.1 | March 15, 2026 | -| GLM 4.7 | March 15, 2026 | -| GLM 4.6 | March 15, 2026 | -| Gemini 3 Pro | March 9, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | -| Claude Haiku 3.5 | Feb 16, 2026 | -| Qwen3 Coder 480B | Feb 6, 2026 | +| 模型 | 棄用日期 | +| ------------------ | ----------------- | +| GPT 5.2 Codex | July 23, 2026 | +| GPT 5.1 Codex | July 23, 2026 | +| GPT 5.1 Codex Max | July 23, 2026 | +| GPT 5.1 Codex Mini | July 23, 2026 | +| GPT 5 Codex | July 23, 2026 | +| Claude Opus 4.1 | August 5, 2026 | +| Claude Sonnet 4 | June 15, 2026 | +| Claude Haiku 3.5 | February 16, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| MiniMax M2.5 | August 5, 2026 | +| MiniMax M2.1 | March 15, 2026 | +| GLM 5 | May 14, 2026 | +| GLM 4.7 | March 15, 2026 | +| GLM 4.6 | March 15, 2026 | +| Kimi K2.5 | August 5, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | February 6, 2026 | --- From c88ca972cfea26867d9d0cf6f96eac2f5df0f3e0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 30 Jun 2026 23:59:52 +0000 Subject: [PATCH 004/140] chore: generate --- packages/web/src/content/docs/ar/zen.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/ar/zen.mdx b/packages/web/src/content/docs/ar/zen.mdx index c0f75fb9e997..aa827f6304a6 100644 --- a/packages/web/src/content/docs/ar/zen.mdx +++ b/packages/web/src/content/docs/ar/zen.mdx @@ -217,7 +217,7 @@ https://opencode.ai/zen/v1/models ### النماذج المهملة -| النموذج | تاريخ الإيقاف | +| النموذج | تاريخ الإيقاف | | ------------------ | ----------------- | | GPT 5.2 Codex | July 23, 2026 | | GPT 5.1 Codex | July 23, 2026 | From f1407e41c4743cb40a20fd3417df60eee5e3c500 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:19:02 -0500 Subject: [PATCH 005/140] fix(core): stop replaying stale GitHub Copilot Responses item IDs (#34686) --- .../convert-to-openai-responses-input.ts | 10 +- .../openai-responses-language-model.ts | 40 ++-- .../openai-responses-language-model.test.ts | 206 ++++++++++++++++++ packages/opencode/src/provider/transform.ts | 4 +- .../opencode/test/provider/transform.test.ts | 76 +++++++ 5 files changed, 310 insertions(+), 26 deletions(-) create mode 100644 packages/core/test/github-copilot/openai-responses-language-model.test.ts diff --git a/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts b/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts index 83e46015dd1e..1e4f86d93355 100644 --- a/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts +++ b/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts @@ -86,7 +86,7 @@ export async function convertToOpenAIResponsesInput({ : { image_url: `data:${mediaType};base64,${convertToBase64(part.data)}`, }), - detail: part.providerOptions?.openai?.imageDetail, + detail: part.providerOptions?.copilot?.imageDetail, } } else if (part.mediaType === "application/pdf") { if (part.data instanceof URL) { @@ -127,7 +127,7 @@ export async function convertToOpenAIResponsesInput({ input.push({ role: "assistant", content: [{ type: "output_text", text: part.text }], - id: (part.providerOptions?.openai?.itemId as string) ?? undefined, + id: (part.providerOptions?.copilot?.itemId as string) ?? undefined, }) break } @@ -143,7 +143,7 @@ export async function convertToOpenAIResponsesInput({ input.push({ type: "local_shell_call", call_id: part.toolCallId, - id: (part.providerOptions?.openai?.itemId as string) ?? undefined, + id: (part.providerOptions?.copilot?.itemId as string) ?? undefined, action: { type: "exec", command: parsedInput.action.command, @@ -162,7 +162,7 @@ export async function convertToOpenAIResponsesInput({ call_id: part.toolCallId, name: part.toolName, arguments: JSON.stringify(part.input), - id: (part.providerOptions?.openai?.itemId as string) ?? undefined, + id: (part.providerOptions?.copilot?.itemId as string) ?? undefined, }) break } @@ -275,7 +275,7 @@ export async function convertToOpenAIResponsesInput({ const output = part.output if (output.type === "execution-denied") { - const approvalId = (output.providerOptions?.openai as { approvalId?: string } | undefined)?.approvalId + const approvalId = (output.providerOptions?.copilot as { approvalId?: string } | undefined)?.approvalId if (approvalId) { continue diff --git a/packages/core/src/github-copilot/responses/openai-responses-language-model.ts b/packages/core/src/github-copilot/responses/openai-responses-language-model.ts index 250d1f6f3405..8df1dcedadb3 100644 --- a/packages/core/src/github-copilot/responses/openai-responses-language-model.ts +++ b/packages/core/src/github-copilot/responses/openai-responses-language-model.ts @@ -525,7 +525,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "reasoning" as const, text: summary.text, providerMetadata: { - openai: { + copilot: { itemId: part.id, reasoningEncryptedContent: part.encrypted_content ?? null, }, @@ -563,7 +563,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { toolName: "local_shell", input: JSON.stringify({ action: part.action } satisfies z.infer), providerMetadata: { - openai: { + copilot: { itemId: part.id, }, }, @@ -574,7 +574,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { case "message": { for (const contentPart of part.content) { - if (options.providerOptions?.openai?.logprobs && contentPart.logprobs) { + if (options.providerOptions?.copilot?.logprobs && contentPart.logprobs) { logprobs.push(contentPart.logprobs) } @@ -582,7 +582,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "text", text: contentPart.text, providerMetadata: { - openai: { + copilot: { itemId: part.id, }, }, @@ -622,7 +622,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { toolName: part.name, input: part.arguments, providerMetadata: { - openai: { + copilot: { itemId: part.id, }, }, @@ -724,15 +724,15 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { } const providerMetadata: SharedV3ProviderMetadata = { - openai: { responseId: response.id }, + copilot: { responseId: response.id }, } if (logprobs.length > 0) { - providerMetadata.openai.logprobs = logprobs + providerMetadata.copilot.logprobs = logprobs } if (typeof response.service_tier === "string") { - providerMetadata.openai.serviceTier = response.service_tier + providerMetadata.copilot.serviceTier = response.service_tier } return { @@ -954,7 +954,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "text-start", id: value.item.id, providerMetadata: { - openai: { + copilot: { itemId: value.item.id, }, }, @@ -971,7 +971,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "reasoning-start", id: `${value.item.id}:0`, providerMetadata: { - openai: { + copilot: { itemId: value.item.id, reasoningEncryptedContent: value.item.encrypted_content ?? null, }, @@ -994,7 +994,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { toolName: value.item.name, input: value.item.arguments, providerMetadata: { - openai: { + copilot: { itemId: value.item.id, }, }, @@ -1103,7 +1103,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { }, } satisfies z.infer), providerMetadata: { - openai: { itemId: value.item.id }, + copilot: { itemId: value.item.id }, }, }) } else if (value.item.type === "message") { @@ -1122,7 +1122,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "reasoning-end", id: `${activeReasoningPart.canonicalId}:${summaryIndex}`, providerMetadata: { - openai: { + copilot: { itemId: activeReasoningPart.canonicalId, reasoningEncryptedContent: value.item.encrypted_content ?? null, }, @@ -1209,7 +1209,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "text-start", id: currentTextId, providerMetadata: { - openai: { itemId: value.item_id }, + copilot: { itemId: value.item_id }, }, }) } @@ -1220,7 +1220,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { delta: value.delta, }) - if (options.providerOptions?.openai?.logprobs && value.logprobs) { + if (options.providerOptions?.copilot?.logprobs && value.logprobs) { logprobs.push(value.logprobs) } } else if (isResponseReasoningSummaryPartAddedChunk(value)) { @@ -1235,7 +1235,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "reasoning-start", id: `${activeItem.canonicalId}:${value.summary_index}`, providerMetadata: { - openai: { + copilot: { itemId: activeItem.canonicalId, reasoningEncryptedContent: activeItem.encryptedContent ?? null, }, @@ -1252,7 +1252,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { id: `${activeItem.canonicalId}:${value.summary_index}`, delta: value.delta, providerMetadata: { - openai: { + copilot: { itemId: activeItem.canonicalId, }, }, @@ -1306,17 +1306,17 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { } const providerMetadata: SharedV3ProviderMetadata = { - openai: { + copilot: { responseId, }, } if (logprobs.length > 0) { - providerMetadata.openai.logprobs = logprobs + providerMetadata.copilot.logprobs = logprobs } if (serviceTier !== undefined) { - providerMetadata.openai.serviceTier = serviceTier + providerMetadata.copilot.serviceTier = serviceTier } controller.enqueue({ diff --git a/packages/core/test/github-copilot/openai-responses-language-model.test.ts b/packages/core/test/github-copilot/openai-responses-language-model.test.ts new file mode 100644 index 000000000000..ab047d04ebe3 --- /dev/null +++ b/packages/core/test/github-copilot/openai-responses-language-model.test.ts @@ -0,0 +1,206 @@ +import { OpenAIResponsesLanguageModel } from "@opencode-ai/core/github-copilot/responses/openai-responses-language-model" +import { convertToOpenAIResponsesInput } from "@opencode-ai/core/github-copilot/responses/convert-to-openai-responses-input" +import { describe, test, expect, mock } from "bun:test" +import type { LanguageModelV3Prompt } from "@ai-sdk/provider" + +const TEST_PROMPT: LanguageModelV3Prompt = [{ role: "user", content: [{ type: "text", text: "Hello" }] }] + +function createMockFetch(body: unknown) { + return mock( + async () => new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" } }), + ) +} + +function createModel(fetchFn: ReturnType) { + return new OpenAIResponsesLanguageModel("test-model", { + provider: "copilot", + url: () => "https://api.test.com/responses", + headers: () => ({ Authorization: "Bearer test-token" }), + fetch: fetchFn as any, + }) +} + +// GitHub Copilot's Responses model echoes item metadata (itemId, reasoningEncryptedContent, +// responseId, ...) under the "copilot" providerOptions/providerMetadata namespace, matching the +// namespace request options already use. It used to echo this metadata under "openai" (a leftover +// from forking the OpenAI Responses model), which left it unreachable by anything reading the +// "copilot" namespace and let stale itemIds slip past stripping meant for that namespace. +describe("doGenerate", () => { + test("attaches item metadata under the copilot namespace, not openai", async () => { + const mockFetch = createMockFetch({ + id: "resp_1", + created_at: 0, + model: "gpt-5.5", + output: [ + { + type: "reasoning", + id: "rs_1", + encrypted_content: "enc_1", + summary: [{ type: "summary_text", text: "thinking..." }], + }, + { + type: "message", + role: "assistant", + id: "msg_1", + content: [{ type: "output_text", text: "Hello there", annotations: [] }], + }, + { + type: "function_call", + call_id: "call_1", + name: "bash", + arguments: "{}", + id: "fc_1", + }, + ], + usage: { input_tokens: 10, output_tokens: 5 }, + }) + const model = createModel(mockFetch) + + const { content, providerMetadata } = await model.doGenerate({ + prompt: TEST_PROMPT, + includeRawChunks: false, + } as any) + + const reasoning = content.find((part: any) => part.type === "reasoning") as any + expect(reasoning.providerMetadata?.copilot?.itemId).toBe("rs_1") + expect(reasoning.providerMetadata?.copilot?.reasoningEncryptedContent).toBe("enc_1") + expect(reasoning.providerMetadata?.openai).toBeUndefined() + + const text = content.find((part: any) => part.type === "text") as any + expect(text.providerMetadata?.copilot?.itemId).toBe("msg_1") + expect(text.providerMetadata?.openai).toBeUndefined() + + const toolCall = content.find((part: any) => part.type === "tool-call") as any + expect(toolCall.providerMetadata?.copilot?.itemId).toBe("fc_1") + expect(toolCall.providerMetadata?.openai).toBeUndefined() + + expect(providerMetadata?.copilot?.responseId).toBe("resp_1") + expect(providerMetadata?.openai).toBeUndefined() + }) +}) + +describe("convertToOpenAIResponsesInput", () => { + test("echoes a stale tool-call itemId from the copilot namespace as the function_call id", async () => { + const { input } = await convertToOpenAIResponsesInput({ + prompt: [ + { + role: "assistant", + content: [ + { + type: "tool-call", + toolCallId: "call_1", + toolName: "bash", + input: { command: "ls" }, + providerOptions: { copilot: { itemId: "fc_999" } }, + }, + ], + }, + ], + systemMessageMode: "system", + store: false, + }) + + expect(input).toEqual([ + { + type: "function_call", + call_id: "call_1", + name: "bash", + arguments: JSON.stringify({ command: "ls" }), + id: "fc_999", + }, + ]) + }) + + test("omits the function_call id once the stale copilot itemId has been stripped", async () => { + const { input } = await convertToOpenAIResponsesInput({ + prompt: [ + { + role: "assistant", + content: [ + { + type: "tool-call", + toolCallId: "call_1", + toolName: "bash", + input: { command: "ls" }, + providerOptions: {}, + }, + ], + }, + ], + systemMessageMode: "system", + store: false, + }) + + expect((input[0] as any).id).toBeUndefined() + }) + + test("preserves reasoning items keyed by the copilot namespace instead of dropping them", async () => { + const { input, warnings } = await convertToOpenAIResponsesInput({ + prompt: [ + { + role: "assistant", + content: [ + { + type: "reasoning", + text: "thinking...", + providerOptions: { copilot: { itemId: "rs_1", reasoningEncryptedContent: "enc_1" } }, + }, + ], + }, + ], + systemMessageMode: "system", + store: false, + }) + + expect(warnings).toEqual([]) + expect(input).toEqual([ + { + type: "reasoning", + id: "rs_1", + encrypted_content: "enc_1", + summary: [{ type: "summary_text", text: "thinking..." }], + }, + ]) + }) + + test("drops reasoning items with no copilot itemId and warns, as before", async () => { + const { input, warnings } = await convertToOpenAIResponsesInput({ + prompt: [ + { + role: "assistant", + content: [{ type: "reasoning", text: "thinking...", providerOptions: {} }], + }, + ], + systemMessageMode: "system", + store: false, + }) + + expect(input).toEqual([]) + expect(warnings).toHaveLength(1) + expect(warnings[0]).toMatchObject({ + message: expect.stringContaining("Non-OpenAI reasoning parts are not supported"), + }) + }) + + test("reads imageDetail from the copilot namespace on user file parts", async () => { + const { input } = await convertToOpenAIResponsesInput({ + prompt: [ + { + role: "user", + content: [ + { + type: "file", + mediaType: "image/png", + data: "aGVsbG8=", + providerOptions: { copilot: { imageDetail: "high" } }, + }, + ], + }, + ], + systemMessageMode: "system", + store: false, + }) + + expect((input[0] as any).content[0].detail).toBe("high") + }) +}) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 4bb81b753b52..466b05b62161 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -463,7 +463,9 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re if ( options.store !== true && key && - ["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(model.api.npm) + ["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle", "@ai-sdk/github-copilot"].includes( + model.api.npm, + ) ) { msgs = mapProviderOptions(msgs, (options) => { if (!options?.[key] || !("itemId" in options[key])) return options diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index dcbaf8171609..550903bf46e9 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -2268,6 +2268,82 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( expect(result[0].content[0].providerOptions?.openai?.reasoningEncryptedContent).toBe("encrypted") }) + test("strips GitHub Copilot itemId from the copilot namespace, preserving other copilot options", () => { + const copilotModel = { + ...openaiModel, + id: "github-copilot/gpt-5.5", + providerID: "github-copilot", + api: { + id: "gpt-5.5", + url: "https://api.githubcopilot.com", + npm: "@ai-sdk/github-copilot", + }, + } + const msgs = [ + { + role: "assistant", + content: [ + { + type: "reasoning", + text: "thinking...", + providerOptions: { + copilot: { itemId: "rs_123", reasoningEncryptedContent: "encrypted" }, + }, + }, + { + // The stale itemId on tool-call parts is what Copilot echoes back as the + // `function_call` item `id`, which is what the upstream connection rejects. + type: "tool-call", + toolCallId: "call_1", + toolName: "bash", + input: { command: "ls" }, + providerOptions: { + copilot: { itemId: "fc_456", reasoningEffort: "medium" }, + }, + }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[] + + expect(result[0].content[0].providerOptions?.copilot?.itemId).toBeUndefined() + expect(result[0].content[0].providerOptions?.copilot?.reasoningEncryptedContent).toBe("encrypted") + expect(result[0].content[1].providerOptions?.copilot?.itemId).toBeUndefined() + expect(result[0].content[1].providerOptions?.copilot?.reasoningEffort).toBe("medium") + }) + + test("leaves a stray openai namespace on a Copilot model untouched, since Copilot's Responses model only reads the copilot namespace", () => { + const copilotModel = { + ...openaiModel, + id: "github-copilot/gpt-5.5", + providerID: "github-copilot", + api: { + id: "gpt-5.5", + url: "https://api.githubcopilot.com", + npm: "@ai-sdk/github-copilot", + }, + } + const msgs = [ + { + role: "assistant", + content: [ + { + type: "text", + text: "Hello", + providerOptions: { + openai: { itemId: "msg_456" }, + }, + }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[] + + expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_456") + }) + test("preserves metadata for openai package when store is true", () => { const msgs = [ { From ae3fefb42a34bbb6b2076f8bc080d79533e3955d Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:52:10 +0000 Subject: [PATCH 006/140] fix(session-ui): recognize more inline file paths (#34688) Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: opencode-agent[bot] --- .../markdown-inline-code-kind.test.ts | 14 + .../components/markdown-inline-code-kind.ts | 1914 ++++++++++++++++- 2 files changed, 1921 insertions(+), 7 deletions(-) diff --git a/packages/session-ui/src/components/markdown-inline-code-kind.test.ts b/packages/session-ui/src/components/markdown-inline-code-kind.test.ts index 43e8ad4ed268..5b7c00855b61 100644 --- a/packages/session-ui/src/components/markdown-inline-code-kind.test.ts +++ b/packages/session-ui/src/components/markdown-inline-code-kind.test.ts @@ -13,10 +13,24 @@ describe("inlineCodeKind", () => { expect(inlineCodeKind(`@opencode-ai/app )`)).toBeUndefined() expect(inlineCodeKind(`sync.data.session`)).toBeUndefined() expect(inlineCodeKind(`window.api`)).toBeUndefined() + expect(inlineCodeKind(`1.2`)).toBeUndefined() }) test("detects file and directory paths", () => { expect(inlineCodeKind(`app.tsx`)).toBe("path") + expect(inlineCodeKind(`vite.config.mjs`)).toBe("path") + expect(inlineCodeKind(`eslint.config.cjs`)).toBe("path") + expect(inlineCodeKind(`app.d.ts`)).toBe("path") + expect(inlineCodeKind(`component.svelte`)).toBe("path") + expect(inlineCodeKind(`schema.graphql`)).toBe("path") + expect(inlineCodeKind(`Dockerfile`)).toBe("path") + expect(inlineCodeKind(`Dockerfile.dev`)).toBe("path") + expect(inlineCodeKind(`.gitignore`)).toBe("path") + expect(inlineCodeKind(`Cargo.lock`)).toBe("path") + expect(inlineCodeKind(`go.sum`)).toBe("path") + expect(inlineCodeKind(`bun.lockb`)).toBe("path") + expect(inlineCodeKind(`terraform.tfvars`)).toBe("path") + expect(inlineCodeKind(`pnpm-lock.yaml`)).toBe("path") expect(inlineCodeKind(`packages/desktop-electron`)).toBe("path") expect(inlineCodeKind(`~/.config/opencode`)).toBe("path") expect(inlineCodeKind(`@opencode-ai/app`)).toBe("path") diff --git a/packages/session-ui/src/components/markdown-inline-code-kind.ts b/packages/session-ui/src/components/markdown-inline-code-kind.ts index 3679fe4ff429..6d32c3b24690 100644 --- a/packages/session-ui/src/components/markdown-inline-code-kind.ts +++ b/packages/session-ui/src/components/markdown-inline-code-kind.ts @@ -1,3 +1,1893 @@ +// One-off copy from GitHub Linguist languages.yml (e9fe3c9f230cd9220afcd057f75702de4d7700c9), plus common lockfile suffixes. +// Normalized to lower-case; numeric manpage-style extensions are excluded so versions like `1.2` stay plain code. +const pathExtensions = new Set([ + "a51", + "abap", + "abnf", + "action", + "ada", + "adb", + "adml", + "admx", + "ado", + "adoc", + "adp", + "ads", + "afm", + "agc", + "agda", + "ahk", + "ahkl", + "aidl", + "aj", + "ak", + "al", + "alg", + "als", + "ampl", + "angelscript", + "anim", + "ant", + "antlers.html", + "antlers.php", + "antlers.xml", + "apacheconf", + "apex", + "apib", + "apl", + "app", + "app.src", + "applescript", + "arc", + "arpa", + "arr", + "as", + "asax", + "asc", + "asciidoc", + "ascx", + "asd", + "asddls", + "ash", + "ashx", + "asl", + "asm", + "asmx", + "asn", + "asn1", + "asp", + "aspx", + "asset", + "astro", + "asy", + "au3", + "aug", + "auk", + "aux", + "avdl", + "avsc", + "aw", + "awk", + "axaml", + "axd", + "axi", + "axi.erb", + "axml", + "axs", + "axs.erb", + "b", + "bal", + "baml", + "bas", + "bash", + "bat", + "bats", + "bb", + "bbappend", + "bbclass", + "bbx", + "bdf", + "bdy", + "be", + "befunge", + "bf", + "bi", + "bib", + "bibtex", + "bicep", + "bicepparam", + "bison", + "blade", + "blade.php", + "blp", + "bmx", + "bones", + "boo", + "boot", + "bpl", + "bqn", + "brd", + "bro", + "brs", + "bru", + "bs", + "bsl", + "bst", + "bsv", + "builder", + "builds", + "bzl", + "c", + "c++", + "c++-objdump", + "c++objdump", + "c-objdump", + "c3", + "cabal", + "caddyfile", + "cairo", + "cake", + "capnp", + "carbon", + "cats", + "cbl", + "cbx", + "cc", + "ccp", + "ccproj", + "ccxml", + "cdc", + "cdf", + "cds", + "ceylon", + "cfc", + "cfg", + "cfm", + "cfml", + "cgi", + "cginc", + "ch", + "chem", + "chpl", + "chs", + "cil", + "circom", + "cirru", + "cj", + "cjs", + "cjsx", + "ck", + "cl", + "cl2", + "clar", + "click", + "clixml", + "clj", + "cljc", + "cljs", + "cljs.hl", + "cljscm", + "cljx", + "clp", + "cls", + "clue", + "clw", + "cmake", + "cmake.in", + "cmd", + "cmp", + "cnc", + "cnf", + "cob", + "cobol", + "cocci", + "code-snippets", + "code-workspace", + "coffee", + "coffee.md", + "com", + "command", + "conll", + "conllu", + "container", + "containerfile", + "cook", + "coq", + "cp", + "cpp", + "cpp-objdump", + "cppm", + "cppobjdump", + "cproject", + "cps", + "cpy", + "cql", + "cr", + "crc32", + "creole", + "cs", + "cs.pp", + "csc", + "cscfg", + "csd", + "csdef", + "csh", + "cshtml", + "csl", + "cson", + "csproj", + "css", + "csv", + "csx", + "ct", + "ctl", + "ctp", + "cts", + "cu", + "cue", + "cuh", + "curry", + "cw", + "cwl", + "cxx", + "cxx-objdump", + "cy", + "cylc", + "cyp", + "cypher", + "d", + "d-objdump", + "d2", + "dae", + "darcspatch", + "dart", + "das", + "dats", + "db2", + "dcl", + "ddl", + "decls", + "depproj", + "desktop", + "desktop.in", + "dfm", + "dfy", + "dhall", + "di", + "diff", + "dircolors", + "dita", + "ditamap", + "ditaval", + "djs", + "dll.config", + "dlm", + "dm", + "do", + "dockerfile", + "dof", + "doh", + "dot", + "dotsettings", + "dpatch", + "dpr", + "druby", + "dsc", + "dsl", + "dsp", + "dsr", + "dtx", + "duby", + "dwl", + "dyalog", + "dyl", + "dylan", + "dzn", + "e", + "eam.fs", + "eb", + "ebnf", + "ebuild", + "ec", + "ecl", + "eclass", + "eclxml", + "ecr", + "ect", + "edc", + "edge", + "edgeql", + "editorconfig", + "edn", + "eh", + "ejs", + "ejs.t", + "el", + "eliom", + "eliomi", + "elm", + "elv", + "em", + "emacs", + "emacs.desktop", + "emberscript", + "eml", + "env", + "epj", + "eps", + "epsi", + "eq", + "erb", + "erb.deface", + "erl", + "es", + "es6", + "escript", + "esdl", + "ex", + "exs", + "eye", + "f", + "f03", + "f08", + "f77", + "f90", + "f95", + "factor", + "fan", + "fancypack", + "fbs", + "fcgi", + "fea", + "feature", + "filters", + "fir", + "fish", + "flex", + "flf", + "flix", + "flux", + "fnc", + "fnl", + "for", + "forth", + "fp", + "fpp", + "fr", + "frag", + "frg", + "frm", + "frt", + "fs", + "fsh", + "fshader", + "fsi", + "fsproj", + "fst", + "fsti", + "fsx", + "fth", + "ftl", + "ftlh", + "fun", + "fut", + "fx", + "fxh", + "fxml", + "fy", + "g", + "g4", + "gaml", + "gap", + "gawk", + "gbl", + "gbo", + "gbp", + "gbr", + "gbs", + "gco", + "gcode", + "gd", + "gdb", + "gdbinit", + "gdnlib", + "gdns", + "gdshader", + "gdshaderinc", + "ged", + "gemspec", + "geo", + "geojson", + "geom", + "gf", + "gi", + "gitconfig", + "gitignore", + "gjs", + "gko", + "glade", + "gleam", + "glf", + "glsl", + "glslf", + "glslv", + "gltf", + "glyphs", + "gmi", + "gml", + "gms", + "gmx", + "gn", + "gni", + "gnu", + "gnuplot", + "go", + "god", + "gohtml", + "golo", + "gotmpl", + "gp", + "gpb", + "gpt", + "gpx", + "gql", + "grace", + "gradle", + "gradle.kts", + "graphql", + "graphqls", + "groovy", + "grt", + "grxml", + "gs", + "gsc", + "gsh", + "gshader", + "gsp", + "gst", + "gsx", + "gtkrc", + "gtl", + "gto", + "gtp", + "gtpl", + "gts", + "gv", + "gvy", + "gyp", + "gypi", + "h", + "h++", + "h.in", + "ha", + "hack", + "haml", + "haml.deface", + "handlebars", + "har", + "hats", + "hb", + "hbs", + "hc", + "hcl", + "heex", + "hh", + "hhi", + "hic", + "hip", + "hlean", + "hlsl", + "hlsli", + "hocon", + "hoon", + "hpp", + "hqf", + "hql", + "hrl", + "hs", + "hs-boot", + "hsc", + "hta", + "htm", + "html", + "html.eex", + "html.hl", + "html.tmpl", + "http", + "hurl", + "hx", + "hxml", + "hxsl", + "hxx", + "hy", + "hzp", + "i", + "i3", + "i7x", + "ical", + "ice", + "iced", + "icl", + "icls", + "ics", + "idc", + "idr", + "ig", + "ihlp", + "ijm", + "ijs", + "ik", + "il", + "ily", + "imba", + "iml", + "inc", + "ini", + "ink", + "inl", + "ino", + "ins", + "intr", + "io", + "iol", + "ipf", + "ipp", + "ipynb", + "irclog", + "isl", + "ispc", + "iss", + "iuml", + "ivy", + "ixx", + "j", + "j2", + "jac", + "jade", + "jai", + "jake", + "janet", + "jav", + "java", + "javascript", + "jbuilder", + "jcl", + "jelly", + "jflex", + "jinja", + "jinja2", + "jison", + "jisonlex", + "jl", + "jq", + "js", + "js.erb", + "jsb", + "jscad", + "jsfl", + "jsh", + "jslib", + "jsm", + "json", + "json-tmlanguage", + "json.example", + "json5", + "jsonc", + "jsonl", + "jsonld", + "jsonnet", + "jsp", + "jspre", + "jsproj", + "jss", + "jst", + "jsx", + "jte", + "just", + "k", + "kak", + "kdl", + "kicad_mod", + "kicad_pcb", + "kicad_sch", + "kicad_sym", + "kicad_wks", + "kid", + "kit", + "kk", + "kml", + "kojo", + "kql", + "krl", + "ks", + "ksh", + "ksy", + "kt", + "ktm", + "kts", + "kv", + "l", + "lagda", + "langium", + "lark", + "las", + "lasso", + "lasso8", + "lasso9", + "latte", + "launch", + "lbx", + "ld", + "lds", + "lean", + "leex", + "lektorproject", + "leo", + "less", + "lex", + "lfe", + "lgt", + "lhs", + "libsonnet", + "lid", + "lidr", + "ligo", + "linq", + "liq", + "liquid", + "lisp", + "litcoffee", + "livecodescript", + "livemd", + "lkml", + "ll", + "lmi", + "lock", + "lockb", + "logtalk", + "lol", + "lookml", + "lp", + "lpr", + "ls", + "lsl", + "lslp", + "lsp", + "ltx", + "lua", + "luau", + "lvclass", + "lvlib", + "lvproj", + "ly", + "m", + "m2", + "m3", + "m3u", + "m3u8", + "m4", + "ma", + "mak", + "make", + "makefile", + "mako", + "man", + "mao", + "markdown", + "marko", + "mask", + "mat", + "mata", + "matah", + "mathematica", + "matlab", + "mawk", + "maxhelp", + "maxpat", + "maxproj", + "mbox", + "mbt", + "mc", + "mcfunction", + "mch", + "mcmeta", + "mcr", + "md", + "md2", + "md4", + "md5", + "mdoc", + "mdown", + "mdpolicy", + "mdwn", + "mdx", + "me", + "mediawiki", + "mermaid", + "meta", + "metal", + "metta", + "mg", + "minid", + "mint", + "mir", + "mirah", + "mjml", + "mjs", + "mk", + "mkd", + "mkdn", + "mkdown", + "mkfile", + "mkii", + "mkiv", + "mkvi", + "ml", + "ml4", + "mli", + "mligo", + "mlir", + "mll", + "mly", + "mm", + "mmd", + "mmk", + "mms", + "mo", + "mod", + "mojo", + "monkey", + "monkey2", + "moo", + "moon", + "mount", + "move", + "mpl", + "mps", + "mq4", + "mq5", + "mqh", + "mrc", + "ms", + "msd", + "msg", + "mspec", + "mss", + "mt", + "mtl", + "mtml", + "mts", + "mu", + "mud", + "muf", + "mumps", + "muse", + "mustache", + "mxml", + "mxt", + "mysql", + "myt", + "mzn", + "n", + "nanorc", + "nas", + "nasl", + "nasm", + "natvis", + "nawk", + "nb", + "nbp", + "nc", + "ncl", + "ndproj", + "ne", + "nearley", + "ned", + "neon", + "network", + "nf", + "nginx", + "nginxconf", + "ni", + "nim", + "nim.cfg", + "nimble", + "nimrod", + "nims", + "ninja", + "nit", + "nix", + "njk", + "njs", + "nl", + "nlogo", + "no", + "nomad", + "nproj", + "nqp", + "nr", + "nse", + "nsh", + "nsi", + "nss", + "nu", + "numpy", + "numpyw", + "numsc", + "nuspec", + "nut", + "ny", + "ob2", + "obj", + "objdump", + "odd", + "odin", + "ol", + "omgrofl", + "ooc", + "opa", + "opal", + "opencl", + "opy", + "orc", + "org", + "os", + "osm", + "outjob", + "overpassql", + "owl", + "ox", + "oxh", + "oxo", + "oxygene", + "oz", + "p", + "p4", + "p6", + "p6l", + "p6m", + "p8", + "pac", + "pact", + "pan", + "parrot", + "pas", + "pascal", + "pasm", + "pat", + "patch", + "pb", + "pbi", + "pbt", + "pbtxt", + "pc", + "pc.in", + "pcbdoc", + "pck", + "pcss", + "pd", + "pd_lua", + "pddl", + "pde", + "peggy", + "pegjs", + "pep", + "per", + "perl", + "pfa", + "pgsql", + "ph", + "php", + "php3", + "php4", + "php5", + "phps", + "phpt", + "phtml", + "pic", + "pig", + "pike", + "pir", + "pkb", + "pkgproj", + "pkl", + "pks", + "pl", + "pl6", + "plantuml", + "plb", + "plist", + "plot", + "pls", + "plsql", + "plt", + "pluginspec", + "plx", + "pm", + "pm6", + "pml", + "pmod", + "po", + "pod", + "pod6", + "podsl", + "podspec", + "pogo", + "polar", + "pony", + "por", + "postcss", + "pot", + "pov", + "pp", + "pprx", + "pq", + "praat", + "prawn", + "prc", + "prefab", + "prefs", + "prg", + "pri", + "prisma", + "prjpcb", + "pro", + "proj", + "prolog", + "properties", + "props", + "proto", + "prw", + "ps", + "ps1", + "ps1xml", + "psc", + "psc1", + "psd1", + "psgi", + "psm1", + "pt", + "pub", + "pubxml", + "pug", + "puml", + "purs", + "pwn", + "pxd", + "pxi", + "py", + "py3", + "pyde", + "pyi", + "pyp", + "pyt", + "pytb", + "pyw", + "pyx", + "q", + "qasm", + "qbs", + "qc", + "qhelp", + "ql", + "qll", + "qmd", + "qml", + "qnt", + "qs", + "r", + "r2", + "r3", + "rabl", + "rake", + "raku", + "rakumod", + "raml", + "rascript", + "raw", + "razor", + "rb", + "rbbas", + "rbfrm", + "rbi", + "rbmnu", + "rbres", + "rbs", + "rbtbar", + "rbuild", + "rbuistate", + "rbw", + "rbx", + "rbxs", + "rchit", + "rd", + "rdf", + "rdoc", + "re", + "reb", + "rebol", + "red", + "reds", + "reek", + "reg", + "regex", + "regexp", + "rego", + "rei", + "religo", + "res", + "resi", + "resource", + "rest", + "rest.txt", + "resx", + "rex", + "rexx", + "rg", + "rhtml", + "ring", + "riot", + "rkt", + "rktd", + "rktl", + "rl", + "rmd", + "rmiss", + "rnh", + "rno", + "rnw", + "robot", + "roc", + "rockspec", + "roff", + "ron", + "ronn", + "rpgle", + "rpy", + "rq", + "rs", + "rs.in", + "rsc", + "rsh", + "rss", + "rst", + "rst.txt", + "rsx", + "rtf", + "ru", + "ruby", + "rviz", + "s", + "sage", + "sagews", + "sail", + "sarif", + "sas", + "sass", + "sats", + "sbatch", + "sbt", + "sc", + "scad", + "scala", + "scaml", + "scd", + "sce", + "scenic", + "sch", + "schdoc", + "sci", + "scm", + "sco", + "scpt", + "scrbl", + "scss", + "scxml", + "sdc", + "sed", + "self", + "service", + "sexp", + "sfd", + "sfproj", + "sfv", + "sh", + "sh-session", + "sh.in", + "sha1", + "sha2", + "sha224", + "sha256", + "sha256sum", + "sha3", + "sha384", + "sha512", + "shader", + "shen", + "shproj", + "sieve", + "sig", + "sj", + "sjs", + "sl", + "slang", + "sld", + "slim", + "slint", + "sln", + "slnlaunch", + "slnx", + "sls", + "slurm", + "sma", + "smali", + "smithy", + "smk", + "sml", + "smt", + "smt2", + "snakefile", + "snap", + "snip", + "snippet", + "snippets", + "socket", + "sol", + "soy", + "sp", + "sparql", + "spc", + "spec", + "spin", + "sps", + "sqf", + "sql", + "sqlrpgle", + "sra", + "srdf", + "srt", + "sru", + "srv", + "srw", + "ss", + "ssjs", + "sss", + "st", + "stan", + "star", + "sthlp", + "stl", + "ston", + "story", + "storyboard", + "sttheme", + "sty", + "styl", + "sublime-build", + "sublime-color-scheme", + "sublime-commands", + "sublime-completions", + "sublime-keymap", + "sublime-macro", + "sublime-menu", + "sublime-mousemap", + "sublime-project", + "sublime-settings", + "sublime-snippet", + "sublime-syntax", + "sublime-theme", + "sublime-workspace", + "sublime_metrics", + "sublime_session", + "sum", + "surql", + "sv", + "svelte", + "svg", + "svh", + "svx", + "sw", + "swg", + "swift", + "swig", + "syntax", + "t", + "tab", + "tac", + "tact", + "tag", + "talon", + "target", + "targets", + "tcc", + "tcl", + "tcl.in", + "tcsh", + "te", + "tea", + "templ", + "tesc", + "tese", + "tex", + "texi", + "texinfo", + "textgrid", + "textile", + "textproto", + "tf", + "tfstate", + "tfstate.backup", + "tftpl", + "tfvars", + "thor", + "thrift", + "thy", + "timer", + "tl", + "tla", + "tlv", + "tm", + "tmac", + "tmcommand", + "tmdl", + "tml", + "tmlanguage", + "tmpl", + "tmpreferences", + "tmsnippet", + "tmtheme", + "tmux", + "toc", + "tofu", + "toit", + "toml", + "toml.example", + "tool", + "topojson", + "tpb", + "tpl", + "tpp", + "tps", + "tres", + "trg", + "trigger", + "ts", + "tscn", + "tsconfig.json", + "tsp", + "tst", + "tsv", + "tsx", + "ttl", + "tu", + "twig", + "txi", + "txl", + "txt", + "txtpb", + "txx", + "typ", + "uc", + "udf", + "udo", + "ui", + "unity", + "uno", + "upc", + "uplc", + "ur", + "urdf", + "url", + "urs", + "ux", + "v", + "vala", + "vapi", + "vark", + "vb", + "vba", + "vbhtml", + "vbproj", + "vbs", + "vcf", + "vcl", + "vcxproj", + "vdf", + "veo", + "vert", + "vh", + "vhd", + "vhdl", + "vhf", + "vhi", + "vho", + "vhost", + "vhs", + "vht", + "vhw", + "vim", + "vimrc", + "viw", + "vmb", + "vmf", + "volt", + "vrx", + "vs", + "vsh", + "vshader", + "vsixmanifest", + "vssettings", + "vstemplate", + "vtl", + "vto", + "vtt", + "vue", + "vw", + "vxml", + "vy", + "w", + "wast", + "wat", + "watchr", + "wdl", + "webapp", + "webidl", + "webmanifest", + "weechatlog", + "wgsl", + "whiley", + "wiki", + "wikitext", + "wisp", + "wit", + "wixproj", + "wl", + "wlk", + "wls", + "wlt", + "wlua", + "workbook", + "workflow", + "wren", + "ws", + "wsdl", + "wsf", + "wsgi", + "wxi", + "wxl", + "wxs", + "x", + "x10", + "x3d", + "x68", + "xacro", + "xaml", + "xbm", + "xc", + "xdc", + "xht", + "xhtml", + "xi", + "xib", + "xlf", + "xliff", + "xm", + "xmi", + "xml", + "xml.dist", + "xmp", + "xojo_code", + "xojo_menu", + "xojo_report", + "xojo_script", + "xojo_toolbar", + "xojo_window", + "xpl", + "xpm", + "xproc", + "xproj", + "xpy", + "xq", + "xql", + "xqm", + "xquery", + "xqy", + "xrl", + "xs", + "xsd", + "xsh", + "xsjs", + "xsjslib", + "xsl", + "xslt", + "xsp-config", + "xsp.metadata", + "xspec", + "xtend", + "xul", + "xzap", + "y", + "yacc", + "yaml", + "yaml-tmlanguage", + "yaml.sed", + "yang", + "yap", + "yar", + "yara", + "yasnippet", + "yml", + "yml.mysql", + "yrl", + "yul", + "yy", + "yyp", + "z3", + "zap", + "zcml", + "zed", + "zeek", + "zep", + "zig", + "zig.zon", + "zil", + "zimpl", + "zmodel", + "zmpl", + "zone", + "zpl", + "zs", + "zsh", + "zsh-theme", +]) + +const pathFileNames = new Set([ + ".abbrev_defs", + ".ackrc", + ".all-contributorsrc", + ".arcconfig", + ".atomignore", + ".auto-changelog", + ".babelignore", + ".babelrc", + ".bash_aliases", + ".bash_functions", + ".bash_history", + ".bash_logout", + ".bash_profile", + ".bashrc", + ".browserslistrc", + ".buckconfig", + ".bzrignore", + ".c8rc", + ".ckignore", + ".clang-format", + ".clang-tidy", + ".clangd", + ".classpath", + ".coffeelintignore", + ".coveragerc", + ".cshrc", + ".curlrc", + ".cvsignore", + ".dir_colors", + ".dockerignore", + ".easignore", + ".eleventyignore", + ".env.ci", + ".env.dev", + ".env.development", + ".env.development.local", + ".env.example", + ".env.local", + ".env.prod", + ".env.production", + ".env.sample", + ".env.staging", + ".env.template", + ".env.test", + ".env.testing", + ".envrc", + ".eslint-ignore", + ".eslintignore", + ".exrc", + ".factor-boot-rc", + ".factor-rc", + ".flake8", + ".flaskenv", + ".gclient", + ".gemrc", + ".git-blame-ignore-revs", + ".gitattributes", + ".gitmodules", + ".gnus", + ".gvimrc", + ".htaccess", + ".htmlhintrc", + ".ignore", + ".imgbotconfig", + ".inputrc", + ".irbrc", + ".jscsrc", + ".jshintrc", + ".jslintrc", + ".justfile", + ".kshrc", + ".latexmkrc", + ".login", + ".luacheckrc", + ".markdownlintignore", + ".nodemonignore", + ".npmignore", + ".npmrc", + ".nvimrc", + ".nycrc", + ".php_cs", + ".php_cs.dist", + ".prettierignore", + ".profile", + ".project", + ".pryrc", + ".pylintrc", + ".rgignore", + ".rprofile", + ".rspec", + ".scalafix.conf", + ".scalafmt.conf", + ".shellcheckrc", + ".simplecov", + ".spacemacs", + ".stylelintignore", + ".swcrc", + ".tern-config", + ".tern-project", + ".tgitconfig", + ".tm_properties", + ".tmux.conf", + ".vercelignore", + ".viper", + ".vscodeignore", + ".watchmanconfig", + ".wgetrc", + ".xcompose", + ".xinitrc", + ".xsession", + ".yardopts", + ".zlogin", + ".zlogout", + ".zprofile", + ".zshenv", + ".zshrc", + "9fs", + "_curlrc", + "_dir_colors", + "_dircolors", + "_emacs", + "_redirects", + "_vimrc", + "abbrev_defs", + "ack", + "ackrc", + "android.bp", + "apache2.conf", + "apkbuild", + "app.config", + "appraisals", + "bash_aliases", + "bash_logout", + "bash_profile", + "bashrc", + "berksfile", + "brewfile", + "browserslist", + "bsdmakefile", + "buck", + "build", + "build.bazel", + "buildfile", + "cabal.config", + "cabal.project", + "caddyfile", + "cakefile", + "capfile", + "cargo.toml.orig", + "cask", + "citation", + "citation.cff", + "citations", + "cksums", + "codeowners", + "commit_editmsg", + "config.worktree", + "configure.ac", + "containerfile", + "contents.lr", + "copying", + "cpanfile", + "crontab", + "cshrc", + "dangerfile", + "deliverfile", + "deps", + "dir_colors", + "dockerfile", + "dune-project", + "earthfile", + "eask", + "emakefile", + "encodings.dir", + "eqnrc", + "expr-dist", + "fakefile", + "fastfile", + "file_contexts", + "firestore.rules", + "fontlog", + "fonts.alias", + "fonts.dir", + "fonts.scale", + "fp-lib-table", + "gemfile", + "genfs_contexts", + "gitignore-global", + "gitignore_global", + "gnumakefile", + "go.work", + "gradlew", + "gtkrc", + "gtkrc-2.0", + "guardfile", + "gvimrc", + "hosts", + "httpd.conf", + "initial_sids", + "inputrc", + "install", + "jakefile", + "jarfile", + "jenkinsfile", + "justfile", + "kakrc", + "kbuild", + "kshrc", + "latexmkrc", + "ld.script", + "license", + "login", + "m3makefile", + "m3overrides", + "makefile", + "makefile.am", + "makefile.in", + "man", + "manifest.mf", + "mavenfile", + "mcmod.info", + "md5sums", + "meson.build", + "mkfile", + "mmn", + "mmt", + "mocha.opts", + "module.bazel", + "modulefile", + "mvnw", + "nanorc", + "news", + "nextflow.config", + "nginx.conf", + "notebook", + "nuget.config", + "nukefile", + "nvimrc", + "owh", + "package.resolved", + "packages.config", + "phakefile", + "pipfile", + "pkgbuild", + "podfile", + "port_contexts", + "procfile", + "profile", + "project.ede", + "project.godot", + "puppetfile", + "pylintrc", + "rakefile", + "readme.1st", + "rebar.config", + "rexfile", + "riemann.config", + "root", + "sconscript", + "sconstruct", + "security_classes", + "settings.stylecop", + "sha1sums", + "sha256sums", + "sha512sums", + "singularity", + "slakefile", + "snakefile", + "snapfile", + "ssh-config", + "ssh_config", + "sshconfig", + "sshd-config", + "sshd_config", + "starfield", + "steepfile", + "suite.rc", + "thorfile", + "tiltfile", + "tmux.conf", + "torrc", + "troffrc", + "troffrc-end", + "vagrantfile", + "vimrc", + "vlcrc", + "web.config", + "web.debug.config", + "web.release.config", + "workspace", + "workspace.bazel", + "workspace.bzlmod", + "wscript", + "xcompose", + "xinitrc", + "xsession", + "zlogin", + "zlogout", + "zprofile", + "zshenv", + "zshrc", +]) + +const pathFileNamePrefixes = new Set([ + "9fs", + "_curlrc", + "_dir_colors", + "_dircolors", + "_emacs", + "_redirects", + "_vimrc", + "abbrev_defs", + "ack", + "ackrc", + "apkbuild", + "appraisals", + "bash_aliases", + "bash_logout", + "bash_profile", + "bashrc", + "berksfile", + "brewfile", + "browserslist", + "bsdmakefile", + "buck", + "build", + "buildfile", + "caddyfile", + "cakefile", + "capfile", + "cask", + "citation", + "citations", + "cksums", + "codeowners", + "commit_editmsg", + "containerfile", + "copying", + "cpanfile", + "crontab", + "cshrc", + "dangerfile", + "deliverfile", + "deps", + "dir_colors", + "dockerfile", + "dune-project", + "earthfile", + "eask", + "emakefile", + "eqnrc", + "expr-dist", + "fakefile", + "fastfile", + "file_contexts", + "fontlog", + "fp-lib-table", + "gemfile", + "genfs_contexts", + "gitignore-global", + "gitignore_global", + "gnumakefile", + "gradlew", + "gtkrc", + "guardfile", + "gvimrc", + "hosts", + "initial_sids", + "inputrc", + "install", + "jakefile", + "jarfile", + "jenkinsfile", + "justfile", + "kakrc", + "kbuild", + "kshrc", + "latexmkrc", + "license", + "login", + "m3makefile", + "m3overrides", + "makefile", + "man", + "mavenfile", + "md5sums", + "mkfile", + "mmn", + "mmt", + "modulefile", + "mvnw", + "nanorc", + "news", + "notebook", + "nukefile", + "nvimrc", + "owh", + "phakefile", + "pipfile", + "pkgbuild", + "podfile", + "port_contexts", + "procfile", + "profile", + "puppetfile", + "pylintrc", + "rakefile", + "rexfile", + "root", + "sconscript", + "sconstruct", + "security_classes", + "sha1sums", + "sha256sums", + "sha512sums", + "singularity", + "slakefile", + "snakefile", + "snapfile", + "ssh-config", + "ssh_config", + "sshconfig", + "sshd-config", + "sshd_config", + "starfield", + "steepfile", + "thorfile", + "tiltfile", + "torrc", + "troffrc", + "troffrc-end", + "vagrantfile", + "vimrc", + "vlcrc", + "workspace", + "wscript", + "xcompose", + "xinitrc", + "xsession", + "zlogin", + "zlogout", + "zprofile", + "zshenv", + "zshrc", +]) + export function inlineCodeKind(text: string): "path" | "url" | undefined { if (/^https?:\/\//i.test(text)) return "url" if (/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) return @@ -5,12 +1895,22 @@ export function inlineCodeKind(text: string): "path" | "url" | undefined { if (/^\/[a-z][a-z0-9-]*$/i.test(text)) return if (/\s/.test(text)) return if (/[()\[\]{}*+=<>|&^"';]/.test(text)) return - if ( - /[/\\]/.test(text) || - /^\.\.?[/\\]/.test(text) || - /\.(tsx?|jsx?|json|py|go|rs|rb|php|css|html|md|sh|ya?ml|toml|sql|c|cpp|h|java|kt|swift|scala|xml|png|jpe?g|gif|svg|ico)$/i.test( - text, - ) - ) + if (/[/\\]/.test(text) || /^\.\.?[/\\]/.test(text) || hasPathExtension(text) || hasPathFileName(text)) return "path" } + +function hasPathExtension(text: string) { + const value = text.toLowerCase() + if (value.endsWith(".d.ts")) return true + const index = value.lastIndexOf(".") + if (index === -1) return false + return pathExtensions.has(value.slice(index + 1)) +} + +function hasPathFileName(text: string) { + const value = text.toLowerCase() + if (pathFileNames.has(value)) return true + const index = value.indexOf(".") + if (index === -1) return false + return pathFileNamePrefixes.has(value.slice(0, index)) +} From 439ec9b76fefd0fd8ca3d55e1c898402d9841ae3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 1 Jul 2026 00:53:45 +0000 Subject: [PATCH 007/140] chore: generate --- .../session-ui/src/components/markdown-inline-code-kind.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/session-ui/src/components/markdown-inline-code-kind.ts b/packages/session-ui/src/components/markdown-inline-code-kind.ts index 6d32c3b24690..ee6cc8dccca4 100644 --- a/packages/session-ui/src/components/markdown-inline-code-kind.ts +++ b/packages/session-ui/src/components/markdown-inline-code-kind.ts @@ -1895,8 +1895,7 @@ export function inlineCodeKind(text: string): "path" | "url" | undefined { if (/^\/[a-z][a-z0-9-]*$/i.test(text)) return if (/\s/.test(text)) return if (/[()\[\]{}*+=<>|&^"';]/.test(text)) return - if (/[/\\]/.test(text) || /^\.\.?[/\\]/.test(text) || hasPathExtension(text) || hasPathFileName(text)) - return "path" + if (/[/\\]/.test(text) || /^\.\.?[/\\]/.test(text) || hasPathExtension(text) || hasPathFileName(text)) return "path" } function hasPathExtension(text: string) { From 45a437cbb2bc1bbfa20d6ce37a9cdf3f9d539085 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:28:34 +1000 Subject: [PATCH 008/140] fix(app): clamp virtualizer range to scroll bounds (#34588) Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> Co-authored-by: opencode-agent[bot] --- patches/@tanstack%2Fvirtual-core@3.17.0.patch | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/patches/@tanstack%2Fvirtual-core@3.17.0.patch b/patches/@tanstack%2Fvirtual-core@3.17.0.patch index 388f8855a532..58616e31e6f5 100644 --- a/patches/@tanstack%2Fvirtual-core@3.17.0.patch +++ b/patches/@tanstack%2Fvirtual-core@3.17.0.patch @@ -1,5 +1,5 @@ diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs -index df75d0cf0347b62906e04e454d4f4ef062ed5c48..58913f2d30e0beee9d09dffa5ebcaab4601a2c22 100644 +index df75d0cf0347b62906e04e454d4f4ef062ed5c48..e5110715b049c6c8b992bd1375a7432df7c0182e 100644 --- a/dist/cjs/index.cjs +++ b/dist/cjs/index.cjs @@ -526,6 +526,7 @@ class Virtualizer { @@ -10,6 +10,20 @@ index df75d0cf0347b62906e04e454d4f4ef062ed5c48..58913f2d30e0beee9d09dffa5ebcaab4 this.getFurthestMeasurement = (measurements, index) => { const furthestMeasurementsFound = /* @__PURE__ */ new Map(); const furthestMeasurements = /* @__PURE__ */ new Map(); +@@ -715,10 +716,12 @@ class Virtualizer { + this.options.lanes + ], + (measurements, outerSize, scrollOffset, lanes) => { ++ const maxScrollOffset = Math.max(this.getTotalSize() - outerSize, 0); ++ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset); + return this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({ + measurements, + outerSize, +- scrollOffset, ++ scrollOffset: effectiveScrollOffset, + lanes, + // Pass the typed array so binary search + forward-walk can + // read start/end directly from Float64Array, skipping the diff --git a/dist/cjs/index.d.cts b/dist/cjs/index.d.cts index c61ee17752565253f795c7fc7d57e86237ecbb52..705bb7e3a121b040fb1a3e7890179eaa3e9b219e 100644 --- a/dist/cjs/index.d.cts @@ -35,7 +49,7 @@ index b03abab604eb6578f6f56ff92c489259cfaf8f19..0495f372ea000dffc416c4f56809946f private _iosDeferredAdjustment; private _iosTouching; diff --git a/dist/esm/index.js b/dist/esm/index.js -index e384cf7541978a2782b9dca68146e869b16ac3f2..53b15b7a36247ea4aa5b70b00d08b6d7dec3ea79 100644 +index e384cf7541978a2782b9dca68146e869b16ac3f2..77af22006325377bf9ca0052a3554f881f9e75fe 100644 --- a/dist/esm/index.js +++ b/dist/esm/index.js @@ -524,6 +524,7 @@ class Virtualizer { @@ -46,13 +60,46 @@ index e384cf7541978a2782b9dca68146e869b16ac3f2..53b15b7a36247ea4aa5b70b00d08b6d7 this.getFurthestMeasurement = (measurements, index) => { const furthestMeasurementsFound = /* @__PURE__ */ new Map(); const furthestMeasurements = /* @__PURE__ */ new Map(); +@@ -713,10 +714,12 @@ class Virtualizer { + this.options.lanes + ], + (measurements, outerSize, scrollOffset, lanes) => { ++ const maxScrollOffset = Math.max(this.getTotalSize() - outerSize, 0); ++ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset); + return this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({ + measurements, + outerSize, +- scrollOffset, ++ scrollOffset: effectiveScrollOffset, + lanes, + // Pass the typed array so binary search + forward-walk can + // read start/end directly from Float64Array, skipping the diff --git a/src/index.ts b/src/index.ts -index d35b3e0695a9c85b261bc1a4fbe23c0a60d5b204..d516a9d00f1233173f9a3e2144666c8159552f91 100644 +index d35b3e0695a9c85b261bc1a4fbe23c0a60d5b204..c504e630a60a3a7791345622bb02754b1308fd86 100644 --- a/src/index.ts +++ b/src/index.ts -@@ -1050,3 +1050,5 @@ export class Virtualizer< +@@ -1047,6 +1047,8 @@ export class Virtualizer< + return this.scrollOffset + } + + getLogicalScrollOffset = () => this.getScrollOffset() + this.scrollAdjustments + private getFurthestMeasurement = ( measurements: Array, index: number, +@@ -1334,12 +1336,14 @@ export class Virtualizer< + this.options.lanes, + ], + (measurements, outerSize, scrollOffset, lanes) => { ++ const maxScrollOffset = Math.max(this.getTotalSize() - outerSize, 0) ++ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset) + return (this.range = + measurements.length > 0 && outerSize > 0 + ? calculateRange({ + measurements, + outerSize, +- scrollOffset, ++ scrollOffset: effectiveScrollOffset, + lanes, + // Pass the typed array so binary search + forward-walk can + // read start/end directly from Float64Array, skipping the From 2b611a5b1465e64dd72a806cac36c4bdc10afcea Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 1 Jul 2026 01:44:56 +0000 Subject: [PATCH 009/140] 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 769d63a0d3e8..a55fa083990e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-An7vZmA+sVlxoSOMK4vQvkq3KJOF6Rm9IBU62LN3Hkg=", - "aarch64-linux": "sha256-Hx4trrBLxRDyY4HQc96EQZUa8abIeetNvE8GtRFvrrc=", - "aarch64-darwin": "sha256-/eXdkybUkDCrfQRCkZHsgCa3BM/OfaQzH/z7OxwlpVM=", - "x86_64-darwin": "sha256-flhsuFXikpI8tzK7nTwIVF/jzpmb5p+BWxGa/5vhN54=" + "x86_64-linux": "sha256-Ulflihjr8JDRVyNxSchoqaey6z/12256zs1FOiw+4vo=", + "aarch64-linux": "sha256-wP6p30a9f7s3tua5qbIq/10OHdbJb8xUni5KxE3F+/k=", + "aarch64-darwin": "sha256-7+4skFiJ+tLSjNG6WR53bT/IektA5B7gEcWLYpkK4z8=", + "x86_64-darwin": "sha256-NL945zD4J6HomH5SF0XIO8ZFCbIh2MTsJFtTyuxm2Q0=" } } From 55552c521fb428644f64e91c7fa7662656059ceb Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:45:39 -0500 Subject: [PATCH 010/140] fix(provider): force openai reasoning variants (#34702) --- packages/opencode/src/provider/transform.ts | 18 ++++- .../opencode/test/provider/transform.test.ts | 80 ++++++++++++++++++- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 466b05b62161..2a8c3cfb413c 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1263,6 +1263,16 @@ const SLUG_OVERRIDES: Record = { } export function providerOptions(model: Provider.Model, options: { [x: string]: any }) { + const usesOpenAIReasoningGate = + model.api.npm === "@ai-sdk/openai" || + model.api.npm === "@ai-sdk/azure" || + model.api.npm === "@ai-sdk/amazon-bedrock/mantle" + const normalized = + usesOpenAIReasoningGate && + (model.capabilities.reasoning || options.reasoningEffort !== undefined || options.reasoningSummary !== undefined) + ? { ...options, forceReasoning: true } + : options + if (model.api.npm === "@ai-sdk/gateway") { // Gateway providerOptions are split across two namespaces: // - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.) @@ -1272,8 +1282,8 @@ export function providerOptions(model: Provider.Model, options: { [x: string]: a const i = model.api.id.indexOf("/") const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined - const gateway = options.gateway - const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway")) + const gateway = normalized.gateway + const rest = Object.fromEntries(Object.entries(normalized).filter(([k]) => k !== "gateway")) const has = Object.keys(rest).length > 0 const result: Record = {} @@ -1307,9 +1317,9 @@ export function providerOptions(model: Provider.Model, options: { [x: string]: a // providerOptions["openai"], but OpenAIResponsesLanguageModel checks // "azure" first. Pass both so model options work on either code path. if (model.api.npm === "@ai-sdk/azure") { - return { openai: options, azure: options } + return { openai: normalized, azure: normalized } } - return { [key]: options } + return { [key]: normalized } } export function maxOutputTokens(model: Provider.Model, outputTokenMax = OUTPUT_TOKEN_MAX): number { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 550903bf46e9..912dbb13cb2f 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -613,6 +613,84 @@ describe("ProviderTransform.providerOptions", () => { }) }) + test("forces reasoning for custom OpenAI package models with explicit effort", () => { + const model = createModel({ + providerID: "meta", + api: { + id: "muse-spark", + url: "https://api.ai.meta.com/v1", + npm: "@ai-sdk/openai", + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh", reasoningSummary: "auto" })).toEqual({ + openai: { forceReasoning: true, reasoningEffort: "xhigh", reasoningSummary: "auto" }, + }) + }) + + test("forces reasoning for OpenAI package models marked reasoning-capable", () => { + expect(ProviderTransform.providerOptions(createModel(), { store: false })).toEqual({ + openai: { forceReasoning: true, store: false }, + }) + }) + + test("forces reasoning for explicit effort even when model is not marked reasoning-capable", () => { + const model = createModel({ + capabilities: { + temperature: true, + reasoning: false, + 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, + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({ + openai: { forceReasoning: true, reasoningEffort: "xhigh" }, + }) + }) + + test("forces reasoning for Azure OpenAI models with explicit effort", () => { + const model = createModel({ + providerID: "azure", + api: { + id: "custom-gpt-5-deployment", + url: "https://azure.openai.example.com/openai/v1", + npm: "@ai-sdk/azure", + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({ + openai: { forceReasoning: true, reasoningEffort: "xhigh" }, + azure: { forceReasoning: true, reasoningEffort: "xhigh" }, + }) + }) + + test("forces reasoning for Bedrock Mantle OpenAI models with explicit effort", () => { + const model = createModel({ + providerID: "amazon-bedrock", + api: { + id: "openai.gpt-5-custom", + url: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", + npm: "@ai-sdk/amazon-bedrock/mantle", + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({ + openai: { forceReasoning: true, reasoningEffort: "xhigh" }, + }) + }) + + test("overrides forceReasoning false when reasoning should be forced", () => { + expect( + ProviderTransform.providerOptions(createModel(), { forceReasoning: false, reasoningEffort: "xhigh" }), + ).toEqual({ + openai: { forceReasoning: true, reasoningEffort: "xhigh" }, + }) + }) + test("uses gateway model provider slug for gateway models", () => { const model = createModel({ providerID: "vercel", @@ -707,7 +785,7 @@ describe("ProviderTransform.providerOptions", () => { }) expect(ProviderTransform.providerOptions(model, { reasoningEffort: "medium" })).toEqual({ - openai: { reasoningEffort: "medium" }, + openai: { forceReasoning: true, reasoningEffort: "medium" }, }) }) From af72cec799f8c07184779670c7a8852d522a6afe Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:02:10 +0800 Subject: [PATCH 011/140] feat(desktop): scope tabs to windows (#34669) --- .../timeline/timeline-test-helpers.ts | 2 +- .../regression/cross-server-tab-close.spec.ts | 4 +- .../session-todo-dock-navigation.spec.ts | 2 +- .../app/e2e/smoke/session-timeline.spec.ts | 4 +- packages/app/src/context/platform.tsx | 3 + packages/app/src/context/tabs.tsx | 4 +- packages/app/src/utils/persist.ts | 24 +++++- packages/desktop/src/main/index.ts | 25 +++--- packages/desktop/src/main/ipc.ts | 16 +++- packages/desktop/src/main/store-keys.ts | 1 + packages/desktop/src/main/windows.ts | 76 ++++++++++++++++++- packages/desktop/src/preload/index.ts | 1 + packages/desktop/src/preload/types.ts | 1 + packages/desktop/src/renderer/index.tsx | 64 +++++++++++----- 14 files changed, 185 insertions(+), 42 deletions(-) diff --git a/packages/app/e2e/performance/timeline/timeline-test-helpers.ts b/packages/app/e2e/performance/timeline/timeline-test-helpers.ts index 401fb74496cd..6360b4a79318 100644 --- a/packages/app/e2e/performance/timeline/timeline-test-helpers.ts +++ b/packages/app/e2e/performance/timeline/timeline-test-helpers.ts @@ -45,7 +45,7 @@ export async function installStressSessionTabs(page: Page, input?: { draftID?: s }), ) localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify([ ...sessionIDs.map((sessionId) => ({ type: "session", diff --git a/packages/app/e2e/regression/cross-server-tab-close.spec.ts b/packages/app/e2e/regression/cross-server-tab-close.spec.ts index 031440f16602..159b5a506767 100644 --- a/packages/app/e2e/regression/cross-server-tab-close.spec.ts +++ b/packages/app/e2e/regression/cross-server-tab-close.spec.ts @@ -14,7 +14,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify([ { type: "session", server: "http://127.0.0.1:4096", sessionId: sessionA }, { type: "session", server: serverB, sessionId: sessionB }, @@ -52,7 +52,7 @@ test("legacy session routes preserve an existing tab's server", async ({ page }) localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify([{ type: "session", server: serverB, sessionId: sessionB }]), ) }, diff --git a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts index fd0b00f71cdd..603c411d5513 100644 --- a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts +++ b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts @@ -145,7 +145,7 @@ async function configurePage(page: Page) { }), ) localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))), ) }, diff --git a/packages/app/e2e/smoke/session-timeline.spec.ts b/packages/app/e2e/smoke/session-timeline.spec.ts index 5bca533e6a60..a73cc0ccdd7d 100644 --- a/packages/app/e2e/smoke/session-timeline.spec.ts +++ b/packages/app/e2e/smoke/session-timeline.spec.ts @@ -127,7 +127,7 @@ test.describe("smoke: session timeline", () => { await page.addInitScript( ({ dirBase64, sourceID, targetID }) => { localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify( [sourceID, targetID].map((sessionId) => ({ type: "session", @@ -253,7 +253,7 @@ test.describe("smoke: session timeline", () => { await page.addInitScript( ({ dirBase64, sourceID, targetID }) => { localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify( [sourceID, targetID].map((sessionId) => ({ type: "session", diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index de12e8c3f66f..e59bec9aafd9 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -64,6 +64,9 @@ type PlatformBase = { /** Storage mechanism, defaults to localStorage */ storage?: (name?: string) => SyncStorage | AsyncStorage + /** Stable platform window identity for window-scoped persistence */ + windowID?: string + /** Application-global desktop updater */ updater?: UpdaterPlatform diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index 02a5ac7fd694..6a73d7736dc7 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -51,7 +51,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ const fallback = server.key const [store, setStore, _, ready] = persisted( { - ...Persist.global("tabs"), + ...Persist.window("tabs"), migrate: (value: unknown) => { if (!Array.isArray(value)) return value return value.map((tab) => { @@ -62,7 +62,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ }, createStore([]), ) - const [recent, setRecent, , recentReady] = persisted(Persist.global("tabs.recent"), createStore({})) + const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore({})) const params = useParams() const navigate = useNavigate() diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index 7c0fe28fbd5e..671b798f6118 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -16,6 +16,7 @@ type PersistedWithReady = [ type PersistTarget = { storage?: string + scope?: "window" legacyStorageNames?: string[] key: string legacy?: string[] @@ -24,6 +25,7 @@ type PersistTarget = { const LEGACY_STORAGE = "default.dat" const GLOBAL_STORAGE = "opencode.global.dat" +const WINDOW_STORAGE = "opencode.window" const LOCAL_PREFIX = "opencode." const fallback = new Map() @@ -347,6 +349,11 @@ function draftStorage(draftID: string) { return `opencode.draft.${head}.${sum}.dat` } +function windowStorage(windowID: string) { + const safe = (windowID || "browser").replace(/[^a-zA-Z0-9._-]/g, "-") + return `${WINDOW_STORAGE}.${safe}.dat` +} + function legacyWorkspaceStorage(dir: string) { const storage = workspaceStorage(pathKey(dir)) const result = new Set() @@ -467,6 +474,8 @@ export const PersistTesting = { localStorageWithPrefix, migrateLegacy, normalize, + resolveTarget, + windowStorage, workspaceStorage, } @@ -474,6 +483,9 @@ export const Persist = { global(key: string, legacy?: string[]): PersistTarget { return { storage: GLOBAL_STORAGE, key, legacy } }, + window(key: string, legacy?: string[]): PersistTarget { + return { scope: "window", key, legacy } + }, draft(draftID: string, key: string, legacy?: string[]): PersistTarget { return { storage: draftStorage(draftID), key: `draft:${key}`, legacy } }, @@ -503,6 +515,16 @@ export const Persist = { }, } +function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget { + if (target.scope !== "window") return target + if (platform.platform === "desktop" && !platform.windowID) return { ...target, storage: GLOBAL_STORAGE } + const windowID = platform.platform === "desktop" ? (platform.windowID ?? "browser") : "browser" + return { + ...target, + storage: windowStorage(windowID), + } +} + export function removePersisted( target: { storage?: string; legacyStorageNames?: string[]; key: string }, platform?: Platform, @@ -533,7 +555,7 @@ export function persisted( store: [Store, SetStoreFunction], ): PersistedWithReady { const platform = usePlatform() - const config: PersistTarget = typeof target === "string" ? { key: target } : target + const config = resolveTarget(typeof target === "string" ? { key: target } : target, platform) const defaults = snapshot(store[0]) const legacy = config.legacy ?? [] diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index 383e4d994232..33e579a51e2e 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -6,7 +6,7 @@ import { homedir, tmpdir } from "node:os" import { join } from "node:path" import { getCACertificates, setDefaultCACertificates } from "node:tls" import type { Event } from "electron" -import { app, BrowserWindow } from "electron" +import { app } from "electron" import { Deferred, Effect, Fiber } from "effect" import contextMenu from "electron-context-menu" @@ -28,11 +28,13 @@ import { } from "./server" import { setupAutoUpdater, showUpdaterDialog } from "./updater" import { - createMainWindow, + getLastFocusedWindow, registerRendererProtocol, setRelaunchHandler, + setAppQuitting, setBackgroundColor, setDockIcon, + restoreMainWindows, } from "./windows" import { createWslServersController } from "./wsl/servers" import { registerWslIpcHandlers } from "./wsl/ipc" @@ -53,7 +55,6 @@ const TEST_ONBOARDING = process.env.OPENCODE_TEST_ONBOARDING === "1" const jsCallStackFeature = "DocumentPolicyIncludeJSCallStacksInCrashReports" let logger: ReturnType -let mainWindow: BrowserWindow | null = null let server: SidecarListener | null = null const pendingDeepLinks: string[] = [] @@ -70,7 +71,8 @@ function useEnvProxy() { function emitDeepLinks(urls: string[]) { if (urls.length === 0) return pendingDeepLinks.push(...urls) - if (mainWindow) sendDeepLinks(mainWindow, urls) + const win = getLastFocusedWindow() + if (win) sendDeepLinks(win, urls) } async function killSidecar() { @@ -194,9 +196,10 @@ const main = Effect.gen(function* () { logger.log("deep link received via second-instance", { urls }) emitDeepLinks(urls) } - if (mainWindow) { - mainWindow.show() - mainWindow.focus() + const win = getLastFocusedWindow() + if (win) { + win.show() + win.focus() } }) @@ -207,10 +210,12 @@ const main = Effect.gen(function* () { }) app.on("before-quit", () => { + setAppQuitting() void stopSidecars() }) app.on("will-quit", () => { + setAppQuitting() void stopSidecars() }) @@ -347,11 +352,11 @@ const main = Effect.gen(function* () { yield* Fiber.await(loadingTask) - mainWindow = createMainWindow() - if (mainWindow) { + const windows = restoreMainWindows() + if (windows.length) { createMenu({ trigger: (id) => { - const win = BrowserWindow.getFocusedWindow() ?? mainWindow + const win = getLastFocusedWindow() if (win) sendMenuCommand(win, id) }, checkForUpdates: () => { diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 4aaa64d250d1..32e314cb8819 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -9,7 +9,13 @@ import type { FatalRendererError, ServerReadyData, TitlebarTheme } from "../prel import { runDesktopMenuAction } from "./desktop-menu-actions" import { assertAttachmentBudget, createPickedFileAuthorizations } from "./attachment-picker" import { getStore } from "./store" -import { getPinchZoomEnabled, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows" +import { + getPinchZoomEnabled, + getWindowID, + setPinchZoomEnabled, + setTitlebar, + updateTitlebar, +} from "./windows" import type { UpdaterController } from "./updater-controller" import { createUpdaterSubscriptions } from "./updater-subscriptions" @@ -190,6 +196,14 @@ export function registerIpcHandlers(deps: Deps) { ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length) + ipcMain.handle("get-window-id", (event: IpcMainInvokeEvent) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (!win) throw new Error("Window not found") + const id = getWindowID(win) + if (!id) throw new Error("Window ID not found") + return id + }) + ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => { const win = BrowserWindow.fromWebContents(event.sender) return win?.isFocused() ?? false diff --git a/packages/desktop/src/main/store-keys.ts b/packages/desktop/src/main/store-keys.ts index f05018a26920..270ffe7504e0 100644 --- a/packages/desktop/src/main/store-keys.ts +++ b/packages/desktop/src/main/store-keys.ts @@ -2,3 +2,4 @@ export const SETTINGS_STORE = "opencode.settings" export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl" export const WSL_SERVERS_KEY = "wslServers" export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled" +export const WINDOW_IDS_KEY = "windowIds" diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts index 8ede9d383b5c..aae0401c83ed 100644 --- a/packages/desktop/src/main/windows.ts +++ b/packages/desktop/src/main/windows.ts @@ -2,13 +2,15 @@ import windowState from "electron-window-state" import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve" import type { DesktopTheme } from "@opencode-ai/ui/theme/types" import oc2ThemeJson from "../../../ui/src/theme/themes/oc-2.json" +import { randomUUID } from "node:crypto" +import { rmSync } from "node:fs" import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol } from "electron" import { dirname, isAbsolute, join, relative, resolve } from "node:path" import { fileURLToPath, pathToFileURL } from "node:url" import type { TitlebarTheme } from "../preload/types" import { exportDebugLogs, write as writeLog } from "./logging" import { getStore } from "./store" -import { PINCH_ZOOM_ENABLED_KEY } from "./store-keys" +import { PINCH_ZOOM_ENABLED_KEY, WINDOW_IDS_KEY } from "./store-keys" import { createUnresponsiveSampler } from "./unresponsive" const root = dirname(fileURLToPath(import.meta.url)) @@ -42,8 +44,12 @@ let relaunchHandler = () => { app.relaunch() app.exit(0) } +let appQuitting = false +let lastFocusedWindowID: string | undefined const titlebarThemes = new WeakMap>() const pinchZoomEnabled = new WeakMap() +const windowIDs = new WeakMap() +const windowsByID = new Map() const titlebarHeight = 40 const maxZoomLevel = 10 const minZoomLevel = 0.2 @@ -52,6 +58,10 @@ export function setRelaunchHandler(handler: () => void) { relaunchHandler = handler } +export function setAppQuitting() { + appQuitting = true +} + export function setBackgroundColor(color: string) { backgroundColor = color BrowserWindow.getAllWindows().forEach((win) => win.setBackgroundColor(color)) @@ -111,14 +121,33 @@ export function getPinchZoomEnabled() { return getStore().get(PINCH_ZOOM_ENABLED_KEY) === true } +export function getWindowID(win: BrowserWindow) { + return windowIDs.get(win) +} + +export function getLastFocusedWindow() { + const focused = BrowserWindow.getFocusedWindow() + if (focused) return focused + if (!lastFocusedWindowID) return null + const win = windowsByID.get(lastFocusedWindowID) + if (!win || win.isDestroyed()) return null + return win +} + +export function restoreMainWindows() { + const ids = readWindowIDs() + return (ids.length ? ids : [randomUUID()]).map((id) => createMainWindow(id)) +} + export function setDockIcon() { if (process.platform !== "darwin") return const icon = nativeImage.createFromPath(join(iconsDir(), "dock.png")) if (!icon.isEmpty()) app.dock?.setIcon(icon) } -export function createMainWindow() { +export function createMainWindow(id: string = randomUUID()) { const state = windowState({ + file: windowStateFile(id), defaultWidth: 1280, defaultHeight: 800, }) @@ -156,7 +185,7 @@ export function createMainWindow() { }) allowRendererPermissions(win) - wireWindowRecovery(win, "main") + wireWindowRecovery(win, id) win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { const { requestHeaders } = details @@ -171,6 +200,7 @@ export function createMainWindow() { }) state.manage(win) + registerWindow(win, id) loadWindow(win, "index.html") wireZoom(win) @@ -181,6 +211,46 @@ export function createMainWindow() { return win } +function registerWindow(win: BrowserWindow, id: string) { + windowIDs.set(win, id) + windowsByID.set(id, win) + persistWindowID(id) + + win.on("focus", () => { + lastFocusedWindowID = id + }) + win.on("closed", () => { + windowsByID.delete(id) + if (lastFocusedWindowID === id) lastFocusedWindowID = windowsByID.keys().next().value + if (!appQuitting) removeWindowID(id) + }) +} + +function readWindowIDs() { + const value = getStore().get(WINDOW_IDS_KEY) + if (!Array.isArray(value)) return [] + return value.filter((id): id is string => typeof id === "string" && id.length > 0) +} + +function writeWindowIDs(ids: string[]) { + getStore().set(WINDOW_IDS_KEY, [...new Set(ids)]) +} + +function persistWindowID(id: string) { + const ids = readWindowIDs() + if (ids.includes(id)) return + writeWindowIDs([...ids, id]) +} + +function removeWindowID(id: string) { + writeWindowIDs(readWindowIDs().filter((item) => item !== id)) + rmSync(join(app.getPath("userData"), windowStateFile(id)), { force: true }) +} + +function windowStateFile(id: string) { + return `window-state-${id.replace(/[^a-zA-Z0-9._-]/g, "-")}.json` +} + export function registerRendererProtocol() { if (protocol.isProtocolHandled(rendererProtocol)) return diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index a5cb1632eb90..f08a81c0d1af 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -73,6 +73,7 @@ const api: ElectronAPI = { storeLength: (name) => ipcRenderer.invoke("store-length", name), getWindowCount: () => ipcRenderer.invoke("get-window-count"), + getWindowID: () => ipcRenderer.invoke("get-window-id"), onMenuCommand: (cb) => { const handler = (_: unknown, id: string) => cb(id) ipcRenderer.on("menu-command", handler) diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts index 6bb2e029ca2f..5401c2070d7a 100644 --- a/packages/desktop/src/preload/types.ts +++ b/packages/desktop/src/preload/types.ts @@ -62,6 +62,7 @@ export type ElectronAPI = { storeLength: (name: string) => Promise getWindowCount: () => Promise + getWindowID: () => Promise onMenuCommand: (cb: (id: string) => void) => () => void onDeepLink: (cb: (urls: string[]) => void) => () => void diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index fd91badea946..febdec50ffba 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -63,7 +63,10 @@ const [updaterState, setUpdaterState] = createSignal({ status: "di void window.api.updater.subscribe(setUpdaterState) const deepLinkEvent = "opencode:deep-link" -const lastActiveUrlKey = "opencode.desktop.last-active-url" + +type DesktopWindowState = { + id?: string +} const emitDeepLinks = (urls: string[]) => { if (urls.length === 0) return @@ -78,31 +81,35 @@ const listenForDeepLinks = () => { return window.api.onDeepLink((urls) => emitDeepLinks(urls)) } -function getLastActiveUrl() { +function windowLastActiveUrlKey(windowID: string) { + return `opencode.desktop.window.${windowID}.last-active-url` +} + +function getLastActiveUrl(windowID: string) { if (typeof localStorage !== "object") return "/" try { - const value = localStorage.getItem(lastActiveUrlKey) + const value = localStorage.getItem(windowLastActiveUrlKey(windowID)) if (value?.startsWith("/") && !value.startsWith("//")) return value } catch {} return "/" } -function setLastActiveUrl(value: string) { +function setLastActiveUrl(windowID: string, value: string) { if (typeof localStorage !== "object") return try { - localStorage.setItem(lastActiveUrlKey, value) + localStorage.setItem(windowLastActiveUrlKey(windowID), value) } catch {} } -function DesktopMemoryRouter(props: BaseRouterProps) { +function DesktopMemoryRouter(props: BaseRouterProps & { windowID: string }) { const history = createMemoryHistory() - const initialUrl = getLastActiveUrl() + const initialUrl = getLastActiveUrl(props.windowID) if (initialUrl !== "/") history.set({ value: initialUrl, replace: true, scroll: false }) - onCleanup(history.listen(setLastActiveUrl)) + onCleanup(history.listen((value) => setLastActiveUrl(props.windowID, value))) return } -const createPlatform = (): Platform => { +const createPlatform = (windowState: DesktopWindowState): Platform => { const attachmentPaths = new WeakMap() const os = (() => { const ua = navigator.userAgent @@ -161,6 +168,7 @@ const createPlatform = (): Platform => { platform: "desktop", os, version: pkg.version, + windowID: windowState.id, async openDirectoryPickerDialog(opts) { return window.api.openDirectoryPicker({ @@ -307,8 +315,16 @@ window.api.onMenuCommand((id) => { }) listenForDeepLinks() -render(() => { - const platform = createPlatform() +function LoadingSplash() { + return ( +
+ +
+ ) +} + +function DesktopRoot(props: { windowState: DesktopWindowState }) { + const platform = createPlatform(props.windowState) const loadLocale = async () => { const current = await platform.storage?.("opencode.global.dat").getItem("language") const legacy = current ? undefined : await platform.storage?.().getItem("language.v1") @@ -328,6 +344,7 @@ render(() => { const [defaultServer] = createResource(() => platform.getDefaultServer?.()) const [locale] = createResource(loadLocale) + const router = (props: BaseRouterProps) => function handleClick(e: MouseEvent) { const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null @@ -357,12 +374,6 @@ render(() => { function App() { const wslServers = useWslServers() - const splash = ( -
- -
- ) - const ready = createMemo( () => !defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading, ) @@ -389,10 +400,10 @@ render(() => { ) return ( - + }> {(key) => ( - + )} @@ -415,4 +426,19 @@ render(() => { ) +} + +render(() => { + const [windowState] = createResource(async () => { + const api = window.api as typeof window.api & { + getWindowID?: () => Promise + } + return { id: await api.getWindowID?.() } + }) + + return ( + } keyed> + {(state) => } + + ) }, root!) From bd7f7ad98b5326277032e9df2dd1d3d5c1c3c327 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 1 Jul 2026 07:03:41 +0000 Subject: [PATCH 012/140] chore: generate --- packages/desktop/src/main/ipc.ts | 8 +------- packages/desktop/src/renderer/index.tsx | 4 +++- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 32e314cb8819..c6369bc0d1f5 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -9,13 +9,7 @@ import type { FatalRendererError, ServerReadyData, TitlebarTheme } from "../prel import { runDesktopMenuAction } from "./desktop-menu-actions" import { assertAttachmentBudget, createPickedFileAuthorizations } from "./attachment-picker" import { getStore } from "./store" -import { - getPinchZoomEnabled, - getWindowID, - setPinchZoomEnabled, - setTitlebar, - updateTitlebar, -} from "./windows" +import { getPinchZoomEnabled, getWindowID, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows" import type { UpdaterController } from "./updater-controller" import { createUpdaterSubscriptions } from "./updater-subscriptions" diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index febdec50ffba..5369ab21e310 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -344,7 +344,9 @@ function DesktopRoot(props: { windowState: DesktopWindowState }) { const [defaultServer] = createResource(() => platform.getDefaultServer?.()) const [locale] = createResource(loadLocale) - const router = (props: BaseRouterProps) => + const router = (props: BaseRouterProps) => ( + + ) function handleClick(e: MouseEvent) { const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null From 5e1a1ed2e2c590186ebb82b125ba43717ee33024 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:04:18 +0800 Subject: [PATCH 013/140] fix(app): scope session page errors (#34254) --- packages/app/src/app.tsx | 121 +----- .../app/src/components/titlebar-tab-nav.tsx | 128 +++--- .../app/src/components/titlebar-tab-strip.tsx | 5 +- packages/app/src/i18n/en.ts | 5 + packages/app/src/pages/error.tsx | 2 +- packages/app/src/pages/session.tsx | 407 ++++++++++++++---- 6 files changed, 403 insertions(+), 265 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 05b17a1309ea..4ab0a95c23e0 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -31,20 +31,19 @@ import { Dynamic } from "solid-js/web" import { CommandProvider, useCommand, type CommandOption } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" -import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk" -import { ServerSyncProvider, useServerSync } from "@/context/server-sync" +import { ServerSDKProvider } from "@/context/server-sdk" +import { ServerSyncProvider } from "@/context/server-sync" import { GlobalProvider, useGlobal } from "@/context/global" import { HighlightsProvider } from "@/context/highlights" import { LanguageProvider, type Locale, useLanguage } from "@/context/language" import { LayoutProvider } from "@/context/layout" import { ModelsProvider } from "@/context/models" -import { NotificationProvider, useNotification } from "@/context/notification" +import { NotificationProvider } from "@/context/notification" import { PermissionProvider } from "@/context/permission" import { usePlatform } from "@/context/platform" import { PromptProvider } from "@/context/prompt" import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" import { SettingsProvider, useSettings } from "@/context/settings" -import { TerminalProvider } from "@/context/terminal" import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs" import { SDKProvider, useSDK } from "@/context/sdk" import { WslServersProvider } from "@/wsl/context" @@ -53,16 +52,9 @@ import LegacyLayout from "@/pages/layout" import NewLayout from "@/pages/layout-new" import { ErrorPage } from "./pages/error" import { useCheckServerHealth } from "./utils/server-health" -import { - legacySessionHref, - legacySessionServer, - requireServerKey, - selectSessionLineage, - sessionHref, -} from "./utils/session-route" -import { isSessionNotFoundError } from "./utils/server-errors" - -import Session from "@/pages/session" +import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route" + +import { SessionPage, TargetSessionRoute as TargetSessionRouteContent } from "@/pages/session" import { NewHome, LegacyHome } from "@/pages/home" const NewSession = lazy(() => import("@/pages/new-session")) @@ -96,11 +88,7 @@ const SessionRoute = () => { tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt) }) - return ( - - - - ) + return } const TargetSessionRoute = () => { @@ -115,76 +103,13 @@ const TargetSessionRoute = () => { - + ) } -function ResolvedTargetSessionRoute() { - const params = useParams<{ serverKey: string; id: string }>() - const settings = useSettings() - const tabs = useTabs() - const sync = useServerSync() - const serverKey = createMemo(() => requireServerKey(params.serverKey)) - const cached = createMemo(() => sync().session.lineage.peek(params.id)) - const [resolved] = createResource( - () => { - if (cached()) return - return { id: params.id, server: serverKey(), sync: sync() } - }, - ({ id, server, sync }) => - sync.session.lineage.resolve(id).catch((error) => { - if (isSessionNotFoundError(error, id)) tabs.removeSessionTab({ server, sessionId: id }) - throw error - }), - ) - const current = createMemo(() => selectSessionLineage(params.id, cached(), resolved())) - const directory = createMemo(() => current()?.session.directory) - const targetDirectory = () => directory()! - - createEffect(() => { - const session = current() - if (!session) return - tabs.addSessionTab({ - server: serverKey(), - sessionId: session.root.id, - }) - }) - - return ( - params.id}> - }> - - } - > - - - - - - - - - - ) -} - -function TargetSessionPage() { - const sdk = useSDK() - const serverSDK = useServerSDK() - return ( - - - - - - ) -} - // Wraps the non-draft routes. They are gated on (and keyed to) the globally selected // server via ServerKey, then provide the server-scoped shell (Permission/Layout/ // Notification/Models + the visual Layout) for that server. @@ -232,7 +157,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) { - + @@ -240,7 +165,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) { - + @@ -365,38 +290,14 @@ function NewAppLayout(props: ParentProps) { ) } -function TargetServerScopedProviders(props: ServerScopedShellProps) { +function DraftServerScopedProviders(props: ParentProps<{ directory?: () => string | undefined }>) { return ( - {props.children} ) } -function MarkSessionNotificationsViewed(props: { sessionID?: () => string | undefined }) { - const notification = useNotification() - createEffect(() => { - const sessionID = props.sessionID?.() - if (!notification.ready() || !sessionID) return - if (notification.session.unseenCount(sessionID) === 0) return - notification.session.markViewed(sessionID) - }) - return null -} - -function SessionProviders(props: ParentProps) { - return ( - - - - {props.children} - - - - ) -} - // The draft page only renders the prompt composer, so it drops TerminalProvider. // FileProvider and CommentsProvider stay because PromptInput uses file search and comment context. function DraftProviders(props: ParentProps) { diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index 499c50bf6365..937da5aaf86a 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -21,6 +21,7 @@ export function TabNavItem(props: { href: string server: ServerConnection.Key session: () => Session | undefined + fallbackTitle?: string onTitleChange?: (title: string) => void onTitleChangeFailed?: (title: string) => void onClose: () => void @@ -56,6 +57,7 @@ export function TabNavItem(props: { if (!session) return return projectForSession(session, serverCtx()?.projects.list() ?? []) }) + const title = createMemo(() => props.session()?.title ?? props.fallbackTitle) const measureTitleOverflow = () => { if (!titleEl || editing()) { @@ -74,7 +76,7 @@ export function TabNavItem(props: { } createEffect(() => { - props.session()?.title + title() props.forceTruncate editing() scheduleTitleOverflow() @@ -133,9 +135,9 @@ export function TabNavItem(props: { createEffect(() => { if (editing()) return if (!titleEl) return - const title = props.session()?.title - if (title === undefined) return - titleEl.textContent = title + const value = title() + if (value === undefined) return + titleEl.textContent = value }) const openRename = (event: MouseEvent) => { @@ -196,26 +198,26 @@ export function TabNavItem(props: { closeTab(event) }} > - - {(session) => { - return ( - { - event.preventDefault() - event.stopPropagation() - }} - onClick={(event) => { - event.preventDefault() - if (editing()) return - if (props.suppressNavigation?.()) return - props.onNavigate() - }} - class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]" - > + + { + event.preventDefault() + event.stopPropagation() + }} + onClick={(event) => { + event.preventDefault() + if (editing()) return + if (props.suppressNavigation?.()) return + props.onNavigate() + }} + class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]" + > + + {(session) => ( - { - titleEl = el - titleEl.textContent = session().title - }} - data-slot="tab-title" - data-titlebar-tab-title - class="min-w-0 flex-1 outline-none leading-4" - classList={{ - "overflow-hidden text-clip whitespace-nowrap": !editing(), - "select-text": editing(), - }} - contenteditable={editing() ? true : undefined} - onDblClick={openRename} - onKeyDown={(event) => { - event.stopPropagation() - if (event.key === "Enter") { - event.preventDefault() - void closeRename(true) - return - } - if (event.key !== "Escape") return - event.preventDefault() - titleEl.textContent = session().title - void closeRename(false) - }} - onBlur={() => void closeRename(true)} - onPointerDown={(event) => { - if (!editing()) return - event.stopPropagation() - }} - onClick={(event) => { - if (!editing()) return - event.preventDefault() - }} - /> - - ) - }} + )} + + { + titleEl = el + titleEl.textContent = title() ?? "" + }} + data-slot="tab-title" + data-titlebar-tab-title + class="min-w-0 flex-1 outline-none leading-4" + classList={{ + "overflow-hidden text-clip whitespace-nowrap": !editing(), + "select-text": editing(), + }} + contenteditable={editing() ? true : undefined} + onDblClick={openRename} + onKeyDown={(event) => { + event.stopPropagation() + if (event.key === "Enter") { + event.preventDefault() + void closeRename(true) + return + } + if (event.key !== "Escape") return + event.preventDefault() + titleEl.textContent = props.session()?.title ?? "" + void closeRename(false) + }} + onBlur={() => void closeRename(true)} + onPointerDown={(event) => { + if (!editing()) return + event.stopPropagation() + }} + onClick={(event) => { + if (!editing()) return + event.preventDefault() + }} + /> +
diff --git a/packages/app/src/components/titlebar-tab-strip.tsx b/packages/app/src/components/titlebar-tab-strip.tsx index fe476d798ec6..9b877614d11d 100644 --- a/packages/app/src/components/titlebar-tab-strip.tsx +++ b/packages/app/src/components/titlebar-tab-strip.tsx @@ -31,6 +31,7 @@ function SessionTabSlot(props: { onClose: () => void }) { const tabs = useTabs() + const language = useLanguage() const sortable = useSortable({ get id() { return props.id @@ -50,6 +51,7 @@ function SessionTabSlot(props: { ({ id, ctx }) => ctx.sync.session.resolve(id).catch(() => undefined), ) const session = createMemo(() => cachedSession() ?? loadedSession()) + const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session()) let prefetched = false createEffect(() => { @@ -87,7 +89,7 @@ function SessionTabSlot(props: { data-tab-key={props.id} data-active={props.active()} class="relative flex w-56 min-w-7 max-w-56 flex-shrink" - classList={{ hidden: !session() }} + classList={{ hidden: !session() && !missingSession() }} > { @@ -96,6 +98,7 @@ function SessionTabSlot(props: { href={tabHref(props.tab)} server={props.tab.server} session={session} + fallbackTitle={missingSession() ? language.t("session.tab.unknown") : undefined} onTitleChange={(title) => { const value = session() const ctx = props.serverCtx() diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 3872d01ae78b..3a320cc7a5ec 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -604,7 +604,12 @@ export const dict = { "session.tab.session": "Session", "session.tab.review": "Review", "session.tab.context": "Context", + "session.tab.unknown": "Unknown Session", "session.panel.reviewAndFiles": "Review and files", + "session.error.notFound": "This session cannot be found", + "session.error.notFound.description": "This tab points to a session that no longer exists on this server.", + "session.error.notFound.closeTab": "Close Tab", + "session.error.serverConnection": "Can't connect to this server", "session.review.filesChanged": "{{count}} Files Changed", "session.review.change.one": "Change", "session.review.change.other": "Changes", diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx index 1c3b996e5709..86a997b39d7b 100644 --- a/packages/app/src/pages/error.tsx +++ b/packages/app/src/pages/error.tsx @@ -277,7 +277,7 @@ export const ErrorPage: Component = (props) => { return (
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 1c9391e0fdcf..ba672d8fe812 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -3,6 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query" import { batch, + ErrorBoundary, onCleanup, Show, Match, @@ -10,8 +11,10 @@ import { createMemo, createEffect, createComputed, + createResource, on, onMount, + type ParentProps, untrack, } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" @@ -19,29 +22,37 @@ import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { debounce } from "@solid-primitives/scheduled" import { useLocal } from "@/context/local" -import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" +import { FileProvider, selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" import { createStore } from "solid-js/store" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Select } from "@opencode-ai/ui/select" import { Tabs } from "@opencode-ai/ui/tabs" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge" import { Button } from "@opencode-ai/ui/button" import { showToast } from "@/utils/toast" import { base64Encode, checksum } from "@opencode-ai/core/util/encode" -import { useLocation, useNavigate, useSearchParams } from "@solidjs/router" +import { Navigate, useLocation, useNavigate, useParams, useSearchParams } from "@solidjs/router" import { NewSessionView, SessionHeader } from "@/components/session" -import { useComments } from "@/context/comments" +import { ErrorPage } from "@/pages/error" +import { CommentsProvider, useComments } from "@/context/comments" +import { DirectoryDataProvider } from "@/pages/directory-layout" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" -import { usePrompt } from "@/context/prompt" +import { ModelsProvider } from "@/context/models" +import { useNotification } from "@/context/notification" +import { PermissionProvider } from "@/context/permission" +import { PromptProvider, usePrompt } from "@/context/prompt" import { usePlatform } from "@/context/platform" -import { useSDK } from "@/context/sdk" +import { SDKProvider, useSDK } from "@/context/sdk" import { useServerSDK } from "@/context/server-sdk" +import { ServerConnection, serverName, useServer } from "@/context/server" import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" -import { useTerminal } from "@/context/terminal" +import { useTabs } from "@/context/tabs" +import { TerminalProvider, useTerminal } from "@/context/terminal" import { PromptInput } from "@/components/prompt-input" import { useSettingsCommand } from "@/components/settings-dialog" import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit" @@ -73,8 +84,8 @@ import { Identifier } from "@/utils/id" import { diffs as list } from "@/utils/diffs" import { Persist, persisted } from "@/utils/persist" import { extractPromptFromParts } from "@/utils/prompt" -import { formatServerError } from "@/utils/server-errors" -import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" +import { formatServerError, isSessionNotFoundError } from "@/utils/server-errors" +import { legacySessionHref, requireServerKey, selectSessionLineage, sessionHref } from "@/utils/session-route" import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs" import { createSessionOwnership } from "./session/session-ownership" @@ -91,6 +102,15 @@ const sessionViewState = () => ({ changes: "git" as ChangeMode, }) +function isLocalSessionNotFoundError(error: unknown, sessionID: string) { + return error instanceof Error && error.message === `Session not found: ${sessionID}` +} + +function isCurrentSessionNotFoundError(error: unknown, sessionID: string | undefined) { + if (!sessionID) return false + return isSessionNotFoundError(error, sessionID) || isLocalSessionNotFoundError(error, sessionID) +} + async function runPromptRollbackMutation(input: { capturePrompt: () => { current: () => T[]; set: (value: T[]) => void; reset: () => void } optimistic: (prompt: { set: (value: T[]) => void; reset: () => void }) => void @@ -114,6 +134,206 @@ async function runPromptRollbackMutation(input: { }) } +export function SessionPage() { + return ( + + + + ) +} + +export function TargetSessionRoute() { + const params = useParams<{ serverKey: string; id: string }>() + return ( + + + + + + ) +} + +function SessionRouteErrorBoundary( + props: ParentProps<{ sessionID?: string; serverKey?: ServerConnection.Key; padded?: boolean }>, +) { + const settings = useSettings() + return ( + + settings.general.newLayoutDesigns() ? ( + + + + + + ) : ( + + ) + } + > + {props.children} + + ) +} + +function SessionErrorFallback(props: { error: unknown; sessionID?: string; serverKey?: ServerConnection.Key }) { + const language = useLanguage() + const server = useServer() + const tabs = useTabs() + const displayServer = createMemo(() => { + const key = props.serverKey ?? server.key + const conn = server.list.find((item) => ServerConnection.key(item) === key) + return conn ? serverName(conn) : key + }) + const closeTab = () => { + if (!props.sessionID) return + tabs.removeSessionTab({ server: props.serverKey ?? server.key, sessionId: props.sessionID }) + } + if (isCurrentSessionNotFoundError(props.error, props.sessionID)) { + return ( +
+
+
+
{language.t("session.error.notFound")}
+
+ {language.t("session.error.notFound.description")} +
+
+ + {(sessionID) => ( +
+
+ {displayServer()} +
+ + {sessionID()} + +
+ )} +
+ + {language.t("session.error.notFound.closeTab")} + +
+
+ ) + } + return +} + +function ResolvedTargetSessionRoute() { + const params = useParams<{ serverKey: string; id: string }>() + const settings = useSettings() + const tabs = useTabs() + const sync = useServerSync() + const serverKey = createMemo(() => requireServerKey(params.serverKey)) + const cached = createMemo(() => sync().session.lineage.peek(params.id)) + const [resolved] = createResource( + () => { + if (cached()) return + return { id: params.id, sync: sync() } + }, + ({ id, sync }) => sync.session.lineage.resolve(id), + ) + const current = createMemo(() => selectSessionLineage(params.id, cached(), resolved())) + const directory = createMemo(() => current()?.session.directory) + const targetDirectory = () => directory()! + + createEffect(() => { + const session = current() + if (!session) return + tabs.addSessionTab({ + server: serverKey(), + sessionId: session.root.id, + }) + }) + + return ( + params.id}> + + } + > + + + + + + + + + ) +} + +function TargetSessionPage() { + const sdk = useSDK() + const serverSDK = useServerSDK() + return ( + + + + ) +} + +function TargetServerScopedProviders( + props: ParentProps<{ directory?: () => string | undefined; sessionID?: () => string | undefined }>, +) { + return ( + + + {props.children} + + ) +} + +function MarkSessionNotificationsViewed(props: { sessionID?: () => string | undefined }) { + const notification = useNotification() + createEffect(() => { + const sessionID = props.sessionID?.() + if (!notification.ready() || !sessionID) return + if (notification.session.unseenCount(sessionID) === 0) return + notification.session.markViewed(sessionID) + }) + return null +} + +function SessionProviders(props: ParentProps) { + return ( + + + + {props.children} + + + + ) +} + +function SessionRouteFrame(props: ParentProps<{ padded?: boolean }>) { + return ( +
+ {props.children} +
+ ) +} + +function SessionPanelFrame(props: ParentProps<{ newLayout: boolean; raised?: boolean }>) { + return ( +
+ {props.children} +
+ ) +} + export default function Page() { const serverSync = useServerSync() const layout = useLayout() @@ -1695,9 +1915,88 @@ export default function Page() { () => !isDesktop() && settings.general.newLayoutDesigns() && settings.general.mobileTitlebarPosition() === "bottom", ) - return ( -
+ const sessionErrorFallback = (error: unknown, reset: () => void) => { + createEffect(on(sessionKey, reset, { defer: true })) + return + } + + const sessionPanelContent = () => ( + <> {sessionSync() ?? ""} + + {mobileTabs(true)} + +
+ + +
+ {reviewContent({ + diffStyle: "unified", + classes: { + root: "pb-8 [&_[data-slot=session-review-list]]:pb-0", + header: "px-4 !h-16 !pb-4", + container: "px-4", + }, + loadingClass: "px-4 py-4 text-text-weak", + emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6", + })} +
+
+ + + {(_id) => ( + + !location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled() + } + centered={centered()} + setContentRef={(el) => { + content = el + autoScroll.contentRef(el) + + const root = scroller + if (root) scheduleScrollState(root) + }} + userMessages={visibleUserMessages()} + setHistoryAnchor={(handlers) => { + captureHistoryAnchor = handlers.capture + restoreHistoryAnchor = handlers.restore + }} + anchor={anchor} + setRevealMessage={(fn) => { + revealMessage = fn + }} + setScrollToEnd={(fn) => { + scrollToEnd = fn + }} + /> + )} + + + + + +
+
+ + {(_) => composerRegion()} + {mobileTabs(true, true)} + + ) + + return ( +
-
- - {mobileTabs(true)} - -
- - -
- {reviewContent({ - diffStyle: "unified", - classes: { - root: "pb-8 [&_[data-slot=session-review-list]]:pb-0", - header: "px-4 !h-16 !pb-4", - container: "px-4", - }, - loadingClass: "px-4 py-4 text-text-weak", - emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6", - })} -
-
- - - {(_id) => ( - - !location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled() - } - centered={centered()} - setContentRef={(el) => { - content = el - autoScroll.contentRef(el) - - const root = scroller - if (root) scheduleScrollState(root) - }} - userMessages={visibleUserMessages()} - setHistoryAnchor={(handlers) => { - captureHistoryAnchor = handlers.capture - restoreHistoryAnchor = handlers.restore - }} - anchor={anchor} - setRevealMessage={(fn) => { - revealMessage = fn - }} - setScrollToEnd={(fn) => { - scrollToEnd = fn - }} - /> - )} - - - - - -
-
- - {(_) => composerRegion()} - {mobileTabs(true, true)} -
+ + {settings.general.newLayoutDesigns() ? ( + {sessionPanelContent()} + ) : ( + sessionPanelContent() + )} +
size.start()}> @@ -1832,6 +2059,6 @@ export default function Page() {
-
+
) } From bf18cc971f1e78e0b5eb678a254a2467781be272 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 1 Jul 2026 07:06:04 +0000 Subject: [PATCH 014/140] chore: generate --- packages/app/src/pages/session.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index ba672d8fe812..36be14546bbf 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -202,9 +202,7 @@ function SessionErrorFallback(props: { error: unknown; sessionID?: string; serve {(sessionID) => (
-
- {displayServer()} -
+
{displayServer()}
{sessionID()} From 4d91f0cf289d2e0b991f962cab5fa508ac63b10d Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:09:00 +0530 Subject: [PATCH 015/140] feat(app): v2 wsl ui (#34233) Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> --- packages/app/src/i18n/en.ts | 15 + packages/app/src/wsl/add-server-probes.ts | 57 ++ packages/app/src/wsl/dialog-add-server.tsx | 935 ++++++++---------- .../app/src/wsl/dialog-add-wsl-server.css | 392 ++++++++ packages/app/src/wsl/settings-model.test.ts | 192 +++- packages/app/src/wsl/settings-model.ts | 344 ++++++- packages/app/src/wsl/settings.tsx | 14 +- packages/app/src/wsl/types.ts | 6 +- packages/desktop/src/main/wsl/ipc.ts | 12 +- packages/desktop/src/main/wsl/policy.ts | 7 + packages/desktop/src/main/wsl/servers.test.ts | 68 +- packages/desktop/src/main/wsl/servers.ts | 44 +- packages/desktop/src/preload/index.ts | 3 +- packages/ui/src/v2/components/button-v2.css | 11 +- .../src/v2/components/button-v2.stories.tsx | 11 +- packages/ui/src/v2/components/button-v2.tsx | 2 +- packages/ui/src/v2/components/loader-v2.css | 32 + .../src/v2/components/loader-v2.stories.tsx | 42 + packages/ui/src/v2/components/loader-v2.tsx | 31 + packages/ui/src/v2/components/radio-v2.css | 12 +- 20 files changed, 1612 insertions(+), 618 deletions(-) create mode 100644 packages/app/src/wsl/add-server-probes.ts create mode 100644 packages/app/src/wsl/dialog-add-wsl-server.css create mode 100644 packages/ui/src/v2/components/loader-v2.css create mode 100644 packages/ui/src/v2/components/loader-v2.stories.tsx create mode 100644 packages/ui/src/v2/components/loader-v2.tsx diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 3a320cc7a5ec..561fbb5888ec 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -388,6 +388,20 @@ export const dict = { "wsl.onboarding.chooseDistroFirst": "Choose a distro first.", "wsl.onboarding.loadFailed": "Failed to load WSL state.", "wsl.onboarding.loading": "Loading...", + "wsl.onboarding.installedDistros": "Installed distros", + "wsl.onboarding.checkAgain": "Check again", + "wsl.onboarding.distroStatus.ready": "Ready", + "wsl.onboarding.distroStatus.checking": "Checking...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode not installed", + "wsl.onboarding.distroStatus.missingTools": "Missing bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Unsupported · Use WSL 2", + "wsl.onboarding.needAnotherDistro": "Need another distro?", + "wsl.onboarding.needAnotherDistroHint": "Install a Linux distribution from the WSL catalog", + "wsl.onboarding.wslNotInstalled.title": "WSL not installed", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) is required before OpenCode can add a WSL server", + "wsl.onboarding.wslUnavailable.title": "WSL unavailable", + "wsl.onboarding.wslUnavailable.description": "OpenCode could not verify WSL on this machine.", "wsl.onboarding.installWsl": "Install WSL", "wsl.onboarding.windowsRestartRequired": "Restart Windows to finish installing WSL, then reopen OpenCode.", "wsl.onboarding.next": "Next", @@ -397,6 +411,7 @@ export const dict = { "wsl.onboarding.install": "Install", "wsl.onboarding.installing": "Installing...", "wsl.onboarding.installDistro": "Install distro", + "wsl.onboarding.searchDistros": "Search distros", "wsl.onboarding.wsl2Required": "WSL 2 is required.", "wsl.onboarding.toolsRequired": "This distro needs bash and curl.", "wsl.onboarding.openTerminal": "Open terminal", diff --git a/packages/app/src/wsl/add-server-probes.ts b/packages/app/src/wsl/add-server-probes.ts new file mode 100644 index 000000000000..f9e1fd85e85f --- /dev/null +++ b/packages/app/src/wsl/add-server-probes.ts @@ -0,0 +1,57 @@ +import { useMutation } from "@tanstack/solid-query" +import { createEffect } from "solid-js" +import type { Accessor } from "solid-js" +import { + addServerProbePlan, + createProbeFailureGate, + runAddableProbePlan, + type AddServerProbePlan, + type WslAddServerView, +} from "./settings-model" +import type { WslInstalledDistro, WslServersPlatform, WslServersState } from "./types" + +export function useWslAddServerProbes(input: { + state: Accessor + api: WslServersPlatform + view: Accessor + adding: Accessor + busy: Accessor + selectedDistro: Accessor + addableInstalledDistros: Accessor + onError: (error: unknown) => void +}) { + const gate = createProbeFailureGate() + const probe = useMutation(() => ({ + mutationFn: async (command: AddServerProbePlan) => { + if (command.kind === "addable") { + await runAddableProbePlan({ plan: command.plan, api: input.api }) + return + } + if (command.plan.action === "probeRuntime") await input.api.probeRuntime() + if (command.plan.action === "refreshDistros") await input.api.refreshDistros() + }, + onError: input.onError, + onSettled: (_result, error, command) => { + if (command) gate.settle(command.key, error) + }, + })) + + createEffect(() => { + if (probe.isPending) return + const command = addServerProbePlan({ + state: input.state(), + view: input.view(), + adding: input.adding(), + busy: input.busy(), + selectedDistro: input.selectedDistro(), + addableInstalledDistros: input.addableInstalledDistros(), + }) + if (!command || !gate.accepts(command.key)) return + probe.mutate(command) + }) + + return { + probingAddable: () => probe.isPending && probe.variables?.kind === "addable", + resetProbeFailure: () => gate.reset(), + } +} diff --git a/packages/app/src/wsl/dialog-add-server.tsx b/packages/app/src/wsl/dialog-add-server.tsx index 6c79824136fa..6600347d008f 100644 --- a/packages/app/src/wsl/dialog-add-server.tsx +++ b/packages/app/src/wsl/dialog-add-server.tsx @@ -1,20 +1,28 @@ -import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Spinner } from "@opencode-ai/ui/spinner" import { showToast } from "@opencode-ai/ui/toast" -import { createEffect, createMemo, For, Match, onCleanup, Show, Switch } from "solid-js" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2" +import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2" +import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2" +import { RadioGroupV2, RadioItemV2 } from "@opencode-ai/ui/v2/radio-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { createMemo, For, Show } from "solid-js" import { createStore } from "solid-js/store" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" +import { useWslAddServerProbes } from "./add-server-probes" import { useWslServers } from "./context" -import { enterWslOpencodeStep } from "./settings-model" +import { addServerViewModel, type AddServerText } from "./settings-model" +import "./dialog-add-wsl-server.css" -type WslServerStep = "wsl" | "distro" | "opencode" - -const STEPS: WslServerStep[] = ["wsl", "distro", "opencode"] +function isWslRuntimeMissing(error: string | null | undefined) { + if (!error) return true + return /WSL is not installed|not been installed|wsl(?:\.exe)? --install/i.test(error) +} -function isHiddenDistro(name: string) { - return /^docker-desktop(?:-data)?$/i.test(name) +function translate(language: ReturnType, value: AddServerText) { + if (value.params) return language.t(value.key, value.params) + return language.t(value.key) } interface DialogWslServerProps { @@ -22,210 +30,269 @@ interface DialogWslServerProps { } export function DialogAddWslServer(props: DialogWslServerProps = {}) { + const language = useLanguage() + const controller = useWslAddServerController(props) + const model = controller.model + const primaryButton = () => model().primaryButton + const primaryButtonStyle = () => { + const width = primaryButton().width + if (!width) return undefined + return { width } + } + + return ( + + {controller.loadError()}
} + > +
+ +
+
+ + } + > + + } + > + +
+ +
+
+
+ } + > + + + + {controller.view() === "main" + ? language.t("wsl.server.add") + : language.t("wsl.onboarding.installDistro")} + + + + + + controller.setCatalogSearch(event.currentTarget.value)} + /> +
+ + + {(item) => ( + {item.label}} + /> + )} + + +
+
+ + + {language.t("common.cancel")} + + + + + + + + + } + > + +
+ {language.t("wsl.onboarding.installedDistros")} + + {language.t("wsl.onboarding.checkAgain")} + +
+ + 0} + fallback={ +
+
+ {model().visibleInstalledDistros.length + ? language.t("wsl.onboarding.allDistrosAdded") + : language.t("wsl.onboarding.noDistros")} +
+
+ } + > +
+ + + {(item) => { + const status = () => model().distroStatuses[item.name] ?? null + return ( + {item.name}} + description={ + + {(value) => ( + + {translate(language, value().label)} + + )} + + } + /> + ) + }} + + +
+
+ + 0}> + + +
+ + + + {language.t("common.cancel")} + + + + + + + +
+
+ + + ) +} + +function useWslAddServerController(props: DialogWslServerProps) { const language = useLanguage() const platform = usePlatform() const dialog = useDialog() const wslServers = useWslServers() const api = platform.wslServers! const [store, setStore] = createStore({ - step: undefined as WslServerStep | undefined, + view: "main" as "main" | "catalog", selectedDistro: null as string | null, - installTarget: undefined as string | undefined, + catalogSearch: "", + catalogTarget: null as string | null, adding: false, }) const current = () => wslServers.data - let disposed = false - onCleanup(() => { - disposed = true - }) - const busy = createMemo(() => !!current()?.job || store.adding) - const visibleInstalledDistros = createMemo(() => - (current()?.installed ?? []).filter((item) => !isHiddenDistro(item.name)), - ) - const visibleOnlineDistros = createMemo(() => (current()?.online ?? []).filter((item) => !isHiddenDistro(item.name))) - const defaultInstalledDistro = createMemo(() => visibleInstalledDistros().find((item) => item.isDefault) ?? null) - const existingServerDistros = createMemo(() => new Set((current()?.servers ?? []).map((item) => item.config.distro))) - const addableInstalledDistros = createMemo(() => { - return visibleInstalledDistros().filter((item) => !existingServerDistros().has(item.name)) - }) - const selectedDistro = createMemo(() => { - if (store.selectedDistro && addableInstalledDistros().some((item) => item.name === store.selectedDistro)) { - return store.selectedDistro - } - const distro = defaultInstalledDistro() - if (distro && !existingServerDistros().has(distro.name)) return distro.name - return null - }) - const selectedProbe = createMemo(() => { - const distro = selectedDistro() - if (!distro) return null - return current()?.distroProbes[distro] ?? null - }) - const selectedInstalled = createMemo(() => { - const distro = selectedDistro() - if (!distro) return null - return (current()?.installed ?? []).find((item) => item.name === distro) ?? null - }) - const opencodeCheck = createMemo(() => { - const distro = selectedDistro() - if (!distro) return null - return current()?.opencodeChecks[distro] ?? null - }) - const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart) - const distroReady = createMemo(() => { - const probe = selectedProbe() - if (!probe || !selectedDistro()) return false - if (selectedInstalled()?.version === 1) return false - return probe.canExecute && probe.hasBash && probe.hasCurl - }) - const opencodeReady = createMemo(() => { - const check = opencodeCheck() - return !!check?.resolvedPath && !check.error - }) - const distroWarningProbe = createMemo(() => { - const probe = selectedProbe() - if (!probe) return null - if (distroReady()) return null - return probe - }) - const distroUnavailableMessage = createMemo(() => { - const probe = distroWarningProbe() - const distro = selectedDistro() - if (!probe || probe.canExecute || !distro) return null - if (!selectedInstalled()) return language.t("wsl.onboarding.distroNotInstalled", { distro }) - return language.t("wsl.onboarding.openDistroOnce", { distro }) - }) - const distroMissingTools = createMemo(() => { - const probe = distroWarningProbe() - if (!probe?.canExecute) return null - if (probe.hasBash && probe.hasCurl) return null - return probe - }) - const installableDistros = createMemo(() => { - const online = visibleOnlineDistros() - const installed = new Set(visibleInstalledDistros().map((item) => item.name)) - const hasVersionedUbuntu = online.some((item) => /^Ubuntu-\d/.test(item.name)) - return online - .filter((item) => !installed.has(item.name)) - .filter((item) => !(item.name === "Ubuntu" && hasVersionedUbuntu)) - }) - const installTarget = createMemo( - () => installableDistros().find((item) => item.name === store.installTarget) ?? installableDistros()[0] ?? null, - ) - const installingDistro = createMemo(() => current()?.job?.kind === "install-distro") - const installingOpencode = createMemo(() => { - const job = current()?.job - return job?.kind === "install-opencode" && job.distro === selectedDistro() - }) - const allReady = createMemo(() => wslReady() && distroReady() && opencodeReady()) - const addDisabled = createMemo(() => { - const job = current()?.job - if (!job) return store.adding - return store.adding || job.kind !== "probe-opencode" - }) - const recommendedStep = createMemo(() => { - if (!wslReady()) return "wsl" - if (!distroReady()) return "distro" - return "opencode" - }) - // activeStep falls back to recommendedStep when the user hasn't picked one. - // Once the user clicks a step tab we respect their choice rather than snapping - // them back when a probe result updates recommendedStep. - const activeStep = createMemo(() => store.step ?? recommendedStep()) - - const autoProbe = createMemo(() => { - const state = current() - if (!state || busy()) return null - if (state.pendingRestart) return null - if (!state.runtime) return { key: "runtime", run: () => api.probeRuntime() } - if (!wslReady()) return null - if (!state.installed.length && !state.online.length) { - return { key: "distros", run: () => api.refreshDistros() } - } - const distro = selectedDistro() - if (distro && !state.distroProbes[distro]) { - return { key: `probe-distro:${distro}`, run: () => api.probeDistro(distro) } - } - if (!distro || !distroReady()) return null - if (!state.opencodeChecks[distro]) { - return { key: `probe-opencode:${distro}`, run: () => api.probeOpencode(distro) } - } - return null - }) - - let lastAutoProbe: string | null = null - createEffect(() => { - const probe = autoProbe() - if (!probe || probe.key === lastAutoProbe) return - const key = probe.key - lastAutoProbe = key - void (async () => { - try { - await probe.run() - } catch (err) { - if (disposed) return - // Allow the same probe to run again when reactive inputs next change - // (e.g. user reselects a distro). Without this the user would be stuck - // on a transient wsl.exe failure until they pick a different distro. - if (lastAutoProbe === key) lastAutoProbe = null - requestError(language, err) - } - })() - }) - - const wslMessage = createMemo(() => { - const state = current() - if (!state || state.job?.kind === "runtime") return language.t("wsl.onboarding.checkingRuntime") - if (state.pendingRestart) return language.t("wsl.onboarding.restartRequired") - if (state.runtime?.available) return state.runtime.version ?? language.t("wsl.onboarding.ready") - return state.runtime?.error ?? language.t("wsl.onboarding.required") - }) - - const distroMessage = createMemo(() => { - const state = current() - if (!state) return language.t("wsl.onboarding.checkingDistros") - const distro = selectedDistro() - if (state.job?.kind === "install-distro") - return language.t("wsl.onboarding.installingDistro", { distro: state.job.distro }) - if (state.job?.kind === "probe-distro") - return language.t("wsl.onboarding.checkingDistro", { distro: state.job.distro }) - if (state.job?.kind === "distros") return language.t("wsl.onboarding.listingDistros") - if (distroUnavailableMessage()) return distroUnavailableMessage()! - if (selectedProbe() && distroReady()) - return language.t("wsl.onboarding.distroReady", { distro: selectedProbe()!.name }) - if (distro) return language.t("wsl.onboarding.finishingDistro", { distro }) - return language.t("wsl.onboarding.pickDistro") + const viewModel = (probingAddable: boolean) => + addServerViewModel({ + state: current(), + view: store.view, + selectedDistro: store.selectedDistro, + catalogSearch: store.catalogSearch, + catalogTarget: store.catalogTarget, + adding: store.adding, + probingAddable, + }) + const baseModel = createMemo(() => viewModel(false)) + const probes = useWslAddServerProbes({ + state: current, + api, + view: () => store.view, + adding: () => store.adding, + busy: () => baseModel().busy, + selectedDistro: () => baseModel().selectedDistro, + addableInstalledDistros: () => baseModel().addableInstalledDistros, + onError: (error) => requestError(language, error), }) + const model = createMemo(() => viewModel(probes.probingAddable())) - const opencodeMessage = createMemo(() => { - const state = current() - if (!state) return language.t("wsl.onboarding.checkingOpencode") - const distro = selectedDistro() - if (state.job?.kind === "install-opencode") { - return distro - ? language.t("wsl.onboarding.updatingOpencodeIn", { distro }) - : language.t("wsl.onboarding.updatingOpencode") - } - if (state.job?.kind === "probe-opencode") { - return distro - ? language.t("wsl.onboarding.checkingOpencodeIn", { distro }) - : language.t("wsl.onboarding.checkingOpencode") - } - if (opencodeCheck()?.error) return opencodeCheck()!.error - if (opencodeCheck()?.matchesDesktop === false) { - return distro - ? language.t("wsl.onboarding.updateOpencodeIn", { distro }) - : language.t("wsl.onboarding.updateOpencode") - } - if (opencodeReady()) { - return distro - ? language.t("wsl.onboarding.opencodeReadyIn", { distro }) - : language.t("wsl.onboarding.opencodeReady") - } - return distro - ? language.t("wsl.onboarding.installOpencodeIn", { distro }) - : language.t("wsl.onboarding.chooseDistroFirst") - }) + const openCatalog = () => { + const first = model().installableDistros[0] + setStore({ + view: "catalog", + catalogSearch: "", + catalogTarget: first?.name ?? null, + }) + } const run = async (action: () => Promise) => { try { @@ -235,26 +302,43 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) { } } - const runSelectedDistro = (action: (distro: string) => Promise) => { - const distro = selectedDistro() - if (!distro) return - void run(() => action(distro)) + const refreshDistros = () => { + void run(async () => { + probes.resetProbeFailure() + await api.refreshDistros() + }) + } + + const installDistro = (name: string) => { + void run(async () => { + probes.resetProbeFailure() + await api.installDistro(name) + setStore("view", "main") + }) } - const selectDistro = (name: string) => { - setStore("selectedDistro", name) - setStore("step", undefined) + const installCatalogDistro = () => { + if (model().installingCatalogDistro) return + const name = model().catalogTarget + if (!name) return + installDistro(name) } - const openOpencodeStep = () => { - const distro = selectedDistro() - if (!distro) return - void run(() => enterWslOpencodeStep(distro, api.probeOpencode, (step) => setStore("step", step))) + const closeCatalog = () => { + probes.resetProbeFailure() + setStore({ view: "main", catalogSearch: "", catalogTarget: null }) } - const finish = async () => { - const distro = selectedDistro() - if (!distro) return + const runPrimary = async () => { + const button = model().primaryButton + if (button.loading) return + const distro = model().selectedDistro + const action = button.action + if (!distro || !action) return + if (action === "install-opencode") { + await run(() => api.installOpencode(distro)) + return + } setStore("adding", true) try { await api.addServer(distro) @@ -270,346 +354,103 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) { } } - const steps = createMemo(() => { - const active = activeStep() - const activeIndex = STEPS.indexOf(active) - const recommendedIndex = STEPS.indexOf(recommendedStep()) - return STEPS.map((step) => { - const index = STEPS.indexOf(step) - return { - step, - title: - step === "wsl" - ? language.t("wsl.server.label") - : step === "distro" - ? language.t("wsl.onboarding.step.distro") - : language.t("wsl.onboarding.step.opencode"), - state: - active === step - ? "current" - : step === "wsl" - ? wslReady() - ? "done" - : "warning" - : step === "distro" - ? distroReady() - ? "done" - : index > activeIndex - ? "locked" - : "warning" - : opencodeCheck()?.matchesDesktop === false - ? "warning" - : opencodeReady() - ? "done" - : index > activeIndex - ? "locked" - : "warning", - locked: index > recommendedIndex, - } - }) - }) - const loadError = createMemo(() => { + const loadError = () => { const error = wslServers.error if (!error) return language.t("wsl.onboarding.loadFailed") return error instanceof Error ? error.message : String(error) - }) - - return ( -
- {language.t("wsl.onboarding.loading")}
} - > - {loadError()}
} - > -
- - {(item) => ( - - )} - -
- - - -
-
-
{language.t("wsl.server.label")}
- - - -
-
{wslMessage()}
- -
-
- {language.t("wsl.onboarding.windowsRestartRequired")} -
-
-
-
- -
-
-
- - -
-
-
{language.t("wsl.onboarding.step.distro")}
- - - -
-
{distroMessage()}
- -
- 0} - fallback={ -
- {visibleInstalledDistros().length - ? language.t("wsl.onboarding.allDistrosAdded") - : current()?.runtime?.available - ? language.t("wsl.onboarding.noDistros") - : language.t("wsl.onboarding.checkingDistros")} -
- } - > - - {(item) => ( - - )} - -
-
- - 0}> -
-
-
{language.t("wsl.onboarding.install")}
-
- - - - -
-
-
- - {(item) => { - const selected = () => installTarget()?.name === item.name - return ( - - ) - }} - -
-
-
- - -
- -
- {language.t("wsl.onboarding.wsl2Required")} -
-
- - {(message) =>
{message()}
} -
- -
- {language.t("wsl.onboarding.toolsRequired")} -
-
-
-
- -
- - -
+ } -
- -
-
-
+ return { + wslServers, + model, + loadError, + runtimeError: () => current()?.runtime?.error ?? null, + view: () => store.view, + catalogSearch: () => store.catalogSearch, + adding: () => store.adding, + setCatalogSearch: (value: string) => setStore("catalogSearch", value), + setCatalogTarget: (value: string) => setStore("catalogTarget", value), + setSelectedDistro: (value: string) => setStore("selectedDistro", value), + openCatalog, + closeCatalog, + refreshDistros, + installCatalogDistro, + installWsl: () => void run(() => api.installWsl()), + runPrimary: () => void runPrimary(), + close: () => dialog.close(), + } +} - -
-
-
{language.t("wsl.onboarding.step.opencode")}
-
- - - - - - -
-
-
{opencodeMessage()}
- - {(check) => ( -
-
- {language.t("wsl.onboarding.path", { - path: check().resolvedPath ?? language.t("wsl.onboarding.notFound"), - })} -
-
- {language.t("wsl.onboarding.version", { - version: check().version ?? language.t("wsl.onboarding.unknown"), - })} - - {(expected) => ( - {` · ${language.t("wsl.onboarding.desktopVersion", { version: expected() })}`} - )} - -
-
- {language.t("wsl.onboarding.versionMismatch")} -
-
- )} -
-
-
-
+function DialogWslSetup(props: { + state: string + error: string | null + installable: boolean + busy: boolean + onInstall: () => void +}) { + const language = useLanguage() + const dialog = useDialog() + const title = () => + props.state === "pendingRestart" + ? language.t("wsl.onboarding.restartRequired") + : props.installable + ? language.t("wsl.onboarding.wslNotInstalled.title") + : language.t("wsl.onboarding.wslUnavailable.title") + const description = () => { + if (props.state === "pendingRestart") return language.t("wsl.onboarding.windowsRestartRequired") + if (!props.installable) return language.t("wsl.onboarding.wslUnavailable.description") + return language.t("wsl.onboarding.wslNotInstalled.description") + } - -
- - -
+ return ( + +
+
+ +

{title()}

+

{description()}

+ +

{props.error}

+
+ + + {language.t("wsl.onboarding.installWsl")} + - -
+ + dialog.close()}> + {language.t("common.close")} + + +
+ ) } diff --git a/packages/app/src/wsl/dialog-add-wsl-server.css b/packages/app/src/wsl/dialog-add-wsl-server.css new file mode 100644 index 000000000000..19cb42613aed --- /dev/null +++ b/packages/app/src/wsl/dialog-add-wsl-server.css @@ -0,0 +1,392 @@ +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-container"] { + width: 480px; + max-width: calc(100vw - 32px); + height: auto; + align-items: stretch; +} + +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-content"] { + align-items: stretch; + width: 100%; + overflow: hidden; +} + +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-header"] { + align-items: center; + padding: 24px 16px 0; +} + +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-body"] { + display: flex; + width: 100%; + min-width: 0; + flex-direction: column; + align-items: stretch; + padding: 0; + gap: 0; +} + +.settings-v2-wsl-dialog-body { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + min-width: 0; + align-self: stretch; + box-sizing: border-box; + padding: 16px 16px 1px; +} + +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-footer"] { + padding: 0 16px 24px; + margin-top: 8px; +} + +[data-component="dialog-v2"].settings-v2-wsl-not-installed-dialog [data-slot="dialog-container"] { + width: 480px; + max-width: calc(100vw - 32px); + min-height: 264px; + height: auto; + background: var(--v2-background-bg-layer-01); + box-shadow: var(--v2-elevation-overlay); + border-radius: 10px; +} + +[data-component="dialog-v2"].settings-v2-wsl-not-installed-dialog [data-slot="dialog-content"] { + align-items: stretch; + min-height: 264px; + border-radius: 10px; + overflow: hidden; +} + +[data-component="dialog-v2"].settings-v2-wsl-not-installed-dialog [data-slot="dialog-body"] { + display: flex; + flex: 1; + flex-direction: column; + align-items: stretch; + justify-content: center; + padding: 0; +} + +.settings-v2-wsl-not-installed-content { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 56px 0; + gap: 24px; + text-align: center; +} + +.settings-v2-wsl-not-installed-message { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + gap: 8px; +} + +.settings-v2-wsl-not-installed-icon { + flex: none; + flex-shrink: 0; +} + +.settings-v2-wsl-not-installed-title { + display: flex; + flex: none; + align-items: center; + justify-content: center; + margin: 0; + font-style: normal; + font-weight: 530; + font-size: 15px; + line-height: 20px; + letter-spacing: -0.13px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-base); +} + +.settings-v2-wsl-not-installed-description { + display: flex; + flex: none; + align-items: center; + justify-content: center; + width: 100%; + max-width: 300px; + margin: 0; + font-style: normal; + font-weight: 440; + font-size: 13px; + line-height: 20px; + text-align: center; + letter-spacing: -0.04px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-muted); +} + +.settings-v2-wsl-unavailable-error { + width: 100%; + max-width: 360px; + margin: 4px 0 0; + font-style: normal; + font-weight: 440; + font-size: 12px; + line-height: 18px; + text-align: center; + letter-spacing: -0.04px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-faint); + overflow-wrap: anywhere; +} + +.settings-v2-wsl-section-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + flex: none; + align-self: stretch; + gap: 12px; + height: 24px; + padding: 0 0 0 9px; +} + +.settings-v2-wsl-section-title { + flex: none; + user-select: none; + font-style: normal; + font-weight: 440; + font-size: 13px; + line-height: 16px; + letter-spacing: -0.04px; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-base); +} + +.settings-v2-wsl-distro-list { + display: flex; + flex-direction: column; + align-items: flex-start; + flex: none; + align-self: stretch; + width: 100%; + height: fit-content; + max-height: 220px; + padding: 0; + border-radius: 8px; + background: var(--v2-background-bg-base); + box-shadow: var(--v2-elevation-raised); + overflow-x: hidden; + overflow-y: auto; +} + +.settings-v2-wsl-distro-group { + width: 100%; +} + +.settings-v2-wsl-distro-group [data-slot="radio-v2-items"] { + width: 100%; + gap: 0; +} + +.settings-v2-wsl-distro-row { + box-sizing: border-box; + width: 100%; + min-height: 62px; + padding: 14px 14px 14px 16px; + border-bottom: 0.5px solid var(--v2-border-border-base); +} + +.settings-v2-wsl-distro-row:last-child { + border-bottom: none; +} + +.settings-v2-wsl-distro-row [data-slot="radio-v2-item-label"] { + position: relative; + display: flex; + flex: 1; + width: 100%; + min-width: 0; + align-self: stretch; +} + +.settings-v2-wsl-distro-row [data-slot="radio-v2-item-label"]::before { + content: ""; + position: absolute; + inset: -14px -14px -14px -44px; +} + +.settings-v2-wsl-distro-row [data-slot="radio-v2-item-text"] { + width: 100%; +} + +.settings-v2-wsl-distro-label { + font-style: normal; + font-weight: 440; + font-size: 13px; + line-height: 16px; + letter-spacing: -0.04px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; + color: inherit; +} + +.settings-v2-wsl-distro-status { + flex: none; + font-style: normal; + font-weight: 440; + font-size: 12px; + line-height: 12px; + letter-spacing: 0.05px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; +} + +.settings-v2-wsl-distro-status[data-tone="success"] { + color: var(--v2-state-fg-success); +} + +.settings-v2-wsl-distro-status[data-tone="warning"] { + color: var(--v2-state-fg-warning); +} + +.settings-v2-wsl-distro-status[data-tone="muted"] { + color: var(--v2-text-text-muted); +} + +.settings-v2-wsl-distro-row:where([data-disabled]) .settings-v2-wsl-distro-status { + color: inherit; +} + +.settings-v2-wsl-distro-empty { + padding: 16px; + font-size: 13px; + font-weight: 440; + line-height: 1.4; + color: var(--v2-text-text-muted); +} + +.settings-v2-wsl-catalog-card { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: flex-start; + flex: none; + align-self: stretch; + gap: 12px; + width: 100%; + min-height: 64px; + padding: 14px 14px 16px 16px; + border: 0.5px solid var(--v2-border-border-base); + border-radius: 8px; + background: transparent; + cursor: pointer; + text-align: left; +} + +.settings-v2-wsl-catalog-card:focus-visible { + outline: 2px solid var(--v2-border-border-focus); + outline-offset: 2px; +} + +.settings-v2-wsl-catalog-card:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.settings-v2-wsl-catalog-icon { + display: flex; + flex: none; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: var(--v2-icon-icon-base); +} + +.settings-v2-wsl-catalog-copy { + display: flex; + min-width: 0; + flex: 1; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 6px; +} + +.settings-v2-wsl-catalog-title { + user-select: none; + font-style: normal; + font-weight: 440; + font-size: 13px; + line-height: 16px; + letter-spacing: -0.04px; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-base); +} + +.settings-v2-wsl-catalog-description { + user-select: none; + font-style: normal; + font-weight: 440; + font-size: 12px; + line-height: 12px; + letter-spacing: 0.05px; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-muted); +} + +.settings-v2-wsl-catalog-chevron { + display: flex; + flex: none; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: var(--v2-icon-icon-muted); +} + +.settings-v2-wsl-catalog-list { + display: flex; + flex-direction: column; + align-items: flex-start; + flex: none; + align-self: stretch; + width: 100%; + height: fit-content; + max-height: 220px; + padding: 0; + border-radius: 8px; + box-shadow: inset 0 0 0 0.5px var(--v2-border-border-strong); + overflow-x: hidden; + overflow-y: auto; +} + +.settings-v2-wsl-catalog-picker { + gap: 12px; +} + +.settings-v2-wsl-catalog-search { + width: 100%; +} + +.settings-v2-wsl-catalog-row { + min-height: 48px; +} + +.settings-v2-wsl-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 48px 24px; + font-size: 13px; + font-weight: 440; + line-height: 1; + color: var(--v2-text-text-muted); + text-align: center; +} diff --git a/packages/app/src/wsl/settings-model.test.ts b/packages/app/src/wsl/settings-model.test.ts index 77a5f55d6410..812852ff10c0 100644 --- a/packages/app/src/wsl/settings-model.test.ts +++ b/packages/app/src/wsl/settings-model.test.ts @@ -1,5 +1,31 @@ import { describe, expect, test } from "bun:test" -import { enterWslOpencodeStep, wslOpencodeAction, wslRuntimeRetryable } from "./settings-model" +import { + addableProbePlan, + addServerProbePlan, + addServerViewModel, + autoProbePlan, + createProbeFailureGate, + runAddableProbePlan, + wslOpencodeAction, + wslRuntimeRetryable, +} from "./settings-model" +import type { WslServersState } from "./types" + +const readyWslState = readyState() + +function readyState(input: Partial = {}): WslServersState { + return { + runtime: { available: true, version: "2.4.13.0", error: null }, + installed: [], + online: [], + distroProbes: {}, + opencodeChecks: {}, + pendingRestart: false, + servers: [], + job: null, + ...input, + } +} describe("WSL server settings presentation", () => { test("retries only settled unsuccessful runtimes", () => { @@ -45,13 +71,161 @@ describe("WSL server settings presentation", () => { ).toBeUndefined() }) - test("probes the selected distro before entering the OpenCode step", async () => { - const calls: string[] = [] - await enterWslOpencodeStep( - "Debian", - async (distro) => calls.push(distro), - (step) => calls.push(step), - ) - expect(calls).toEqual(["Debian", "opencode"]) + test("plans addable distro probes with the selected distro first", () => { + const plan = addableProbePlan({ + state: readyWslState, + view: "main", + adding: false, + selectedDistro: "Ubuntu", + addableInstalledDistros: [ + { name: "Debian", version: 2, isDefault: true }, + { name: "Ubuntu", version: 2, isDefault: false }, + ], + }) + + expect(plan?.key).toBe("distro:Ubuntu|distro:Debian") + expect(plan?.distros).toEqual(["Ubuntu", "Debian"]) + }) + + test("plans bootstrap probes for missing runtime and initial distro lists", () => { + expect(autoProbePlan({ state: undefined, busy: false })).toBeUndefined() + expect(autoProbePlan({ state: { ...readyWslState, runtime: null }, busy: false })).toEqual({ + key: "runtime", + action: "probeRuntime", + }) + expect(autoProbePlan({ state: readyWslState, busy: false })).toEqual({ + key: "distros", + action: "refreshDistros", + }) + }) + + test("uses one command plan for bootstrap before addable distro probing", () => { + expect( + addServerProbePlan({ + state: { ...readyWslState, runtime: null }, + view: "main", + adding: false, + busy: false, + selectedDistro: null, + addableInstalledDistros: [{ name: "Debian", version: 2, isDefault: true }], + }), + ).toEqual({ kind: "auto", key: "auto:runtime", plan: { key: "runtime", action: "probeRuntime" } }) + + expect( + addServerProbePlan({ + state: { + ...readyWslState, + installed: [{ name: "Debian", version: 2, isDefault: true }], + online: [{ name: "Ubuntu", label: "Ubuntu" }], + }, + view: "main", + adding: false, + busy: false, + selectedDistro: "Debian", + addableInstalledDistros: [{ name: "Debian", version: 2, isDefault: true }], + }), + ).toEqual({ kind: "addable", key: "addable:distro:Debian", plan: { key: "distro:Debian", distros: ["Debian"] } }) + }) + + test("does not accept the same failed probe command until reset", () => { + const gate = createProbeFailureGate() + + expect(gate.accepts("addable:distro:Debian")).toBe(true) + gate.settle("addable:distro:Debian", new Error("wsl failed")) + expect(gate.accepts("addable:distro:Debian")).toBe(false) + expect(gate.accepts("addable:distro:Ubuntu")).toBe(true) + gate.reset() + expect(gate.accepts("addable:distro:Debian")).toBe(true) + }) + + test("keeps default distro selection stable while probes resolve", () => { + const model = addServerViewModel({ + state: { + ...readyWslState, + installed: [ + { name: "Debian", version: 2, isDefault: true }, + { name: "Ubuntu", version: 2, isDefault: false }, + ], + online: [{ name: "Alpine", label: "Alpine Linux" }], + distroProbes: { + Ubuntu: { name: "Ubuntu", canExecute: true, hasBash: true, hasCurl: true, error: null }, + }, + }, + view: "main", + selectedDistro: null, + catalogSearch: "", + catalogTarget: null, + adding: false, + probingAddable: false, + }) + + expect(model.selectedDistro).toBe("Debian") + }) + + test("keeps the dialog busy across serial addable probe job gaps", () => { + const model = addServerViewModel({ + state: readyState({ + installed: [{ name: "Debian", version: 2, isDefault: true }], + online: [{ name: "Ubuntu", label: "Ubuntu" }], + }), + view: "main", + selectedDistro: null, + catalogSearch: "", + catalogTarget: null, + adding: false, + probingAddable: true, + }) + + expect(model.busy).toBe(true) + }) + + test("does not report ready when OpenCode is present but cannot run", () => { + const model = addServerViewModel({ + state: { + ...readyWslState, + installed: [{ name: "Debian", version: 2, isDefault: true }], + online: [{ name: "Ubuntu", label: "Ubuntu" }], + distroProbes: { + Debian: { name: "Debian", canExecute: true, hasBash: true, hasCurl: true, error: null }, + }, + opencodeChecks: { + Debian: { + distro: "Debian", + resolvedPath: "/home/me/.opencode/bin/opencode", + version: null, + expectedVersion: "1.2.3", + matchesDesktop: null, + error: "opencode is installed but could not run", + }, + }, + }, + view: "main", + selectedDistro: null, + catalogSearch: "", + catalogTarget: null, + adding: false, + probingAddable: false, + }) + + expect(model.distroStatuses.Debian).toEqual({ + label: { key: "wsl.onboarding.installOpencode" }, + tone: "warning", + }) + expect(model.primaryButton.action).toBe("install-opencode") + }) + + test("delegates addable probe plans to one batch command", async () => { + const calls: string[][] = [] + + await runAddableProbePlan({ + plan: { key: "distro:Debian|distro:Ubuntu", distros: ["Debian", "Ubuntu"] }, + api: { + probeAddable: async (distros) => { + calls.push(distros) + }, + }, + }) + + expect(calls).toEqual([["Debian", "Ubuntu"]]) }) }) diff --git a/packages/app/src/wsl/settings-model.ts b/packages/app/src/wsl/settings-model.ts index 1c84856559ab..6d43e0a70ddd 100644 --- a/packages/app/src/wsl/settings-model.ts +++ b/packages/app/src/wsl/settings-model.ts @@ -1,19 +1,343 @@ -import type { WslOpencodeCheck, WslServerRuntime } from "./types" +import fuzzysort from "fuzzysort" +import type { + WslInstalledDistro, + WslOnlineDistro, + WslOpencodeCheck, + WslServersPlatform, + WslServerRuntime, + WslServersState, +} from "./types" -export const wslRuntimeRetryable = (runtime: WslServerRuntime) => - runtime.kind === "failed" || runtime.kind === "stopped" +export type AddServerText = { + key: string + params?: Record +} -export async function enterWslOpencodeStep( - distro: string, - probe: (distro: string) => Promise, - select: (step: "opencode") => void, -) { - await probe(distro) - select("opencode") +export type DistroStatusTone = "success" | "warning" | "muted" + +export type DistroStatus = { + label: AddServerText + tone: DistroStatusTone +} + +export type AddServerPrimaryButton = { + variant: "neutral" | "contrast" + label: AddServerText + disabled: boolean + action: "install-opencode" | "add" | null + loading: boolean + width: string | null } +export type AddServerRuntimeState = "loading" | "pendingRestart" | "checking" | "unavailable" | "ready" + +export type AddableProbePlan = { + key: string + distros: string[] +} + +export type AutoProbePlan = + | { key: "runtime"; action: "probeRuntime" } + | { key: "distros"; action: "refreshDistros" } + +export type AddServerProbePlan = + | { kind: "auto"; key: string; plan: AutoProbePlan } + | { kind: "addable"; key: string; plan: AddableProbePlan } + +export type WslAddServerView = "main" | "catalog" + +function isHiddenDistro(name: string) { + return /^docker-desktop(?:-data)?$/i.test(name) +} + +export const wslRuntimeRetryable = (runtime: WslServerRuntime) => + runtime.kind === "failed" || runtime.kind === "stopped" + export function wslOpencodeAction(check?: WslOpencodeCheck) { if (!check) return if (!check.resolvedPath) return "Install OpenCode" if (check.matchesDesktop === false) return "Update OpenCode" } + +export function wslDistroReady(state: WslServersState | undefined, name: string) { + const installed = state?.installed.find((item) => item.name === name) + const probe = state?.distroProbes[name] + if (!probe || !installed) return false + if (installed.version === 1) return false + return probe.canExecute && probe.hasBash && probe.hasCurl +} + +export function addServerViewModel(input: { + state: WslServersState | undefined + view: WslAddServerView + selectedDistro: string | null + catalogSearch: string + catalogTarget: string | null + adding: boolean + probingAddable: boolean +}) { + const state = input.state + const visibleInstalledDistros = (state?.installed ?? []).filter((item) => !isHiddenDistro(item.name)) + const visibleOnlineDistros = (state?.online ?? []).filter((item) => !isHiddenDistro(item.name)) + const existingServerDistros = new Set((state?.servers ?? []).map((item) => item.config.distro)) + const addableInstalledDistros = visibleInstalledDistros.filter((item) => !existingServerDistros.has(item.name)) + const selectedDistro = addServerSelectedDistro(input.selectedDistro, visibleInstalledDistros, addableInstalledDistros) + const opencodeCheck = selectedDistro ? (state?.opencodeChecks[selectedDistro] ?? null) : null + const installableDistros = addServerInstallableDistros(visibleInstalledDistros, visibleOnlineDistros) + const filteredInstallableDistros = addServerFilteredInstallableDistros(installableDistros, input.catalogSearch) + const catalogTarget = addServerCatalogTarget(input.catalogTarget, filteredInstallableDistros) + const busy = !!state?.job || input.adding || input.probingAddable + + return { + busy, + runtimeState: addServerRuntimeState(state), + visibleInstalledDistros, + visibleOnlineDistros, + addableInstalledDistros, + selectedDistro, + opencodeCheck, + wslReady: !!state?.runtime?.available && !state?.pendingRestart, + distroStatuses: Object.fromEntries( + addableInstalledDistros.flatMap((item) => { + const status = addServerDistroStatus({ state, name: item.name, probingAddable: input.probingAddable }) + if (!status) return [] + return [[item.name, status]] + }), + ) as Record, + primaryButton: addServerPrimaryButton({ + state, + selectedDistro, + opencodeCheck, + adding: input.adding, + probingAddable: input.probingAddable, + }), + installableDistros, + filteredInstallableDistros, + catalogTarget, + installingCatalogDistro: state?.job?.kind === "install-distro", + } +} + +function addServerSelectedDistro( + selected: string | null, + visibleInstalledDistros: WslInstalledDistro[], + addableInstalledDistros: WslInstalledDistro[], +) { + if (selected && addableInstalledDistros.some((item) => item.name === selected && item.version !== 1)) return selected + const defaultDistro = visibleInstalledDistros.find((item) => item.isDefault) + if ( + defaultDistro && + defaultDistro.version !== 1 && + addableInstalledDistros.some((item) => item.name === defaultDistro.name) + ) { + return defaultDistro.name + } + return addableInstalledDistros.find((item) => item.version !== 1)?.name ?? null +} + +function addServerRuntimeState(state: WslServersState | undefined): AddServerRuntimeState { + if (!state) return "loading" + if (state.pendingRestart) return "pendingRestart" + if (!state.runtime) return "checking" + if (!state.runtime.available) return "unavailable" + return "ready" +} + +function addServerDistroStatus(input: { + state: WslServersState | undefined + name: string + probingAddable: boolean +}): DistroStatus | undefined { + const installed = input.state?.installed.find((item) => item.name === input.name) + if (installed?.version === 1) return { label: { key: "wsl.onboarding.distroStatus.unsupported" }, tone: "muted" } + const job = input.state?.job + const probe = input.state?.distroProbes[input.name] + if (!probe) { + if (input.probingAddable || (job?.kind === "probe-addable" && job.distros.includes(input.name))) { + return checkingStatus() + } + return + } + if (!probe.canExecute) { + if (!installed) { + return { label: { key: "wsl.onboarding.distroNotInstalled", params: { distro: input.name } }, tone: "warning" } + } + return { label: { key: "wsl.onboarding.openDistroOnce", params: { distro: input.name } }, tone: "warning" } + } + if (!probe.hasBash || !probe.hasCurl) { + return { label: { key: "wsl.onboarding.distroStatus.missingTools" }, tone: "warning" } + } + const check = input.state?.opencodeChecks[input.name] + if (!check) { + if (input.probingAddable || (job?.kind === "probe-addable" && job.distros.includes(input.name))) { + return checkingStatus() + } + return + } + if (check.matchesDesktop === false) return { label: { key: "wsl.onboarding.updateOpencode" }, tone: "warning" } + if (!check.resolvedPath) return { label: { key: "wsl.onboarding.distroStatus.opencodeMissing" }, tone: "warning" } + if (check.error) return { label: { key: "wsl.onboarding.installOpencode" }, tone: "warning" } + return { label: { key: "wsl.onboarding.distroStatus.ready" }, tone: "success" } +} + +function checkingStatus(): DistroStatus { + return { label: { key: "wsl.onboarding.distroStatus.checking" }, tone: "muted" } +} + +function addServerPrimaryButton(input: { + state: WslServersState | undefined + selectedDistro: string | null + opencodeCheck: WslOpencodeCheck | null + adding: boolean + probingAddable: boolean +}): AddServerPrimaryButton { + const ready = !!input.selectedDistro && wslDistroReady(input.state, input.selectedDistro) + const probingSelected = input.probingAddable && !addServerSelectedDistroSettled(input.state, input.selectedDistro) + const probingOpencode = + probingSelected || + (ready && + (!input.opencodeCheck || + (!!input.selectedDistro && + input.state?.job?.kind === "probe-addable" && + input.state.job.distros.includes(input.selectedDistro)))) + const installingOpencode = + input.state?.job?.kind === "install-opencode" && input.state.job.distro === input.selectedDistro + if (!ready || probingOpencode) { + return { + variant: "contrast", + label: probingSelected ? { key: "wsl.onboarding.distroStatus.checking" } : { key: "wsl.server.add" }, + disabled: true, + action: null, + loading: probingSelected, + width: null, + } + } + if (!addServerOpencodeReady(input.opencodeCheck)) { + const update = !!input.opencodeCheck?.resolvedPath && input.opencodeCheck.matchesDesktop === false + return { + variant: "neutral", + label: installingOpencode + ? { key: "wsl.onboarding.updatingOpencode" } + : update + ? { key: "wsl.onboarding.updateOpencode" } + : { key: "wsl.onboarding.installOpencode" }, + disabled: !!input.state?.job || input.adding, + action: "install-opencode", + loading: installingOpencode, + width: update ? "138px" : "129px", + } + } + return { + variant: "contrast", + label: input.adding ? { key: "wsl.onboarding.adding" } : { key: "wsl.server.add" }, + disabled: input.adding || !!input.state?.job, + action: "add", + loading: input.adding, + width: null, + } +} + +function addServerOpencodeReady(check: WslOpencodeCheck | null) { + return !!check?.resolvedPath && check.matchesDesktop !== false && !check.error +} + +function addServerSelectedDistroSettled(state: WslServersState | undefined, selectedDistro: string | null) { + if (!selectedDistro) return false + const installed = state?.installed.find((item) => item.name === selectedDistro) + if (installed?.version === 1) return false + if (!state?.distroProbes[selectedDistro]) return false + if (!wslDistroReady(state, selectedDistro)) return true + return !!state.opencodeChecks[selectedDistro] +} + +function addServerInstallableDistros(installedDistros: WslInstalledDistro[], onlineDistros: WslOnlineDistro[]) { + const installed = new Set(installedDistros.map((item) => item.name)) + const hasVersionedUbuntu = onlineDistros.some((item) => /^Ubuntu-\d/.test(item.name)) + return onlineDistros + .filter((item) => !installed.has(item.name)) + .filter((item) => item.name !== "Ubuntu" || !hasVersionedUbuntu) +} + +function addServerFilteredInstallableDistros(installableDistros: WslOnlineDistro[], search: string) { + const query = search.trim() + if (!query) return installableDistros + return fuzzysort.go(query, installableDistros, { keys: ["label", "name"] }).map((item) => item.obj) +} + +function addServerCatalogTarget(target: string | null, distros: WslOnlineDistro[]) { + if (target && distros.some((item) => item.name === target)) return target + return distros[0]?.name ?? null +} + +export function addableProbePlan(input: { + state: WslServersState | undefined + view: WslAddServerView + adding: boolean + selectedDistro: string | null + addableInstalledDistros: WslInstalledDistro[] +}) { + const state = input.state + if (!state?.runtime?.available || state.pendingRestart || input.view !== "main" || input.adding) return + if (state.job) return + const ordered = input.selectedDistro + ? [ + ...input.addableInstalledDistros.filter((item) => item.name === input.selectedDistro), + ...input.addableInstalledDistros.filter((item) => item.name !== input.selectedDistro), + ] + : input.addableInstalledDistros + const pending = ordered.flatMap((item) => { + if (item.version === 1) return [] + if (!state.distroProbes[item.name]) return [`distro:${item.name}`] + if (wslDistroReady(state, item.name) && !state.opencodeChecks[item.name]) return [`opencode:${item.name}`] + return [] + }) + if (!pending.length) return + return { + key: pending.join("|"), + distros: ordered.filter((item) => item.version !== 1).map((item) => item.name), + } satisfies AddableProbePlan +} + +export function autoProbePlan(input: { state: WslServersState | undefined; busy: boolean }) { + if (!input.state || input.busy || input.state.pendingRestart) return + if (!input.state.runtime) return { key: "runtime", action: "probeRuntime" } satisfies AutoProbePlan + if (!input.state.runtime.available) return + if (input.state.installed.length || input.state.online.length) return + return { key: "distros", action: "refreshDistros" } satisfies AutoProbePlan +} + +export function addServerProbePlan(input: { + state: WslServersState | undefined + view: WslAddServerView + adding: boolean + busy: boolean + selectedDistro: string | null + addableInstalledDistros: WslInstalledDistro[] +}) { + const auto = autoProbePlan({ state: input.state, busy: input.busy }) + if (auto) return { kind: "auto", key: `auto:${auto.key}`, plan: auto } satisfies AddServerProbePlan + const addable = addableProbePlan(input) + if (addable) return { kind: "addable", key: `addable:${addable.key}`, plan: addable } satisfies AddServerProbePlan +} + +export function createProbeFailureGate() { + let failed: string | undefined + return { + accepts(key: string) { + return key !== failed + }, + settle(key: string, error?: unknown) { + if (error) failed = key + }, + reset() { + failed = undefined + }, + } +} + +export async function runAddableProbePlan(input: { + plan: AddableProbePlan + api: Pick +}) { + await input.api.probeAddable(input.plan.distros) +} diff --git a/packages/app/src/wsl/settings.tsx b/packages/app/src/wsl/settings.tsx index eb67699bbfa1..82616c6dcf7a 100644 --- a/packages/app/src/wsl/settings.tsx +++ b/packages/app/src/wsl/settings.tsx @@ -1,8 +1,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { Tag } from "@opencode-ai/ui/v2/badge-v2" import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" -import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2" -import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" @@ -30,17 +28,7 @@ export function AddServerMenu(props: { onAddServer: () => void }) { const dialog = useDialog() const language = useLanguage() const openAddWsl = () => { - dialog.push(() => ( - - - {language.t("wsl.server.add")} - - - - - - - )) + dialog.push(() => ) } return ( installWsl(): Promise installDistro(name: string): Promise - probeDistro(name: string): Promise - probeOpencode(name: string): Promise + probeAddable(distros: string[]): Promise installOpencode(name: string): Promise openTerminal(name: string): Promise addServer(distro: string): Promise diff --git a/packages/desktop/src/main/wsl/ipc.ts b/packages/desktop/src/main/wsl/ipc.ts index ed98004beeb3..c3a2271bf6c3 100644 --- a/packages/desktop/src/main/wsl/ipc.ts +++ b/packages/desktop/src/main/wsl/ipc.ts @@ -1,7 +1,7 @@ import { app, ipcMain } from "electron" import type { IpcMainInvokeEvent } from "electron" import type { WslServersController } from "./servers" -import { requireWslIpcString } from "./policy" +import { requireWslIpcString, requireWslIpcStrings } from "./policy" import type { WslServersState } from "../../preload/types" export function registerWslIpcHandlers(controller: WslServersController) { @@ -46,11 +46,8 @@ export function registerWslIpcHandlers(controller: WslServersController) { ipcMain.handle("wsl-servers-install-distro", (_event: IpcMainInvokeEvent, name: string) => controller.installDistro(requireWslIpcString("distro", name)), ) - ipcMain.handle("wsl-servers-probe-distro", (_event: IpcMainInvokeEvent, name: string) => - controller.probeDistro(requireWslIpcString("distro", name)), - ) - ipcMain.handle("wsl-servers-probe-opencode", (_event: IpcMainInvokeEvent, name: string) => - controller.probeOpencode(requireWslIpcString("distro", name)), + ipcMain.handle("wsl-servers-probe-addable", (_event: IpcMainInvokeEvent, distros: string[]) => + controller.probeAddable(requireWslIpcStrings("distro", distros)), ) ipcMain.handle("wsl-servers-install-opencode", (_event: IpcMainInvokeEvent, name: string) => controller.installOpencode(requireWslIpcString("distro", name)), @@ -97,8 +94,7 @@ function registerUnavailableWslIpcHandlers() { ipcMain.handle("wsl-servers-refresh-distros", unavailable) ipcMain.handle("wsl-servers-install-wsl", unavailable) ipcMain.handle("wsl-servers-install-distro", unavailable) - ipcMain.handle("wsl-servers-probe-distro", unavailable) - ipcMain.handle("wsl-servers-probe-opencode", unavailable) + ipcMain.handle("wsl-servers-probe-addable", unavailable) ipcMain.handle("wsl-servers-install-opencode", unavailable) ipcMain.handle("wsl-servers-open-terminal", unavailable) ipcMain.handle("wsl-servers-add", unavailable) diff --git a/packages/desktop/src/main/wsl/policy.ts b/packages/desktop/src/main/wsl/policy.ts index 0047ce3b3f90..4ceee26c6b1b 100644 --- a/packages/desktop/src/main/wsl/policy.ts +++ b/packages/desktop/src/main/wsl/policy.ts @@ -24,3 +24,10 @@ export function requireWslIpcString(name: string, value: unknown) { if (typeof value === "string" && value.length > 0) return value throw new Error(`Invalid ${name}`) } + +export function requireWslIpcStrings(name: string, value: unknown) { + if (!Array.isArray(value)) throw new Error(`Invalid ${name}`) + const values = value.map((item) => requireWslIpcString(name, item)) + if (values.length > 0) return values + throw new Error(`Invalid ${name}`) +} diff --git a/packages/desktop/src/main/wsl/servers.test.ts b/packages/desktop/src/main/wsl/servers.test.ts index fcca712a0471..94cff52bfd3a 100644 --- a/packages/desktop/src/main/wsl/servers.test.ts +++ b/packages/desktop/src/main/wsl/servers.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { clearWslDistroState, requireWslIpcString, wslServerIdToRestart, wslTerminalArgs } from "./policy" +import { clearWslDistroState, requireWslIpcString, requireWslIpcStrings, wslServerIdToRestart, wslTerminalArgs } from "./policy" import { expectOpencodeVersion, pendingRestartAfterWslInstall, @@ -87,8 +87,10 @@ test("stops health polling when sidecar startup settles", async () => { test("validates WSL IPC identifiers at the module boundary", () => { expect(requireWslIpcString("distro", "Debian")).toBe("Debian") + expect(requireWslIpcStrings("distro", ["Debian", "Ubuntu"])).toEqual(["Debian", "Ubuntu"]) expect(() => requireWslIpcString("distro", "")).toThrow("Invalid distro") expect(() => requireWslIpcString("server id", undefined)).toThrow("Invalid server id") + expect(() => requireWslIpcStrings("distro", [])).toThrow("Invalid distro") }) test("derives a required Windows restart from the post-install runtime probe", () => { @@ -142,6 +144,70 @@ test("ignores stale startup OpenCode checks after removing a WSL server", async expect(controller.getState().opencodeChecks).toEqual({}) }) +test("probes addable distros in parallel before checking OpenCode", async () => { + persistedServers = [] + const started: string[] = [] + const release = new Map void>() + const opencode: string[] = [] + const controller = createWslServersController( + "1.16.2", + async () => new Promise(() => undefined), + { + ...testControllerOptions(), + probeDistro: async (distro) => { + started.push(distro) + await new Promise((resolve) => release.set(distro, resolve)) + return { name: distro, canExecute: true, hasBash: true, hasCurl: true, error: null } + }, + resolveOpencode: async (distro) => { + opencode.push(distro) + return "/home/me/.opencode/bin/opencode" + }, + }, + ) + + const task = controller.probeAddable(["Debian", "Ubuntu"]) + await waitFor(() => started.length === 2) + expect(started).toEqual(["Debian", "Ubuntu"]) + expect(opencode).toEqual([]) + release.get("Debian")?.() + release.get("Ubuntu")?.() + await task + + expect(Object.keys(controller.getState().distroProbes)).toEqual(["Debian", "Ubuntu"]) + expect(opencode).toEqual(["Debian", "Ubuntu"]) + expect(Object.keys(controller.getState().opencodeChecks)).toEqual(["Debian", "Ubuntu"]) +}) + +test("does not check OpenCode in addable distros that cannot execute commands", async () => { + persistedServers = [] + const opencode: string[] = [] + const controller = createWslServersController( + "1.16.2", + async () => new Promise(() => undefined), + { + ...testControllerOptions(), + probeDistro: async (distro) => ({ + name: distro, + canExecute: distro === "Debian", + hasBash: distro === "Debian", + hasCurl: distro === "Debian", + error: distro === "Debian" ? null : "Open Ubuntu once to finish setup", + }), + resolveOpencode: async (distro) => { + opencode.push(distro) + return "/home/me/.opencode/bin/opencode" + }, + }, + ) + + await controller.probeAddable(["Debian", "Ubuntu"]) + + expect(Object.keys(controller.getState().distroProbes)).toEqual(["Debian", "Ubuntu"]) + expect(opencode).toEqual(["Debian"]) + expect(Object.keys(controller.getState().opencodeChecks)).toEqual(["Debian"]) +}) + async function waitFor(check: () => boolean) { for (let attempt = 0; attempt < 20; attempt++) { if (check()) return diff --git a/packages/desktop/src/main/wsl/servers.ts b/packages/desktop/src/main/wsl/servers.ts index b9cd9201faf7..2f5cdb00ff7a 100644 --- a/packages/desktop/src/main/wsl/servers.ts +++ b/packages/desktop/src/main/wsl/servers.ts @@ -47,6 +47,7 @@ type WslServersControllerOptions = { logger?: ControllerLogger readServers?: () => WslServerConfig[] writeServers?: (servers: WslServerConfig[]) => void + probeDistro?: typeof probeWslDistro resolveOpencode?: typeof resolveWslOpencode readCommandVersion?: typeof readWslCommandVersion } @@ -70,6 +71,7 @@ export function createWslServersController( const logger = options?.logger const readServers = options?.readServers ?? readPersistedServers const writeServers = options?.writeServers ?? writePersistedServers + const probeDistro = options?.probeDistro ?? probeWslDistro const emit = () => { for (const listener of listeners) listener({ type: "state", state }) @@ -140,6 +142,28 @@ export function createWslServersController( setOpencodeCheck(distro, await checkOpencode(distro, opts)) } + const probeAddableDistros = async (distros: string[], opts?: { signal?: AbortSignal }) => { + const unique = [...new Set(distros)] + const distroProbes = await Promise.all( + unique + .filter((distro) => !state.distroProbes[distro]) + .map(async (distro) => [distro, await probeDistro(distro, opts)] as const), + ) + if (distroProbes.length) { + setState({ distroProbes: { ...state.distroProbes, ...Object.fromEntries(distroProbes) } }) + } + + const opencodeChecks = await Promise.all( + unique + .filter((distro) => distroProbeReady(state.distroProbes[distro])) + .filter((distro) => !state.opencodeChecks[distro]) + .map(async (distro) => [distro, await checkOpencode(distro, opts)] as const), + ) + if (opencodeChecks.length) { + setState({ opencodeChecks: { ...state.opencodeChecks, ...Object.fromEntries(opencodeChecks) } }) + } + } + const hasServer = (id: string, distro: string) => { return state.servers.some((item) => item.config.id === id && item.config.distro === distro) } @@ -319,7 +343,7 @@ export function createWslServersController( throw new Error(message) } const distros = await refreshDistroLists({ signal: abort.signal }) - const probe = await probeWslDistro(name, { signal: abort.signal }) + const probe = await probeDistro(name, { signal: abort.signal }) setState({ ...distros, distroProbes: { ...state.distroProbes, [name]: probe }, @@ -327,16 +351,10 @@ export function createWslServersController( }) }, - async probeDistro(name: string) { - await runJob({ kind: "probe-distro", distro: name, startedAt: Date.now() }, async (abort) => { - const probe = await probeWslDistro(name, { signal: abort.signal }) - setState({ distroProbes: { ...state.distroProbes, [name]: probe } }) - }) - }, - - async probeOpencode(name: string) { - await runJob({ kind: "probe-opencode", distro: name, startedAt: Date.now() }, async (abort) => { - await refreshOpencodeCheck(name, { signal: abort.signal }) + async probeAddable(distros: string[]) { + if (!distros.length) return + await runJob({ kind: "probe-addable", distros, startedAt: Date.now() }, async (abort) => { + await probeAddableDistros(distros, { signal: abort.signal }) }) }, @@ -480,6 +498,10 @@ function opencodeCheck( } } +function distroProbeReady(probe: WslDistroProbe | undefined) { + return !!probe?.canExecute && probe.hasBash && probe.hasCurl +} + function startupFailure(code: number | null, signal: NodeJS.Signals | null) { return `WSL server exited after startup (code=${code ?? "null"} signal=${signal ?? "null"})` } diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index f08a81c0d1af..2cb8a9ee073c 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -29,8 +29,7 @@ const api: ElectronAPI = { refreshDistros: () => ipcRenderer.invoke("wsl-servers-refresh-distros"), installWsl: () => ipcRenderer.invoke("wsl-servers-install-wsl"), installDistro: (name) => ipcRenderer.invoke("wsl-servers-install-distro", name), - probeDistro: (name) => ipcRenderer.invoke("wsl-servers-probe-distro", name), - probeOpencode: (name) => ipcRenderer.invoke("wsl-servers-probe-opencode", name), + probeAddable: (distros) => ipcRenderer.invoke("wsl-servers-probe-addable", distros), installOpencode: (name) => ipcRenderer.invoke("wsl-servers-install-opencode", name), openTerminal: (name) => ipcRenderer.invoke("wsl-servers-open-terminal", name), addServer: (distro) => ipcRenderer.invoke("wsl-servers-add", distro), diff --git a/packages/ui/src/v2/components/button-v2.css b/packages/ui/src/v2/components/button-v2.css index 4e5edad9edc4..8f7b4756108a 100644 --- a/packages/ui/src/v2/components/button-v2.css +++ b/packages/ui/src/v2/components/button-v2.css @@ -28,7 +28,7 @@ outline: none; } -[data-component="button-v2"]:is(:focus-visible, [data-state="focus"]):not(:disabled) { +[data-component="button-v2"]:is(:focus-visible, [data-state="focus"]):not(:disabled):not([data-variant="loading"]) { outline: 2px solid var(--v2-border-border-focus); outline-offset: 2.5px; } @@ -170,6 +170,15 @@ cursor: not-allowed; } +/* Loading */ +[data-component="button-v2"][data-variant="loading"] { + background: #f2f2f2; + color: var(--v2-text-text-base); + box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.08); + cursor: default; + pointer-events: none; +} + /* Ghost */ [data-component="button-v2"][data-variant="ghost"] { background-color: transparent; diff --git a/packages/ui/src/v2/components/button-v2.stories.tsx b/packages/ui/src/v2/components/button-v2.stories.tsx index d88c88c0d681..bc9b9f05f0ed 100644 --- a/packages/ui/src/v2/components/button-v2.stories.tsx +++ b/packages/ui/src/v2/components/button-v2.stories.tsx @@ -1,11 +1,11 @@ import { ButtonV2 } from "./button-v2" const docs = `### Overview -Button v2 with three visual variants and two sizes. +Button v2 with visual variants and three sizes. ### API -- \`variant\`: "neutral" | "danger" | "contrast" | "ghost" | "ghost-muted". -- \`size\`: "normal" | "large". +- \`variant\`: "neutral" | "danger" | "contrast" | "ghost" | "ghost-muted" | "loading". +- \`size\`: "small" | "normal" | "large". - \`icon\`: Optional icon name. - Inherits Kobalte Button props and native button attributes. @@ -39,7 +39,7 @@ export default { }, variant: { control: "select", - options: ["neutral", "danger", "contrast", "ghost", "ghost-muted"], + options: ["neutral", "danger", "contrast", "ghost", "ghost-muted", "loading"], }, size: { control: "select", @@ -67,6 +67,7 @@ export const Variants = { Ghost muted + Loading
), } @@ -116,7 +117,7 @@ export const Icon = { export const AllStates = { render: () => { - const variants = ["neutral", "danger", "contrast", "ghost", "ghost-muted"] as const + const variants = ["neutral", "danger", "contrast", "ghost", "ghost-muted", "loading"] as const const states = ["default", "hover", "pressed", "focus", "disabled"] as const const toTitleCase = (value: string) => value.charAt(0).toUpperCase() + value.slice(1) return ( diff --git a/packages/ui/src/v2/components/button-v2.tsx b/packages/ui/src/v2/components/button-v2.tsx index 0900d46d09a6..c6c6099e21db 100644 --- a/packages/ui/src/v2/components/button-v2.tsx +++ b/packages/ui/src/v2/components/button-v2.tsx @@ -7,7 +7,7 @@ export interface ButtonV2Props extends ComponentProps, Pick, "class" | "classList" | "children"> { size?: "small" | "normal" | "large" - variant?: "neutral" | "danger" | "contrast" | "ghost" | "ghost-muted" | "outline" + variant?: "neutral" | "danger" | "outline" | "contrast" | "ghost" | "ghost-muted" | "loading" icon?: IconProps["name"] } diff --git a/packages/ui/src/v2/components/loader-v2.css b/packages/ui/src/v2/components/loader-v2.css new file mode 100644 index 000000000000..5d7e10bb97ef --- /dev/null +++ b/packages/ui/src/v2/components/loader-v2.css @@ -0,0 +1,32 @@ +[data-component="loader-v2"] { + --_duration: 900ms; + display: block; + flex-shrink: 0; + color: var(--v2-icon-icon-muted, #808080); + animation: loader-v2-spin var(--_duration) linear infinite; + transform-origin: center; +} + +[data-component="loader-v2"] [data-slot="loader-v2-background"] { + stroke: currentColor; + opacity: 0.2; +} + +[data-component="loader-v2"] [data-slot="loader-v2-progress"] { + stroke: currentColor; + stroke-linecap: round; + transform: rotate(-90deg); + transform-origin: center; +} + +@keyframes loader-v2-spin { + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: reduce) { + [data-component="loader-v2"] { + animation: none; + } +} diff --git a/packages/ui/src/v2/components/loader-v2.stories.tsx b/packages/ui/src/v2/components/loader-v2.stories.tsx new file mode 100644 index 000000000000..2160d545c4ee --- /dev/null +++ b/packages/ui/src/v2/components/loader-v2.stories.tsx @@ -0,0 +1,42 @@ +import { LoaderV2 } from "./loader-v2" + +const docs = `### Overview +Circular v2 loader for compact loading states. + +### API +- Accepts standard SVG props. + +### Behavior +- The foreground ring covers 33% of the circumference and rotates continuously. + +### Accessibility +- Sets \`aria-hidden="true"\` by default. +` + +export default { + title: "UI V2/Loader", + id: "components-loader-v2", + component: LoaderV2, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => , +} + +export const Sizes = { + render: () => ( +
+ + + +
+ ), +} diff --git a/packages/ui/src/v2/components/loader-v2.tsx b/packages/ui/src/v2/components/loader-v2.tsx new file mode 100644 index 000000000000..eb3697ad74d6 --- /dev/null +++ b/packages/ui/src/v2/components/loader-v2.tsx @@ -0,0 +1,31 @@ +import { splitProps, type ComponentProps } from "solid-js" +import "./loader-v2.css" + +export function LoaderV2(props: ComponentProps<"svg">) { + const [local, rest] = splitProps(props, ["class", "classList", "width", "height"]) + return ( + + + + + ) +} diff --git a/packages/ui/src/v2/components/radio-v2.css b/packages/ui/src/v2/components/radio-v2.css index 1b5699aea353..5177af4330c6 100644 --- a/packages/ui/src/v2/components/radio-v2.css +++ b/packages/ui/src/v2/components/radio-v2.css @@ -45,6 +45,7 @@ } [data-slot="radio-v2-item"] { + position: relative; display: flex; flex-direction: row; align-items: flex-start; @@ -131,10 +132,6 @@ linear-gradient(180deg, var(--v2-alpha-light-20) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-accent); } - &:where([data-checked][data-disabled]) [data-slot="radio-v2-item-control"] { - opacity: 0.5; - } - &:where([data-invalid]):where(:not([data-checked])) [data-slot="radio-v2-item-control"] { background: var(--v2-state-bg-danger); box-shadow: inset 0 0 0 0.5px var(--v2-state-border-danger); @@ -195,8 +192,11 @@ user-select: none; } - &:where([data-disabled]) [data-slot="radio-v2-item-label"], + &:where([data-disabled]) [data-slot="radio-v2-item-label-text"] { + color: var(--v2-text-text-muted); + } + &:where([data-disabled]) [data-slot="radio-v2-item-description"] { - opacity: 0.5; + color: var(--v2-text-text-faint); } } From 34ee94232f442a14dfe43913d89cd6f33c34d4dc Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 1 Jul 2026 07:40:25 +0000 Subject: [PATCH 016/140] chore: generate --- packages/app/src/wsl/dialog-add-server.tsx | 17 +++-- packages/app/src/wsl/settings-model.ts | 4 +- packages/desktop/src/main/wsl/servers.test.ts | 64 +++++++++---------- 3 files changed, 40 insertions(+), 45 deletions(-) diff --git a/packages/app/src/wsl/dialog-add-server.tsx b/packages/app/src/wsl/dialog-add-server.tsx index 6600347d008f..3c0b7ee35b8d 100644 --- a/packages/app/src/wsl/dialog-add-server.tsx +++ b/packages/app/src/wsl/dialog-add-server.tsx @@ -82,9 +82,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) { - {controller.view() === "main" - ? language.t("wsl.server.add") - : language.t("wsl.onboarding.installDistro")} + {controller.view() === "main" ? language.t("wsl.server.add") : language.t("wsl.onboarding.installDistro")} @@ -144,7 +142,12 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
{language.t("wsl.onboarding.installedDistros")} - + {language.t("wsl.onboarding.checkAgain")}
@@ -436,11 +439,7 @@ function DialogWslSetup(props: {
- + {language.t("wsl.onboarding.installWsl")} diff --git a/packages/app/src/wsl/settings-model.ts b/packages/app/src/wsl/settings-model.ts index 6d43e0a70ddd..8a172e0574ed 100644 --- a/packages/app/src/wsl/settings-model.ts +++ b/packages/app/src/wsl/settings-model.ts @@ -36,9 +36,7 @@ export type AddableProbePlan = { distros: string[] } -export type AutoProbePlan = - | { key: "runtime"; action: "probeRuntime" } - | { key: "distros"; action: "refreshDistros" } +export type AutoProbePlan = { key: "runtime"; action: "probeRuntime" } | { key: "distros"; action: "refreshDistros" } export type AddServerProbePlan = | { kind: "auto"; key: string; plan: AutoProbePlan } diff --git a/packages/desktop/src/main/wsl/servers.test.ts b/packages/desktop/src/main/wsl/servers.test.ts index 94cff52bfd3a..35b688d3c868 100644 --- a/packages/desktop/src/main/wsl/servers.test.ts +++ b/packages/desktop/src/main/wsl/servers.test.ts @@ -1,5 +1,11 @@ import { expect, test } from "bun:test" -import { clearWslDistroState, requireWslIpcString, requireWslIpcStrings, wslServerIdToRestart, wslTerminalArgs } from "./policy" +import { + clearWslDistroState, + requireWslIpcString, + requireWslIpcStrings, + wslServerIdToRestart, + wslTerminalArgs, +} from "./policy" import { expectOpencodeVersion, pendingRestartAfterWslInstall, @@ -149,22 +155,18 @@ test("probes addable distros in parallel before checking OpenCode", async () => const started: string[] = [] const release = new Map void>() const opencode: string[] = [] - const controller = createWslServersController( - "1.16.2", - async () => new Promise(() => undefined), - { - ...testControllerOptions(), - probeDistro: async (distro) => { - started.push(distro) - await new Promise((resolve) => release.set(distro, resolve)) - return { name: distro, canExecute: true, hasBash: true, hasCurl: true, error: null } - }, - resolveOpencode: async (distro) => { - opencode.push(distro) - return "/home/me/.opencode/bin/opencode" - }, + const controller = createWslServersController("1.16.2", async () => new Promise(() => undefined), { + ...testControllerOptions(), + probeDistro: async (distro) => { + started.push(distro) + await new Promise((resolve) => release.set(distro, resolve)) + return { name: distro, canExecute: true, hasBash: true, hasCurl: true, error: null } }, - ) + resolveOpencode: async (distro) => { + opencode.push(distro) + return "/home/me/.opencode/bin/opencode" + }, + }) const task = controller.probeAddable(["Debian", "Ubuntu"]) await waitFor(() => started.length === 2) @@ -182,24 +184,20 @@ test("probes addable distros in parallel before checking OpenCode", async () => test("does not check OpenCode in addable distros that cannot execute commands", async () => { persistedServers = [] const opencode: string[] = [] - const controller = createWslServersController( - "1.16.2", - async () => new Promise(() => undefined), - { - ...testControllerOptions(), - probeDistro: async (distro) => ({ - name: distro, - canExecute: distro === "Debian", - hasBash: distro === "Debian", - hasCurl: distro === "Debian", - error: distro === "Debian" ? null : "Open Ubuntu once to finish setup", - }), - resolveOpencode: async (distro) => { - opencode.push(distro) - return "/home/me/.opencode/bin/opencode" - }, + const controller = createWslServersController("1.16.2", async () => new Promise(() => undefined), { + ...testControllerOptions(), + probeDistro: async (distro) => ({ + name: distro, + canExecute: distro === "Debian", + hasBash: distro === "Debian", + hasCurl: distro === "Debian", + error: distro === "Debian" ? null : "Open Ubuntu once to finish setup", + }), + resolveOpencode: async (distro) => { + opencode.push(distro) + return "/home/me/.opencode/bin/opencode" }, - ) + }) await controller.probeAddable(["Debian", "Ubuntu"]) From cd00422298ec34d8aac218ff3bb3148129c757f0 Mon Sep 17 00:00:00 2001 From: Julian Coy Date: Wed, 1 Jul 2026 04:15:31 -0400 Subject: [PATCH 017/140] fix(app): question UI fixes and UX improvements (#34116) Co-authored-by: Brendan Allan Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> --- .../regression/session-request-docks.spec.ts | 23 ++ packages/app/src/i18n/en.ts | 4 + .../composer/session-question-dock.tsx | 347 +++++++++++------- .../src/components/message-part.css | 49 ++- 4 files changed, 279 insertions(+), 144 deletions(-) diff --git a/packages/app/e2e/regression/session-request-docks.spec.ts b/packages/app/e2e/regression/session-request-docks.spec.ts index e66cf81504ee..036eaaef4221 100644 --- a/packages/app/e2e/regression/session-request-docks.spec.ts +++ b/packages/app/e2e/regression/session-request-docks.spec.ts @@ -38,6 +38,29 @@ test("shows a pending question dock", async ({ page }) => { await expect(question.getByRole("radio", { name: /Extended/ })).toBeVisible() await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0) + const rejectRequests: string[] = [] + page.on("request", (request) => { + if (request.method() !== "POST") return + if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url()) + }) + + await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click() + await expect(question).toBeVisible() + await expect(question.getByText("Which implementation should be used?")).toBeVisible() + await expect(question.getByText("Select one answer")).toBeHidden() + await expect(question.getByRole("radio", { name: /Minimal/ })).toBeHidden() + await expect(question.getByRole("radio", { name: /Extended/ })).toBeHidden() + await expect(question.getByRole("button", { name: "Dismiss" })).toBeVisible() + await expect(question.getByRole("button", { name: "Submit" })).toBeVisible() + await expect(page.locator('[data-component="question-minimized-dock"]')).toHaveCount(0) + expect(rejectRequests).toEqual([]) + + await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click() + await expect(question).toBeVisible() + await expect(question.getByText("Which implementation should be used?")).toBeVisible() + await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible() + expect(rejectRequests).toEqual([]) + await question.getByRole("radio", { name: /Minimal/ }).click() const reply = page.waitForRequest( (request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 561fbb5888ec..0124fcf6ed76 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -657,6 +657,10 @@ export const dict = { "session.todo.expand": "Expand", "session.todo.progress": "{{done}} of {{total}} todos completed", "session.question.progress": "{{current}} of {{total}} questions", + "session.question.minimize": "Minimize question", + "session.question.restore": "Restore question", + "session.question.pending.one": "{{count}} pending question", + "session.question.pending.other": "{{count}} pending questions", "session.followupDock.summary.one": "{{count}} queued message", "session.followupDock.summary.other": "{{count}} queued messages", "session.followupDock.sendNow": "Send now", diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 93ab8818c60c..d9a456f6e1a8 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -1,9 +1,10 @@ -import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js" +import { For, Show, createEffect, createMemo, onCleanup, onMount, type Component } from "solid-js" import { createStore } from "solid-js/store" import { useMutation } from "@tanstack/solid-query" import { Button } from "@opencode-ai/ui/button" import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt" import { Icon } from "@opencode-ai/ui/icon" +import { useSpring } from "@opencode-ai/ui/motion-spring" import { showToast } from "@/utils/toast" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useLanguage } from "@/context/language" @@ -77,9 +78,12 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit customOn: cached?.customOn ?? ([] as boolean[]), editing: false, focus: 0, + minimized: false, + optionsHeight: 180, }) let root: HTMLDivElement | undefined + let optionsRef: HTMLDivElement | undefined let customRef: HTMLButtonElement | undefined let optsRef: HTMLButtonElement[] = [] let replied = false @@ -96,11 +100,13 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit const n = Math.min(store.tab + 1, total()) return language.t("session.question.progress", { current: n, total: total() }) }) - const customLabel = () => language.t("ui.messagePart.option.typeOwnAnswer") const customPlaceholder = () => language.t("ui.question.custom.placeholder") const last = createMemo(() => store.tab >= total() - 1) + const collapse = useSpring(() => (store.minimized ? 1 : 0), { visualDuration: 0.3, bounce: 0 }) + const hidden = createMemo(() => Math.max(0, Math.min(1, collapse()))) + const optionsOff = createMemo(() => hidden() > 0.98) const customUpdate = (value: string, selected: boolean = on()) => { const prev = input().trim() @@ -192,6 +198,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit focus(pickFocus()) }) + createEffect(() => { + const el = optionsRef + if (!el) return + const update = () => setStore("optionsHeight", (height) => Math.max(height, el.scrollHeight)) + update() + createResizeObserver(el, update) + }) + onCleanup(() => { if (focusFrame !== undefined) cancelAnimationFrame(focusFrame) if (replied) return @@ -405,7 +419,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit const tab = store.tab + 1 setStore("tab", tab) setStore("editing", false) - focus(pickFocus(tab)) + if (!store.minimized) focus(pickFocus(tab)) } const back = () => { @@ -414,163 +428,212 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit const tab = store.tab - 1 setStore("tab", tab) setStore("editing", false) - focus(pickFocus(tab)) + if (!store.minimized) focus(pickFocus(tab)) } const jump = (tab: number) => { if (sending()) return setStore("tab", tab) setStore("editing", false) - focus(pickFocus(tab)) + if (!store.minimized) focus(pickFocus(tab)) + } + + const minimize = () => { + if (sending()) return + setStore("editing", false) + setStore("minimized", true) + } + + const restore = () => { + if (sending()) return + setStore("minimized", false) + focus(pickFocus()) } return ( - (root = el)} - onKeyDown={nav} - header={ - <> -
{summary()}
- 1}> -
- - {(_, i) => ( -
+
+ -
- - } - footer={ - <> - -
- 0}> - - - -
- - } - > -
- {question()?.question} -
- {language.t("ui.question.singleHint")}}> -
{language.t("ui.question.multiHint")}
-
-
- - {(opt, i) => ( - - - setStore("focus", options().length)} - onClick={customOpen} - > - - - {customLabel()} - {input() || customPlaceholder()} - - - } +
+ 0}> + + + +
+ + } + > +
-
{ - if (sending()) { - e.preventDefault() - return - } - if (e.target instanceof HTMLTextAreaElement) return - const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]') - if (input instanceof HTMLTextAreaElement) input.focus() - }} - onSubmit={(e) => { - e.preventDefault() - commitCustom() - }} - > - - - {customLabel()} -