diff --git a/bun.lock b/bun.lock index 9feda73d6850..e5375a062914 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -36,7 +36,7 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz", + "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -95,7 +95,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.5", + "version": "1.18.7", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -143,7 +143,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -157,7 +157,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@cloudflare/vite-plugin": "1.44.0", "@ibm/plex": "6.4.1", @@ -193,7 +193,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@aws-sdk/client-sts": "3.1085.0", "@jsx-email/render": "1.1.1", @@ -220,7 +220,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -242,7 +242,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@jsx-email/all": "2.2.3", "@tsconfig/bun": "1.0.9", @@ -265,7 +265,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@cloudflare/vite-plugin": "1.44.0", "@opencode-ai/console-core": "workspace:*", @@ -285,7 +285,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.5", + "version": "1.18.7", "bin": { "opencode": "./bin/opencode", }, @@ -337,7 +337,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.11.1", + "gitlab-ai-provider": "6.12.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -379,7 +379,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -433,7 +433,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -447,7 +447,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "effect": "catalog:", }, @@ -459,7 +459,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -491,7 +491,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -507,7 +507,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -538,7 +538,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -557,7 +557,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.5", + "version": "1.18.7", "bin": { "opencode": "./bin/opencode", }, @@ -630,7 +630,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.11.1", + "gitlab-ai-provider": "6.12.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -688,7 +688,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -764,7 +764,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "cross-spawn": "catalog:", }, @@ -779,7 +779,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -794,10 +794,10 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz", + "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -839,7 +839,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -852,7 +852,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -886,7 +886,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@aws-sdk/client-athena": "3.1085.0", "@planetscale/database": "1.19.0", @@ -905,7 +905,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@aws-sdk/client-firehose": "3.1085.0", "@effect/platform-node": "catalog:", @@ -947,7 +947,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -974,7 +974,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1025,7 +1025,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@astrojs/cloudflare": "13.7.0", "@astrojs/markdown-remark": "7.2.1", @@ -3647,7 +3647,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.11.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-SJ6f5qa7P8md6lPrserryER3zerLkrezlnqqYQ2AbvDPpHLbwtbyk0FYJ5kNRcmbI80i/VMcsMBP0YIRdc3ucQ=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.12.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-uGan7MQaNfmV5SH9wiMJkZ7D5QxM4KVPRcjIHtPCM5icZjgXl5mrHMjr3/e7TAHa9OjEJKT0a2FxzPgwRtN3PQ=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], @@ -5629,7 +5629,7 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13.tgz", {}, "sha512-1cYJikTCrXNhnS2qQ3P3rtdbGqhvJKksswG0amJNqgaeUfz3xDlKEDx+YoIosT7Cqk/AtO18jtip0lYL3TVdHQ=="], + "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], "@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], @@ -5647,7 +5647,7 @@ "@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13.tgz", {}, "sha512-1cYJikTCrXNhnS2qQ3P3rtdbGqhvJKksswG0amJNqgaeUfz3xDlKEDx+YoIosT7Cqk/AtO18jtip0lYL3TVdHQ=="], + "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], "@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], diff --git a/nix/hashes.json b/nix/hashes.json index 407d7812fb22..06650c6488ec 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-0kcwV34P2C3yKg2eG9W2nW+OedrSBb+1TdpuUeYtauY=", - "aarch64-linux": "sha256-yHVygApQchAB34wrtFR4GU0CkmZOlLsl3wsp15u0xzs=", - "aarch64-darwin": "sha256-DyalcwyK2Wn5R6249keFcNVECbgtjYNjscOFqTi88FI=", - "x86_64-darwin": "sha256-BkGw0GWN9W9q+/g4FYR0MqxUuFP80BPoERO+ypz/arQ=" + "x86_64-linux": "sha256-szhy0258K6IMWi3WRrAzRfhEEIDkGTS04KQwQw+DXyI=", + "aarch64-linux": "sha256-eVqUhXGKgx7rmRzpdr9G59Xrxvj2J0Dhx4Q7QRBHKiQ=", + "aarch64-darwin": "sha256-r/xAkD/yktfxOVTAArWYb0ZMWe/hEqh80dkYsigRkm8=", + "x86_64-darwin": "sha256-xEKSQZM4gYoz9RoaHEvuxrT8AFXHx8hMD1oKGi8lc3Y=" } } diff --git a/packages/app/e2e/regression/review-state-persistence.spec.ts b/packages/app/e2e/regression/review-state-persistence.spec.ts index 4f67756d53c2..0d6756201e7e 100644 --- a/packages/app/e2e/regression/review-state-persistence.spec.ts +++ b/packages/app/e2e/regression/review-state-persistence.spec.ts @@ -65,7 +65,7 @@ async function switchSession(page: Page, title: string) { async function setup(page: Page) { await mockOpenCodeServer(page, { - protocol: "v2", + protocol: "v1", directory, project: { id: projectID, @@ -93,20 +93,18 @@ async function setup(page: Page) { route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: { directory }, data: { branch: "feature", defaultBranch: "dev" } }), + body: JSON.stringify({ branch: "feature", default_branch: "dev" }), }), ) await page.route("**/vcs/diff**", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - location: { directory }, - data: - new URL(route.request().url()).searchParams.get("mode") === "branch" - ? [diff("src/alpha.ts"), diff("src/beta.ts")] - : [diff("src/alpha.ts"), diff("src/gamma.ts")], - }), + body: JSON.stringify( + new URL(route.request().url()).searchParams.get("mode") === "branch" + ? [diff("src/alpha.ts"), diff("src/beta.ts")] + : [diff("src/alpha.ts"), diff("src/gamma.ts")], + ), }), ) await page.addInitScript( diff --git a/packages/app/e2e/regression/review-terminal-stacked.spec.ts b/packages/app/e2e/regression/review-terminal-stacked.spec.ts index 7cc8723a8cf7..79b564820e20 100644 --- a/packages/app/e2e/regression/review-terminal-stacked.spec.ts +++ b/packages/app/e2e/regression/review-terminal-stacked.spec.ts @@ -25,7 +25,7 @@ test("keeps the review tree and terminal sized when both panels are open", async let detailFailures = 1 await page.setViewportSize({ width: 1400, height: 900 }) await mockOpenCodeServer(page, { - protocol: "v2", + protocol: "v1", directory, project: { id: projectID, @@ -67,33 +67,31 @@ test("keeps the review tree and terminal sized when both panels are open", async status: 200, contentType: "application/json", body: JSON.stringify({ - location: { directory }, - data: { branch: "review-pane-performance", defaultBranch: "dev" }, + branch: "review-pane-performance", + default_branch: "dev", }), }), ) - await page.route("**/api/vcs/diff**", (route) => { + await page.route("**/vcs/diff**", (route) => { const url = new URL(route.request().url()) - const scope = url.searchParams.get("location[directory]")?.replaceAll("\\", "/") + const scope = url.searchParams.get("directory")?.replaceAll("\\", "/") const detail = scope?.endsWith("/src/branch/d00027") if (detail && detailFailures-- > 0) return route.fulfill({ status: 500, body: "retry detail" }) return route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - location: { directory: scope ?? directory, project: { id: projectID, directory } }, - data: - url.searchParams.get("mode") === "branch" - ? detail - ? branchDiffs - .filter((diff) => diff.file.startsWith("src/branch/d00027/")) - .map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion)) - : branchDiffs - : Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)), - }), + body: JSON.stringify( + url.searchParams.get("mode") === "branch" + ? detail + ? branchDiffs + .filter((diff) => diff.file.startsWith("src/branch/d00027/")) + .map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion)) + : branchDiffs + : Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)), + ), }) }) - await page.route("**/api/pty*", (route) => + await page.route("**/pty*", (route) => route.fulfill({ status: 200, contentType: "application/json", @@ -111,7 +109,7 @@ test("keeps the review tree and terminal sized when both panels are open", async }), }), ) - await page.route("**/api/pty/pty_review_terminal*", (route) => + await page.route("**/pty/pty_review_terminal*", (route) => route.fulfill({ status: 200, contentType: "application/json", @@ -129,7 +127,7 @@ test("keeps the review tree and terminal sized when both panels are open", async }), }), ) - await page.route("**/api/pty/pty_review_terminal/connect-token*", (route) => + await page.route("**/pty/pty_review_terminal/connect-token*", (route) => route.fulfill({ status: 200, contentType: "application/json", @@ -139,7 +137,7 @@ test("keeps the review tree and terminal sized when both panels are open", async }), }), ) - await page.routeWebSocket("**/api/pty/pty_review_terminal/connect", () => undefined) + await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined) await page.addInitScript(() => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( @@ -177,8 +175,8 @@ test("keeps the review tree and terminal sized when both panels are open", async const lazyDiff = page.waitForRequest((request) => { const url = new URL(request.url()) return ( - url.pathname === "/api/vcs/diff" && - url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true + url.pathname === "/vcs/diff" && + url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true ) }) await lastFile.click() @@ -192,8 +190,8 @@ test("keeps the review tree and terminal sized when both panels are open", async const refreshedDiff = page.waitForRequest((request) => { const url = new URL(request.url()) return ( - url.pathname === "/api/vcs/diff" && - url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true + url.pathname === "/vcs/diff" && + url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true ) }) sessionStatus[sessionID] = { type: "idle" } diff --git a/packages/app/e2e/regression/terminal-tab-switch.spec.ts b/packages/app/e2e/regression/terminal-tab-switch.spec.ts index 0076c5a2ca2f..165920753cb2 100644 --- a/packages/app/e2e/regression/terminal-tab-switch.spec.ts +++ b/packages/app/e2e/regression/terminal-tab-switch.spec.ts @@ -32,7 +32,7 @@ test("keeps the terminal session alive when switching session tabs in a workspac const connection = new URL(connections[0]!) expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`) expect(connection.searchParams.get("location[directory]")).toBe(directory) - expect(connection.searchParams.get("ticket")).toBe("e2e-ticket") + expect(connection.searchParams.get("ticket")).toBeNull() await writeProbe(page) await switchTab(page, titleB) diff --git a/packages/app/e2e/user-story/model-selection-flow.spec.ts b/packages/app/e2e/user-story/model-selection-flow.spec.ts new file mode 100644 index 000000000000..22b8bb41fe25 --- /dev/null +++ b/packages/app/e2e/user-story/model-selection-flow.spec.ts @@ -0,0 +1,97 @@ +import { expect, test } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +const directory = "C:/OpenCode/NewProject" + +test("creates a session in a new project, connects OpenCode Go, and selects its model", async ({ page }) => { + let connectedGo = false + let pendingGo = false + const connections: Array<{ integrationID: string; body: unknown }> = [] + + await mockOpenCodeServer(page, { + directory, + project: { + id: "proj_model_selection_flow", + worktree: directory, + vcs: "git", + name: "NewProject", + time: { created: 1_700_000_000_000, updated: 1_700_000_000_000 }, + sandboxes: [], + }, + provider: () => ({ + all: [ + { + id: "opencode", + name: "OpenCode", + models: { + "free-model": { + id: "free-model", + name: "Free Model", + cost: { input: 0, output: 0 }, + limit: { context: 200_000 }, + }, + }, + }, + { + id: "opencode-go", + name: "OpenCode Go", + models: { + "go-model-1": { + id: "go-model-1", + name: "Go Model 1", + cost: { input: 1, output: 1 }, + limit: { context: 200_000 }, + }, + }, + }, + ], + connected: connectedGo ? ["opencode", "opencode-go"] : ["opencode"], + default: { providerID: "opencode", modelID: "free-model" }, + }), + integrationMethods: { "opencode-go": [{ type: "api", label: "API key" }] }, + onConnectKey: (input) => { + connections.push(input) + if (input.integrationID === "opencode-go") pendingGo = true + }, + onInstanceDispose: () => { + if (pendingGo) connectedGo = true + }, + sessions: [], + pageMessages: () => ({ items: [] }), + fileList: (path) => + path ? [] : [{ name: "NewProject", path: "NewProject", absolute: directory, type: "directory", ignored: false }], + findFiles: () => ["NewProject"], + }) + await page.addInitScript(() => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem("opencode.global.dat:server", JSON.stringify({ projects: { local: [] } })) + }) + + await page.goto("/") + const addProject = page.locator('[data-action="home-add-project-row"]') + await expectAppVisible(addProject) + await addProject.click() + await page.locator("[data-directory-path]").click() + + await page.locator('[data-action="home-new-session"]').click() + await expectAppVisible(page.locator('[data-component="prompt-input-v2"]')) + + const modelControl = page.locator('[data-action="prompt-model"]') + await modelControl.click() + await expect(page.locator('[data-section="free-models"]')).toContainText("Free models provided by OpenCode") + + await page.locator('[data-provider-id="opencode-go"]').click() + await page.locator('[data-input="provider-api-key"]').fill("mock-go-api-key") + await page.locator('[data-action="provider-connect-submit"]').click() + await expect(page.locator('[data-component="dialog-v2"]')).toHaveCount(0) + expect(connections).toEqual([{ integrationID: "opencode-go", body: { type: "api", key: "mock-go-api-key" } }]) + + await expect(modelControl).toHaveAttribute("data-control-type", "popover") + await modelControl.click() + const goModel = page.locator('[data-option-key="opencode-go:go-model-1"]') + await expect(goModel).toBeVisible() + await goModel.click() + + await expect(modelControl).toContainText("Go Model 1") +}) diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index 0e7dfc087cc5..76987421b607 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -5,7 +5,10 @@ const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mc export interface MockServerConfig { protocol?: "v1" | "v2" - provider: unknown + provider: unknown | (() => unknown) + integrationMethods?: Record + onConnectKey?: (input: { integrationID: string; body: unknown }) => void + onInstanceDispose?: () => void directory: string project: unknown sessions: ({ id: string } & Record)[] @@ -31,7 +34,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { const cursors = new Map() let nextCursor = 0 const staticRoutes: Record = { - "/provider": config.provider, "/path": { state: config.directory, config: config.directory, @@ -75,6 +77,18 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { if (path === "/api/health" && config.protocol === "v2") return json(route, { healthy: true, version: "2.0.0", pid: 1 }) if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true }) + if (path === "/provider") + return json(route, typeof config.provider === "function" ? config.provider() : config.provider) + if (path === "/provider/auth") return json(route, config.integrationMethods ?? {}) + const legacyAuth = path.match(/^\/auth\/([^/]+)$/)?.[1] + if (legacyAuth && route.request().method() === "PUT") { + config.onConnectKey?.({ integrationID: legacyAuth, body: route.request().postDataJSON() }) + return json(route, true) + } + if (path === "/instance/dispose" && route.request().method() === "POST") { + config.onInstanceDispose?.() + return json(route, true) + } if (path === "/permission") return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])) if (path === "/question") @@ -130,8 +144,11 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { location: location(config), data: { id: integration, name: integration, methods: [{ type: "key", label: "API key" }], connections: [] }, }) - if (/^\/api\/integration\/[^/]+\/connect\/key$/.test(path) && route.request().method() === "POST") + const integrationConnect = path.match(/^\/api\/integration\/([^/]+)\/connect\/key$/)?.[1] + if (integrationConnect && route.request().method() === "POST") { + config.onConnectKey?.({ integrationID: integrationConnect, body: route.request().postDataJSON() }) return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } if (path === "/api/project") return json(route, [config.project]) if (path === "/api/project/current") return json(route, { id: (config.project as { id?: string }).id, directory: config.directory }) diff --git a/packages/app/package.json b/packages/app/package.json index 23a830fcd57e..8341a2b26884 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.5", + "version": "1.18.7", "description": "", "type": "module", "exports": { @@ -53,7 +53,7 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz", + "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index c1faab2496d2..82503769b840 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -28,7 +28,6 @@ import { Switch, } from "solid-js" import { createStore, produce } from "solid-js/store" -import { useQueryClient } from "@tanstack/solid-query" import { useParams } from "@solidjs/router" import { Link } from "@/components/link" import { useServerSDK } from "@/context/server-sdk" @@ -38,7 +37,6 @@ import { useSettings } from "@/context/settings" import { popularProviders, useProviders } from "@/hooks/use-providers" import { CustomProviderForm } from "./dialog-custom-provider" import { decode64 } from "@/utils/base64" -import { pathKey } from "@/utils/path-key" const CUSTOM_ID = "_custom" type ConnectMethod = Extract @@ -159,7 +157,7 @@ function ProviderPicker(props: { const settings = useSettings() if (settings.general.newLayoutDesigns()) return - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const language = useLanguage() const popularGroup = () => language.t("dialog.provider.group.popular") const otherGroup = () => language.t("dialog.provider.group.other") @@ -231,7 +229,7 @@ function ProviderPickerV2(props: { onSelect: (provider: string) => void onPrepare?: () => void }) { - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const language = useLanguage() const [store, setStore] = createStore({ filter: "", @@ -390,12 +388,11 @@ function ProviderConnection(props: { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() - const queryClient = useQueryClient() const params = useParams() const language = useLanguage() const settings = useSettings() const newLayout = settings.general.newLayoutDesigns - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const directory = () => props.directory?.() ?? decode64(params.dir) const location = () => { const value = directory() @@ -711,9 +708,8 @@ function ProviderConnection(props: { }) async function complete() { - const value = directory() - await queryClient - .refetchQueries(serverSync().queryOptions.providers(value ? pathKey(value) : null)) + await serverSync() + .refreshProviders() .catch(() => undefined) dialog.close() showToast({ @@ -861,6 +857,7 @@ function ProviderConnection(props: { ref={apiKey} class="!w-full" name="apiKey" + data-input="provider-api-key" placeholder={language.t("provider.connect.apiKey.placeholder")} value={formStore.value} invalid={formStore.error !== undefined} @@ -877,7 +874,7 @@ function ProviderConnection(props: { )} - + {language.t("common.continue")} diff --git a/packages/app/src/components/dialog-select-directory-v2.tsx b/packages/app/src/components/dialog-select-directory-v2.tsx index e0909d849bd4..f3376ad35f26 100644 --- a/packages/app/src/components/dialog-select-directory-v2.tsx +++ b/packages/app/src/components/dialog-select-directory-v2.tsx @@ -8,6 +8,7 @@ import { createEffect, createMemo, createResource, createSignal, For, onCleanup, import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" import { ServerConnection } from "@/context/server" +import type { Path } from "@opencode-ai/sdk/v2/client" import { absoluteTreePath, activeTreeNavigation, @@ -68,7 +69,13 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) const [fallbackPath] = createResource( () => (missingBase() ? true : undefined), - () => sdk.api.path.get().catch(() => undefined), + async (): Promise => { + if ((await sdk.protocol) !== "v1") return + return sdk.client.path + .get() + .then((result) => result.data) + .catch(() => undefined) + }, { initialValue: undefined }, ) const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "") @@ -322,6 +329,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { {(suggestion, index) => ( + )} onClose={restoreFocus} - > - - - - - {props.controls.model.selection.current()?.name ?? - language.t("dialog.model.select.title")} - - - + /> diff --git a/packages/app/src/components/prompt-project-selector.tsx b/packages/app/src/components/prompt-project-selector.tsx index 1e5445517dbc..b84afdb56bf0 100644 --- a/packages/app/src/components/prompt-project-selector.tsx +++ b/packages/app/src/components/prompt-project-selector.tsx @@ -18,6 +18,7 @@ import { useLanguage } from "@/context/language" import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" import { pathKey } from "@/utils/path-key" import { handleDocumentSearchKeydown } from "@/utils/search-keydown" +import { createMenuDismissController } from "@/utils/menu-dismiss-controller" export type PromptProject = { name?: string @@ -197,8 +198,8 @@ export function PromptProjectSelector(props: { }) { const [triggerReady, setTriggerReady] = createSignal(false) let contentRef: HTMLDivElement | undefined + const dismiss = createMenuDismissController(() => contentRef) let triggerFrame: number | undefined - let restoreTrigger = true // Floating UI requires a connected anchor; route transitions can construct this trigger before adoption. const setTriggerRef = (element: HTMLButtonElement) => { @@ -221,25 +222,15 @@ export function PromptProjectSelector(props: { props.controller.active() ? contentRef?.querySelector(`[data-option-key="${CSS.escape(props.controller.active())}"]`) : undefined - const afterClose = (callback: () => void) => { - const complete = () => { - if (contentRef?.isConnected) { - requestAnimationFrame(complete) - return - } - requestAnimationFrame(() => requestAnimationFrame(callback)) - } - requestAnimationFrame(complete) - } const selectProject = (project: PromptProject) => { - restoreTrigger = false + dismiss.preventTriggerRestore() props.controller.setOpen(false) - afterClose(() => props.controller.select(project)) + dismiss.afterClose(() => props.controller.select(project)) } const selectAction = (server?: string) => { - restoreTrigger = false + dismiss.preventTriggerRestore() props.controller.setOpen(false) - afterClose(() => props.controller.add(server)) + dismiss.afterClose(() => props.controller.add(server)) } const selectActive = () => { const project = props.controller.activeProject() @@ -267,7 +258,7 @@ export function PromptProjectSelector(props: { ) .filter((element) => !contentRef?.contains(element) && !element.hasAttribute("data-focus-trap")) .findLast((element) => element.offsetParent !== null) - restoreTrigger = false + dismiss.preventTriggerRestore() target?.focus() queueMicrotask(() => { if (props.controller.open()) props.controller.setOpen(false) @@ -291,7 +282,10 @@ export function PromptProjectSelector(props: { placement={props.placement ?? "bottom"} gutter={4} modal={false} - onOpenChange={(open) => props.controller.setOpen(open)} + onOpenChange={(open) => { + if (open) dismiss.allowTriggerRestore() + props.controller.setOpen(open) + }} > @@ -300,11 +294,9 @@ export function PromptProjectSelector(props: { id="prompt-project-menu" class="w-[243px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none [&[data-closed]]:!animate-none" onOpenAutoFocus={(event) => event.preventDefault()} - onPointerDownOutside={() => (restoreTrigger = false)} - onFocusOutside={() => (restoreTrigger = false)} - onCloseAutoFocus={(event) => { - if (!restoreTrigger) event.preventDefault() - }} + onPointerDownOutside={dismiss.preventTriggerRestore} + onFocusOutside={dismiss.preventTriggerRestore} + onCloseAutoFocus={dismiss.onCloseAutoFocus} >
@@ -364,41 +356,45 @@ export function PromptProjectSelector(props: {
- 1} - fallback={ - - - {(project) => ( - - )} - - - } - > - - props.controller.projects().some((project) => project.server?.key === server!.key), - )} +
+ 1} + fallback={ + + + {(project) => ( + + )} + + + } > - {(server) => ( -
-
- {server!.name} + + props.controller.projects().some((project) => project.server?.key === server!.key), + )} + > + {(server) => ( +
+
+ {server!.name} +
+ + project.server?.key === server!.key)} + > + {(project) => ( + + )} + +
- - project.server?.key === server!.key)}> - {(project) => ( - - )} - - -
- )} - - + )} + + +
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 3ab265d72975..a20e66de3ee3 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -132,7 +132,8 @@ export const SettingsGeneral: Component = () => { if ((await sdk.protocol) === "v1") { return (await sdk.client.pty.shells()).data ?? [] } - return (await sdk.api.pty.shells()).data + // return (await sdk.api.pty.shells()).data + return [] as ShellOption[] }, { initialValue: [] as ShellOption[] }, ) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 7a15d82eaf61..080e6c517bfe 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -41,7 +41,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => const serverSDK = useServerSDK() const protocol = useServerProtocol() const serverSync = useServerSync() - const providers = useProviders() + const providers = useProviders(() => undefined) const providerConnect = useProviderConnectController({ onBack: props.onBack }) const connect = (provider?: string) => { diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx index ab8cb433a3e8..c370f509dc28 100644 --- a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx +++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx @@ -1,4 +1,4 @@ -import { Component, createSignal, startTransition } from "solid-js" +import { Component, createMemo, createSignal, startTransition } from "solid-js" import { Dialog } from "@opencode-ai/ui/v2/dialog-v2" import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2" import { Icon } from "@opencode-ai/ui/icon" @@ -11,6 +11,9 @@ import { SettingsModelsV2 } from "./models" import "./settings-v2.css" import { SettingsServersV2 } from "./servers" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLayout } from "@/context/layout" +import { useTabs } from "@/context/tabs" +import { useServerSync } from "@/context/server-sync" export const DialogSettings: Component<{ sessionID?: string @@ -19,7 +22,20 @@ export const DialogSettings: Component<{ const language = useLanguage() const platform = usePlatform() const dialog = useDialog() + const layout = useLayout() + const tabs = useTabs() + const serverSync = useServerSync() const [tab, setTab] = createSignal(props.defaultValue ?? "general") + const directory = createMemo(() => { + const route = layout.route() + if (route.type === "dir-new-sesssion") return route.dir + if (route.type === "draft") { + const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID) + return draft?.type === "draft" ? draft.directory : undefined + } + if (route.type === "session") return serverSync().session.get(route.sessionId)?.directory + return undefined + }) const showProviders = () => { void dialog.show(() => ) @@ -87,7 +103,7 @@ export const DialogSettings: Component<{ - + diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx index 5a4cb186a65d..613ac96cbae8 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -127,7 +127,8 @@ export const SettingsGeneralV2: Component<{ if ((await sdk.protocol) === "v1") { return (await sdk.client.pty.shells()).data ?? [] } - return (await sdk.api.pty.shells()).data + // return (await sdk.api.pty.shells()).data + return [] as ShellOption[] }, { initialValue: [] as ShellOption[] }, ) diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index 29192114f0fb..acd73ddbe544 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -4,7 +4,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { showToast } from "@/utils/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" -import { createMemo, type Component, For, Show } from "solid-js" +import { createMemo, type Accessor, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useServerProtocol, useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" @@ -29,18 +29,21 @@ const PROVIDER_NOTES = [ const PROVIDER_ICON_SIZE = 16 -export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) => { +export const SettingsProvidersV2: Component<{ + directory: Accessor + onBack?: () => void +}> = (props) => { const dialog = useDialog() const language = useLanguage() const serverSdk = useServerSDK() const protocol = useServerProtocol() const serverSync = useServerSync() - const providers = useProviders() + const providers = useProviders(props.directory) const providerConnect = useProviderConnectController({ onBack: props.onBack }) const connect = (provider?: string) => { providerConnect.select(provider) - void dialog.show(() => ) + void dialog.show(() => ) } const connected = createMemo(() => { diff --git a/packages/app/src/components/status-popover-indicator.test.ts b/packages/app/src/components/status-popover-indicator.test.ts index c1c57b9d7082..d6d6d07a1d81 100644 --- a/packages/app/src/components/status-popover-indicator.test.ts +++ b/packages/app/src/components/status-popover-indicator.test.ts @@ -1,11 +1,21 @@ import { describe, expect, test } from "bun:test" -import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator" +import { + hasNonBlockingServiceIssue, + hasServiceNeedingAttention, + serverStatusDotClass, +} from "./status-popover-indicator" describe("serverStatusDotClass", () => { test("uses the success token while the server and services are healthy", () => { expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: false })).toBe("bg-icon-success-base") }) + test("uses the session attention token when a service needs attention", () => { + expect(serverStatusDotClass({ ready: true, serverHealth: true, attention: true, issue: true })).toBe( + "bg-v2-background-bg-accent", + ) + }) + test("uses the warning token for non-blocking issues while the server is online", () => { expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: true })).toBe("bg-icon-warning-base") }) @@ -34,3 +44,15 @@ describe("hasNonBlockingServiceIssue", () => { expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["connected"] })).toBe(false) }) }) + +describe("hasServiceNeedingAttention", () => { + test("detects MCP states that need user attention", () => { + expect(hasServiceNeedingAttention({ mcp: ["needs_auth"] })).toBe(true) + expect(hasServiceNeedingAttention({ mcp: ["needs_client_registration"] })).toBe(true) + }) + + test("ignores states that do not need user attention", () => { + expect(hasServiceNeedingAttention({ mcp: ["failed"] })).toBe(false) + expect(hasServiceNeedingAttention({ mcp: ["connected", "pending", "disabled"] })).toBe(false) + }) +}) diff --git a/packages/app/src/components/status-popover-indicator.ts b/packages/app/src/components/status-popover-indicator.ts index d89f90febbf6..62fc3bedbff3 100644 --- a/packages/app/src/components/status-popover-indicator.ts +++ b/packages/app/src/components/status-popover-indicator.ts @@ -1,6 +1,10 @@ import type { LspStatus } from "@opencode-ai/sdk/v2/client" import type { McpServer } from "@opencode-ai/client/promise" +export function hasServiceNeedingAttention(input: { mcp: Array }) { + return input.mcp.some((status) => status === "needs_auth" || status === "needs_client_registration") +} + export function hasNonBlockingServiceIssue(input: { mcp: Array lsp: Array @@ -11,9 +15,15 @@ export function hasNonBlockingServiceIssue(input: { ) } -export function serverStatusDotClass(input: { ready: boolean; serverHealth: boolean | undefined; issue: boolean }) { +export function serverStatusDotClass(input: { + ready: boolean + serverHealth: boolean | undefined + attention?: boolean + issue: boolean +}) { if (input.serverHealth === false) return "bg-icon-critical-base" if (!input.ready || input.serverHealth === undefined) return "bg-border-weak-base" + if (input.attention) return "bg-v2-background-bg-accent" if (input.issue) return "bg-icon-warning-base" if (input.serverHealth === true) return "bg-icon-success-base" return "bg-border-weak-base" diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index 2aed45702a1b..767e7e62c6f6 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -9,7 +9,11 @@ import { ServerConnection, useServer } from "@/context/server" import { useServerSDK } from "@/context/server-sdk" import { useSync } from "@/context/sync" import { useGlobal } from "@/context/global" -import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator" +import { + hasNonBlockingServiceIssue, + hasServiceNeedingAttention, + serverStatusDotClass, +} from "./status-popover-indicator" const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody }))) const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody }))) @@ -22,6 +26,11 @@ export function StatusPopover() { const [shown, setShown] = createSignal(false) const serverHealth = () => global.servers.health[server.key]?.healthy const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready)) + const attention = createMemo(() => + hasServiceNeedingAttention({ + mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), + }), + ) const issue = createMemo(() => hasNonBlockingServiceIssue({ mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), @@ -49,6 +58,7 @@ export function StatusPopover() { class={`absolute -top-px -right-px size-1.5 rounded-full ${serverStatusDotClass({ ready: ready(), serverHealth: serverHealth(), + attention: attention(), issue: issue(), })}`} /> @@ -85,6 +95,11 @@ function DirectoryStatusPopover() { const [shown, setShown] = createSignal(false) const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready)) + const attention = createMemo(() => + hasServiceNeedingAttention({ + mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), + }), + ) const issue = createMemo(() => hasNonBlockingServiceIssue({ mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), @@ -95,6 +110,7 @@ function DirectoryStatusPopover() { shown: shown(), ready: ready(), serverHealth: serverHealth(), + attention: attention(), issue: issue(), label: language.t("status.popover.trigger"), onOpenChange: setShown, @@ -118,6 +134,7 @@ function ServerStatusPopover() { shown: shown(), ready: serverHealth() !== undefined, serverHealth: serverHealth(), + attention: false, issue: false, label: language.t("status.popover.trigger"), onOpenChange: setShown, @@ -135,6 +152,7 @@ type StatusPopoverState = { shown: boolean ready: boolean serverHealth: boolean | undefined + attention: boolean issue: boolean label: string onOpenChange: (value: boolean) => void diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index b2e827f73a71..65035034760e 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -573,19 +573,13 @@ export const Terminal = (props: TerminalProps) => { throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.") throw new Error(`PTY connect ticket failed with ${result.response.status}`) } - return sdk() - .api.pty.connectToken({ - ptyID: id, - location: { directory }, - "x-opencode-ticket": "1", - }) - .then((result) => result.data.ticket) - .catch((err: unknown) => { - if (err && typeof err === "object" && "_tag" in err && err._tag === "ForbiddenError") { - throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.") - } - throw err - }) + // return sdk() + // .api.pty.connectToken({ + // ptyID: id, + // location: { directory }, + // "x-opencode-ticket": "1", + // }) + // .then((result) => result.data.ticket) } const retry = (err: unknown) => { @@ -616,7 +610,7 @@ export const Terminal = (props: TerminalProps) => { return undefined }) const protocol = await sdk().protocol - if (protocol === "v2" && !ticket) return + // if (protocol === "v2" && !ticket) return if (once.value) return if (disposed) return diff --git a/packages/app/src/components/titlebar-tab-gesture.ts b/packages/app/src/components/titlebar-tab-gesture.ts index 329bf5e864bc..0aa51189363d 100644 --- a/packages/app/src/components/titlebar-tab-gesture.ts +++ b/packages/app/src/components/titlebar-tab-gesture.ts @@ -12,6 +12,6 @@ export function forwardTabRef(ref: Ref | undefined, element: HTM if (typeof ref === "function") ref(element) } -export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, committing: boolean) { - return !dragging && !editing && !committing +export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, pending: boolean) { + return !dragging && !editing && !pending } diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index 3058e6881af7..ceb43d4486f1 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -1,14 +1,13 @@ import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" import { createResizeObserver } from "@solid-primitives/resize-observer" +import { createMutation } from "@tanstack/solid-query" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { useGlobal } from "@/context/global" -import { useLanguage } from "@/context/language" import { ServerConnection, serverName } from "@/context/server" import { displayName, projectForSession } from "@/pages/layout/helpers" import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar" -import { showToast } from "@/utils/toast" import type { Session } from "@opencode-ai/sdk/v2" import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture" import { TabPreviewPopover } from "./titlebar-tab-popover" @@ -23,8 +22,7 @@ export function TabNavItem(props: { server: ServerConnection.Key session: () => Session | undefined fallbackTitle?: string - onTitleChange?: (title: string) => void - onTitleChangeFailed?: (title: string) => void + onRename: (title: string) => Promise onClose: () => void onNavigate: () => void active?: boolean @@ -34,13 +32,12 @@ export function TabNavItem(props: { pressed?: boolean hidden?: boolean }) { - const language = useLanguage() const [editing, setEditing] = createSignal(false) const [titleOverflowing, setTitleOverflowing] = createSignal(false) let tabRoot!: HTMLDivElement let titleEl!: HTMLSpanElement - let committing = false let measureFrame: number | undefined + const rename = createMutation(() => ({ mutationFn: props.onRename })) const closeTab = (event: MouseEvent) => { event.preventDefault() @@ -116,40 +113,20 @@ export function TabNavItem(props: { selection?.addRange(range) } - const rename = async (title: string) => { - const ctx = serverCtx() - const session = props.session() - if (!ctx || !session) return - await ctx.sdk.api.session.rename({ sessionID: session.id, title }) - } - const closeRename = async (save: boolean) => { - if (committing || !editing()) return - committing = true + if (rename.isPending || !editing()) return const original = props.session()?.title ?? "" const next = (titleEl.textContent ?? "").trim() titleEl.scrollLeft = 0 - if (save && next && next !== original) props.onTitleChange?.(next) setEditing(false) if (!save || !next || next === original) { - committing = false return } - try { - await rename(next) - } catch (err) { - props.onTitleChangeFailed?.(original) - showToast({ - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : undefined, - }) - } - - committing = false + await rename.mutateAsync(next) } createEffect(() => { @@ -163,7 +140,7 @@ export function TabNavItem(props: { const openRename = (event: MouseEvent) => { event.preventDefault() event.stopPropagation() - if (!canOpenTabRename(props.dragging, editing(), committing)) return + if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return const session = props.session() if (!session) return titleEl.textContent = session.title diff --git a/packages/app/src/components/titlebar-tab-strip.tsx b/packages/app/src/components/titlebar-tab-strip.tsx index 7ef53324cdb6..9dc44a65cd85 100644 --- a/packages/app/src/components/titlebar-tab-strip.tsx +++ b/packages/app/src/components/titlebar-tab-strip.tsx @@ -15,6 +15,7 @@ import { useCommand } from "@/context/command" import { useTabs } from "@/context/tabs" import { createTabPromptState } from "@/context/prompt" import { base64Encode } from "@opencode-ai/core/util/encode" +import { showToast } from "@/utils/toast" import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture" function SessionTabSlot(props: { @@ -52,6 +53,25 @@ function SessionTabSlot(props: { const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session()) let prefetched = false + const rename = async (title: string) => { + const value = session() + const ctx = props.serverCtx() + if (!value || !ctx) return + + ctx.sync.session.remember({ ...value, title }) + try { + await ctx.sdk.api.session.rename({ sessionID: value.id, title }) + } catch (err) { + const current = session() + const currentCtx = props.serverCtx() + if (current && currentCtx) currentCtx.sync.session.remember({ ...current, title: value.title }) + showToast({ + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : undefined, + }) + } + } + createEffect(() => { const ctx = props.serverCtx() const value = session() @@ -99,16 +119,7 @@ function SessionTabSlot(props: { server={props.tab.server} session={session} fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)} - onTitleChange={(title) => { - const value = session() - const ctx = props.serverCtx() - if (value && ctx) ctx.sync.session.remember({ ...value, title }) - }} - onTitleChangeFailed={(title) => { - const value = session() - const ctx = props.serverCtx() - if (value && ctx) ctx.sync.session.remember({ ...value, title }) - }} + onRename={rename} onNavigate={() => props.onNavigate(ref)} onClose={props.onClose} active={props.active()} diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 5fc6b3466a47..f0b4730d70c2 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -5,7 +5,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon } from "@opencode-ai/ui/icon" import { Button } from "@opencode-ai/ui/button" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { useTheme } from "@opencode-ai/ui/theme/context" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" @@ -30,31 +29,11 @@ import "./titlebar.css" import { newTabTooltipKeybind } from "./command-tooltip-keybind" import { normalizeSessionInfo } from "@/utils/session" -type TauriDesktopWindow = { - startDragging?: () => Promise - toggleMaximize?: () => Promise -} - -type TauriThemeWindow = { - setTheme?: (theme?: "light" | "dark" | null) => Promise -} - -type TauriApi = { - window?: { - getCurrentWindow?: () => TauriDesktopWindow - } - webviewWindow?: { - getCurrentWebviewWindow?: () => TauriThemeWindow - } -} - -const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__ -const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.() -const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.() const legacyTitlebarHeight = 40 const v2TitlebarHeight = 36 const minTitlebarZoom = 0.25 const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each. +const macTrafficLightsBaseWidth = 84 export type TitlebarUpdate = { version: () => string | undefined @@ -74,7 +53,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl const command = useCommand() const language = useLanguage() const settings = useSettings() - const theme = useTheme() const server = useServer() const navigate = useNavigate() const location = useLocation() @@ -85,9 +63,9 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows") - const electronWindows = createMemo(() => windows() && !tauriApi()) const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux") const web = createMemo(() => platform.platform === "web") + const macTrafficLights = createMemo(() => mac() && !platform.windowFullscreen?.()) const zoom = () => platform.webviewZoom?.() ?? 1 const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom()) const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1) @@ -176,56 +154,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl }, ]) - const getWin = () => { - if (platform.platform !== "desktop") return - return currentDesktopWindow() - } - - createEffect(() => { - if (platform.platform !== "desktop") return - - const scheme = theme.colorScheme() - const value = scheme === "system" ? null : scheme - - const win = currentThemeWindow() - if (!win?.setTheme) return - - void win.setTheme(value).catch(() => undefined) - }) - - const interactive = (target: EventTarget | null) => { - if (!(target instanceof Element)) return false - - const selector = - "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']" - - return !!target.closest(selector) - } - - const drag = (e: MouseEvent) => { - if (platform.platform !== "desktop") return - if (e.buttons !== 1) return - if (interactive(e.target)) return - - const win = getWin() - if (!win?.startDragging) return - - e.preventDefault() - void win.startDragging().catch(() => undefined) - } - - const maximize = (e: MouseEvent) => { - if (platform.platform !== "desktop") return - if (interactive(e.target)) return - if (e.target instanceof Element && e.target.closest("[data-tauri-decorum-tb]")) return - - const win = getWin() - if (!win?.toggleMaximize) return - - e.preventDefault() - void win.toggleMaximize().catch(() => undefined) - } - return (
@@ -459,8 +383,8 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl classList={{ "pt-2": !bottom(), "pb-2": bottom(), - "md:pl-2": mac(), - "md:pl-4": !mac(), + "md:pl-2": macTrafficLights(), + "md:pl-4": !macTrafficLights(), }} > @@ -528,9 +452,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
- -
-
) }} @@ -543,14 +464,13 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
- {/*
*/}
- {!tauriApi() &&
} -
+
diff --git a/packages/app/src/context/command.test.ts b/packages/app/src/context/command.test.ts index cf98ff382519..e0d905751133 100644 --- a/packages/app/src/context/command.test.ts +++ b/packages/app/src/context/command.test.ts @@ -1,5 +1,11 @@ import { describe, expect, test } from "bun:test" -import { commandPaletteOptions, resolveKeybindOption, upsertCommandRegistration, type CommandOption } from "./command" +import { + activeCommandRegistrations, + addCommandRegistration, + commandPaletteOptions, + resolveKeybindOption, + type CommandOption, +} from "./command" const paletteOptions: CommandOption[] = [ { id: "settings.open", title: "Open settings" }, @@ -15,22 +21,31 @@ describe("commandPaletteOptions", () => { }) }) -describe("upsertCommandRegistration", () => { - test("replaces keyed registrations", () => { +describe("command registrations", () => { + test("shadows keyed registrations while retaining the previous owner", () => { const one = () => [{ id: "one", title: "One" }] const two = () => [{ id: "two", title: "Two" }] - const next = upsertCommandRegistration([{ key: "layout", options: one }], { key: "layout", options: two }) + const registrations = addCommandRegistration([{ key: "layout", options: one }], { + key: "layout", + options: two, + }) + const active = activeCommandRegistrations(registrations) - expect(next).toHaveLength(1) - expect(next[0]?.options).toBe(two) + expect(registrations).toHaveLength(2) + expect(active).toHaveLength(1) + expect(active[0]?.options).toBe(two) + + const restored = activeCommandRegistrations(registrations.filter((entry) => entry.options !== two)) + expect(restored).toHaveLength(1) + expect(restored[0]?.options).toBe(one) }) test("keeps unkeyed registrations additive", () => { const one = () => [{ id: "one", title: "One" }] const two = () => [{ id: "two", title: "Two" }] - const next = upsertCommandRegistration([{ options: one }], { options: two }) + const next = activeCommandRegistrations(addCommandRegistration([{ options: one }], { options: two })) expect(next).toHaveLength(2) expect(next[0]?.options).toBe(two) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index f88033bd7e21..054b99e806bd 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -114,9 +114,18 @@ export type CommandRegistration = { options: Accessor } -export function upsertCommandRegistration(registrations: CommandRegistration[], entry: CommandRegistration) { - if (entry.key === undefined) return [entry, ...registrations] - return [entry, ...registrations.filter((x) => x.key !== entry.key)] +export function addCommandRegistration(registrations: CommandRegistration[], entry: CommandRegistration) { + return [entry, ...registrations] +} + +export function activeCommandRegistrations(registrations: CommandRegistration[]) { + const keys = new Set() + return registrations.filter((entry) => { + if (entry.key === undefined) return true + if (keys.has(entry.key)) return false + keys.add(entry.key) + return true + }) } export function parseKeybind(config: string): Keybind[] { @@ -281,7 +290,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const seen = new Set() const all: CommandOption[] = [] - for (const reg of store.registrations) { + for (const reg of activeCommandRegistrations(store.registrations)) { for (const opt of reg.options()) { if (seen.has(opt.id)) { if (import.meta.env.DEV && !warnedDuplicates.has(opt.id)) { @@ -425,7 +434,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex key: id, options, } - setStore("registrations", (arr) => upsertCommandRegistration(arr, entry)) + setStore("registrations", (arr) => addCommandRegistration(arr, entry)) onCleanup(() => { setStore("registrations", (arr) => arr.filter((x) => x !== entry)) }) diff --git a/packages/app/src/context/directory-sync.ts b/packages/app/src/context/directory-sync.ts index ca6df85a53ad..befd5b61e095 100644 --- a/packages/app/src/context/directory-sync.ts +++ b/packages/app/src/context/directory-sync.ts @@ -134,7 +134,8 @@ export const createDirSyncContext = ( }, more: createMemo(() => current()[0].session.length >= current()[0].limit), archive: async (sessionID: string) => { - await serverSDK.api.session.archive({ sessionID, directory }) + if ((await serverSDK.protocol) !== "v1") return + await serverSDK.client.session.update({ sessionID, directory, time: { archived: Date.now() } }) current()[1]( "session", produce((draft) => { diff --git a/packages/app/src/context/global-sync/bootstrap.test.ts b/packages/app/src/context/global-sync/bootstrap.test.ts index de2baa704c0c..71986dd3c40d 100644 --- a/packages/app/src/context/global-sync/bootstrap.test.ts +++ b/packages/app/src/context/global-sync/bootstrap.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import { createStore } from "solid-js/store" import { QueryClient } from "@tanstack/solid-query" import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client" -import type { AgentApi, CatalogApi, CommandApi, ProjectApi, ReferenceApi } from "@opencode-ai/client/promise" +import type { AgentApi, CatalogApi, CommandApi, ReferenceApi } from "@opencode-ai/client/promise" import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { bootstrapDirectory, @@ -17,6 +17,8 @@ import type { State, VcsCache } from "./types" import { ServerScope } from "@/utils/server-scope" import type { ServerApi } from "@/utils/server" +type ProjectApi = ServerApi["project"] + const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse const api = { agent: { list: async () => ({ location: {}, data: [] }) }, @@ -73,14 +75,14 @@ function directoryState() { } describe("bootstrapDirectory", () => { - test("marks a loading directory partial during bootstrap and complete after success", async () => { + test("uses legacy MCP endpoints while refreshing a v1 directory", async () => { const mcpReads: string[] = [] const [store, setStore] = directoryState() await bootstrapDirectory({ directory: "/project", scope: ServerScope.local, - mcp: false, + mcp: true, global: { config: {} satisfies Config, path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" }, @@ -90,6 +92,7 @@ describe("bootstrapDirectory", () => { sdk: { app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) }, config: { get: async () => ({ data: {} }) }, + session: { status: async () => ({ data: {} }) }, vcs: { get: async () => ({ data: undefined }) }, command: { list: async () => { @@ -106,6 +109,14 @@ describe("bootstrapDirectory", () => { return { data: {} } }, }, + experimental: { + resource: { + list: async () => { + mcpReads.push("resource") + return { data: {} } + }, + }, + }, provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) }, } as unknown as OpencodeClient, api, @@ -115,6 +126,7 @@ describe("bootstrapDirectory", () => { loadSessions() {}, translate: (key) => key, queryClient: new QueryClient(), + protocol: Promise.resolve("v1"), }) expect(store.status).toBe("partial") @@ -122,7 +134,7 @@ describe("bootstrapDirectory", () => { await new Promise((resolve) => setTimeout(resolve, 80)) expect(store.status).toBe("complete") - expect(mcpReads).toEqual([]) + expect(mcpReads.sort()).toEqual(["command", "resource", "status"]) }) }) @@ -190,7 +202,7 @@ describe("query keys", () => { calls.push(input) return { location: {}, - data: [{ name: "review", template: "Review files", source: "command" as const }], + data: [{ name: "review", template: "Review files" /* source: "command" as const */ }], } }, } as unknown as CommandApi @@ -198,7 +210,7 @@ describe("query keys", () => { const result = await loadCommands("/repo", api) expect(calls).toEqual([{ location: { directory: "/repo" } }]) - expect(result).toEqual([{ name: "review", template: "Review files", source: "command" }]) + expect(result).toEqual([{ name: "review", template: "Review files" /* source: "command" */ }]) }) test("loads projects from the current endpoint", async () => { diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 4f7b949e61a7..39221d551fa6 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -16,18 +16,12 @@ import type { CommandInfo, CommandListInput, CommandListOutput, - McpApi, - PathGetInput, - PathGetOutput, - PermissionApi, ProjectCurrentInput, ProjectCurrentOutput, ProjectListOutput, - QuestionApi, ReferenceListInput, ReferenceListOutput, SessionApi, - VcsApi, } from "@opencode-ai/client/promise" import { showToast } from "@/utils/toast" import { getFilename } from "@opencode-ai/core/util/path" @@ -50,6 +44,7 @@ import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { ScopedKey, type ServerScope } from "@/utils/server-scope" import { normalizeSessionInfo } from "@/utils/session" import type { ServerProtocol } from "@/utils/server-protocol" +import type { ServerApi } from "@/utils/server" type GlobalStore = { ready: boolean @@ -121,9 +116,10 @@ type ProjectApi = { readonly current: (input?: ProjectCurrentInput) => Promise } -type PathApi = { - readonly get: (input?: PathGetInput) => Promise -} +type McpApi = ServerApi["mcp"] +type PermissionApi = ServerApi["permission"] +type QuestionApi = ServerApi["question"] +type VcsApi = ServerApi["vcs"] export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) => queryOptions({ @@ -143,7 +139,7 @@ export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) => export async function bootstrapGlobal(input: { serverSDK: OpencodeClient - serverAPI: CatalogApi & { readonly path: PathApi; readonly project: ProjectApi } + serverAPI: CatalogApi & { readonly project: ProjectApi } protocol?: Promise scope: ServerScope requestFailedTitle: string @@ -158,7 +154,7 @@ export async function bootstrapGlobal(input: { input.queryClient.fetchQuery( loadProvidersQuery(input.scope, null, input.serverAPI, input.serverSDK, input.protocol), ), - () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.path)), + () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverSDK, input.protocol)), () => input.queryClient .fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project)) @@ -289,17 +285,26 @@ export const loadCommands = ( agent: command.agent, model: providerID && id ? { providerID, id } : undefined, subtask: command.subtask, - source: command.source === "skill" ? undefined : command.source, + // source: command.source === "skill" ? undefined : command.source, } }) } return api.list({ location: { directory } }).then((result) => result.data) }) -export const loadPathQuery = (scope: ServerScope, directory: string | null, api: PathApi) => +export const loadPathQuery = ( + scope: ServerScope, + directory: string | null, + sdk: OpencodeClient, + protocol?: Promise, +) => queryOptions({ queryKey: [scope, directory, "path"], - queryFn: () => retry(() => api.get(directory ? { location: { directory } } : undefined)), + queryFn: async () => { + if ((await protocol) !== "v1") + return { state: "", config: "", worktree: "", directory: directory ?? "", home: "" } + return retry(() => sdk.path.get({ directory: directory ?? undefined }).then((result) => result.data!)) + }, }) export const loadReferencesQuery = ( @@ -328,7 +333,6 @@ export async function bootstrapDirectory(input: { readonly agent: AgentListApi readonly command: CommandListApi readonly mcp: McpApi - readonly path: PathApi readonly permission: PermissionApi readonly project: ProjectApi readonly question: QuestionApi @@ -408,19 +412,20 @@ export async function bootstrapDirectory(input: { !seededPath && (() => input.queryClient - .ensureQueryData(loadPathQuery(input.scope, input.directory, input.api.path)) + .ensureQueryData(loadPathQuery(input.scope, input.directory, input.sdk, input.protocol)) .then((data) => { const next = projectID(data.directory ?? input.directory, input.global.project) if (next) input.setStore("project", next) })), () => - retry(() => - input.api.vcs.get({ location: { directory: input.directory } }).then((result) => { - const next = { branch: result.data.branch, default_branch: result.data.defaultBranch } + retry(async () => { + if ((await input.protocol) !== "v1") return + return input.sdk.vcs.get().then((result) => { + const next = { branch: result.data?.branch, default_branch: result.data?.default_branch } input.setStore("vcs", next) if (next) input.vcsCache.setStore("value", next) - }), - ), + }) + }), input.mcp && (() => loadCommands(input.directory, input.api.command, input.sdk, input.protocol).then((commands) => @@ -503,9 +508,16 @@ export async function bootstrapDirectory(input: { }), ), () => Promise.resolve(input.loadSessions(input.directory)), - input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.api.mcp))), input.mcp && - (() => input.queryClient.fetchQuery(loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp))), + (() => + input.queryClient.fetchQuery( + loadMcpQuery(input.scope, input.directory, input.api.mcp, input.sdk, input.protocol), + )), + input.mcp && + (() => + input.queryClient.fetchQuery( + loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp, input.sdk, input.protocol), + )), () => input.queryClient .fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api, input.sdk, input.protocol)) diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 39ba22c59d51..8f203715a6de 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -211,19 +211,19 @@ export function applyDirectoryEvent(input: { })) break } - case "session.archived": { - const properties = event.properties as { sessionID: string } - const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) - if (!result.found) break - const info = input.store.session[result.index] - input.setStore( - "session", - produce((draft) => void draft.splice(result.index, 1)), - ) - cleanupSessionCaches(input.setStore, properties.sessionID) - if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) - break - } + // case "session.archived": { + // const properties = event.properties as { sessionID: string } + // const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) + // if (!result.found) break + // const info = input.store.session[result.index] + // input.setStore( + // "session", + // produce((draft) => void draft.splice(result.index, 1)), + // ) + // cleanupSessionCaches(input.setStore, properties.sessionID) + // if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) + // break + // } case "session.moved": { const properties = event.properties as { sessionID: string diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index d086582035ae..6235f35c45ae 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -570,16 +570,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( continue } - void serverSdk() - .api.project.update({ projectID: project.id, icon: { color } }) - .then((result) => - serverSync().set("project", (items) => - items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)), - ), - ) - .catch(() => { - if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) - }) + const projectID = project.id + void (async () => { + const sdk = serverSdk() + if ((await sdk.protocol) !== "v1") return + return sdk.client.project + .update({ projectID, directory: worktree, icon: { color } }) + .then((response) => response.data) + .then((result) => { + if (!result) return + serverSync().set("project", (items) => + items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)), + ) + }) + })().catch(() => { + if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) + }) } }) diff --git a/packages/app/src/context/models.tsx b/packages/app/src/context/models.tsx index 736dc3a7c52b..a80cf2e5804d 100644 --- a/packages/app/src/context/models.tsx +++ b/packages/app/src/context/models.tsx @@ -26,7 +26,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( name: "Models", gate: false, init: (props: { directory?: Accessor } = {}) => { - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const [store, setStore, _, ready] = persisted( Persist.global("model", ["model.v1"]), diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 2204aebeeeda..0a1656d67835 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -100,6 +100,9 @@ type PlatformBase = { /** Webview zoom level (desktop only) */ webviewZoom?: Accessor + /** Whether the native desktop window is fullscreen */ + windowFullscreen?: Accessor + /** Get whether native pinch/Ctrl-scroll zoom gestures are enabled (desktop only) */ getPinchZoomEnabled?(): Promise | boolean diff --git a/packages/app/src/context/server-session-v2-reducer.test.ts b/packages/app/src/context/server-session-v2-reducer.test.ts index a1db63b1212b..00cc37cf5240 100644 --- a/packages/app/src/context/server-session-v2-reducer.test.ts +++ b/packages/app/src/context/server-session-v2-reducer.test.ts @@ -114,7 +114,7 @@ describe("v2 session reducer", () => { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", - structured: {}, + metadata: {}, content: [{ type: "text", text: "done" }], executed: true, }, diff --git a/packages/app/src/context/server-session-v2-reducer.ts b/packages/app/src/context/server-session-v2-reducer.ts index 3b9719c09875..b34ab3985ec1 100644 --- a/packages/app/src/context/server-session-v2-reducer.ts +++ b/packages/app/src/context/server-session-v2-reducer.ts @@ -269,13 +269,18 @@ export function createV2SessionReducer() { ...tool, executed: event.data.executed, providerState: event.data.state, - state: { status: "running", input: event.data.input, structured: {}, content: [] }, + // structured: {}, content: [] + state: { status: "running", input: event.data.input, metadata: {} }, time: { ...tool.time, ran: event.created }, })) case "session.tool.progress": return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => tool.state.status === "running" - ? { ...tool, state: { ...tool.state, structured: event.data.structured, content: event.data.content } } + ? { + ...tool, + // state: { ...tool.state, structured: event.data.structured, content: event.data.content }, + state: { ...tool.state, metadata: event.data.metadata }, + } : tool, ) case "session.tool.success": @@ -288,9 +293,10 @@ export function createV2SessionReducer() { state: { status: "completed", input: tool.state.input, - structured: event.data.structured, + // structured: event.data.structured, + metadata: event.data.metadata, content: event.data.content, - result: event.data.result, + // result: event.data.result, }, time: { ...tool.time, completed: event.created }, } @@ -305,10 +311,11 @@ export function createV2SessionReducer() { state: { status: "error", input: typeof tool.state.input === "string" ? {} : tool.state.input, - structured: tool.state.status === "running" ? tool.state.structured : {}, - content: tool.state.status === "running" ? tool.state.content : [], + // structured: tool.state.status === "running" ? tool.state.structured : {}, + metadata: event.data.metadata ?? (tool.state.status === "running" ? tool.state.metadata : {}), + content: event.data.content, error: event.data.error, - result: event.data.result, + // result: event.data.result, }, time: { ...tool.time, completed: event.created }, } diff --git a/packages/app/src/context/server-session.test.ts b/packages/app/src/context/server-session.test.ts index 1e1046fb76fd..e9595e137607 100644 --- a/packages/app/src/context/server-session.test.ts +++ b/packages/app/src/context/server-session.test.ts @@ -1,8 +1,11 @@ import { describe, expect, test } from "bun:test" import type { retry } from "@opencode-ai/core/util/retry" -import type { MessageApi, OpenCodeEvent, SessionApi } from "@opencode-ai/client/promise" +import type { OpenCodeEvent, SessionApi } from "@opencode-ai/client/promise" import type { Message, OpencodeClient, Part, Session } from "@opencode-ai/sdk/v2/client" import { createServerSession } from "./server-session" +import type { ServerApi } from "@/utils/server" + +type MessageApi = ServerApi["message"] const session = (id: string, parentID?: string): Session => ({ id, diff --git a/packages/app/src/context/server-session.ts b/packages/app/src/context/server-session.ts index e8f91cda3f6f..69bec61ba2fc 100644 --- a/packages/app/src/context/server-session.ts +++ b/packages/app/src/context/server-session.ts @@ -1,6 +1,6 @@ import { Binary } from "@opencode-ai/core/util/binary" import { retry } from "@opencode-ai/core/util/retry" -import type { MessageApi, OpenCodeEvent, SessionApi, SessionMessageInfo } from "@opencode-ai/client/promise" +import type { OpenCodeEvent, SessionApi, SessionMessageInfo } from "@opencode-ai/client/promise" import type { Message, OpencodeClient, @@ -21,6 +21,9 @@ import { normalizeSessionInfo } from "@/utils/session" import { normalizeSessionMessages } from "@/utils/session-message" import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache" import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer" +import type { ServerApi } from "@/utils/server" + +type MessageApi = ServerApi["message"] const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id) @@ -954,10 +957,10 @@ export function createServerSession( }) if (event.type === "session.usage.updated" && info) remember({ ...info, cost: event.data.cost, tokens: event.data.tokens }) - if (event.type === "session.archived") { - if (info) remember({ ...info, time: { ...info.time, archived: event.created, updated: event.created } }) - evict([sessionID]) - } + // if (event.type === "session.archived") { + // if (info) remember({ ...info, time: { ...info.time, archived: event.created, updated: event.created } }) + // evict([sessionID]) + // } if (event.type === "session.execution.started") setData("session_status", sessionID, { type: "busy" }) if ( event.type === "session.execution.succeeded" || diff --git a/packages/app/src/context/server-sync.test.ts b/packages/app/src/context/server-sync.test.ts index 9f625c94324a..7821a09e207f 100644 --- a/packages/app/src/context/server-sync.test.ts +++ b/packages/app/src/context/server-sync.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from "bun:test" import type { OpencodeClient } from "@opencode-ai/sdk/v2/client" import type { - McpApi, McpListInput, McpResourceCatalogInput, SessionApi, @@ -14,6 +13,9 @@ import { estimateRootSessionTotal, loadRootSessions } from "./global-sync/sessio import { loadActiveSessionsQuery, loadMcpQuery, loadMcpResourcesQuery, seedActiveSessionStatuses } from "./server-sync" import { ServerScope } from "@/utils/server-scope" import { createServerSession } from "./server-session" +import type { ServerApi } from "@/utils/server" + +type McpApi = ServerApi["mcp"] describe("MCP queries", () => { test("loads current servers for the requested location", async () => { diff --git a/packages/app/src/context/server-sync.tsx b/packages/app/src/context/server-sync.tsx index 109a7bf7d65c..196cda84794e 100644 --- a/packages/app/src/context/server-sync.tsx +++ b/packages/app/src/context/server-sync.tsx @@ -188,7 +188,8 @@ function makeQueryOptionsApi( projects: () => loadProjectsQuery(scope, serverAPI.project), providers: (directory: PathKey | null) => loadProvidersQuery(scope, directory, serverAPI, directory ? sdkFor(directory) : serverSDK(), protocol), - path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.path), + path: (directory: PathKey | null) => + loadPathQuery(scope, directory, directory ? sdkFor(directory) : serverSDK(), protocol), agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent, sdkFor(directory), protocol), references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference, sdkFor(directory), protocol), @@ -289,6 +290,10 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { const queryClient = useQueryClient() const homeSessions = createHomeSessionIndexCache(queryClient, ServerConnection.key(serverSDK.server)) + const refreshProviders = () => + queryClient.refetchQueries({ + predicate: (query) => query.queryKey[0] === serverSDK.scope && query.queryKey[2] === "providers", + }) let bootedAt = 0 let bootingRoot = false @@ -536,6 +541,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { homeSessions.apply(event) } homeSessions.refresh(event.type) + if (eventType === "integration.connection.updated") void refreshProviders() if (directory === "global") { if (eventType === "server.connected" && activeSessionsQuery.data === undefined && !activeSessionsQuery.isFetching) @@ -581,7 +587,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { children.mark(key) if ( event.current?.type === "session.moved" || - event.current?.type === "session.archived" || + // event.current?.type === "session.archived" || event.current?.type === "session.forked" || eventType === "command.updated" || eventType === "config.updated" || @@ -677,6 +683,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { peek: children.peek, disableMcp: children.disableMcp, queryOptions: queryOptionsApi, + refreshProviders, // bootstrap, updateConfig: updateConfigMutation.mutateAsync, project: projectApi, diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 982c26ca9712..60ad01c86743 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -2,7 +2,7 @@ import { useServerSync } from "@/context/server-sync" import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" import { Iterable, pipe } from "effect" -import type { Accessor } from "solid-js" +import { createEffect, createMemo, type Accessor } from "solid-js" import { selectProviderCatalog } from "./provider-catalog" export const popularProviders = [ @@ -17,14 +17,14 @@ export const popularProviders = [ ] const popularProviderSet = new Set(popularProviders) -export function useProviders(directory?: Accessor) { +export function useProviders(directory: Accessor) { const serverSync = useServerSync() const params = useParams() const dir = () => (directory ? directory() : decode64(params.dir)) const providers = () => { const value = dir() const projectStore = value ? serverSync().child(value)[0] : undefined - if (directory) + if (value) return selectProviderCatalog({ explicit: true, directory: value, @@ -37,6 +37,7 @@ export function useProviders(directory?: Accessor) { global: serverSync().data.provider, }) } + return { all: () => providers().all, default: () => providers().default, @@ -58,7 +59,7 @@ export function useProviders(directory?: Accessor) { }, paid: () => { const connected = new Set(providers().connected) - return [ + const paid = [ ...Iterable.filter( providers().all, ([id]) => @@ -66,6 +67,7 @@ export function useProviders(directory?: Accessor) { (id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)), ), ] + return paid }, } } diff --git a/packages/app/src/pages/home/home-projects-view.tsx b/packages/app/src/pages/home/home-projects-view.tsx index 573b606579ad..7ec01bd75feb 100644 --- a/packages/app/src/pages/home/home-projects-view.tsx +++ b/packages/app/src/pages/home/home-projects-view.tsx @@ -360,25 +360,26 @@ function HomeProjectSlot( index: () => number }, ) { - const project = createMemo(() => props.items.find((item) => item.worktree === props.worktree)) + const initial = props.items.find((item) => item.worktree === props.worktree) + if (!initial) return + const project = createMemo( + (previous) => props.items.find((item) => item.worktree === props.worktree) ?? previous, + initial, + ) return ( - - {(item) => ( - - )} - + ) } diff --git a/packages/app/src/pages/home/home-sessions-controller.tsx b/packages/app/src/pages/home/home-sessions-controller.tsx index 06d86c30c9ed..a12e9800e9fe 100644 --- a/packages/app/src/pages/home/home-sessions-controller.tsx +++ b/packages/app/src/pages/home/home-sessions-controller.tsx @@ -211,10 +211,16 @@ export function createHomeSessionsController(home: HomeController) { const ctx = home.server.focusedContext() if (!conn || !ctx) return const [, setStore] = ctx.sync.child(session.directory) + if ((await ctx.sdk.protocol) !== "v1") return await archiveHomeSession({ server: ServerConnection.key(conn), session, - archive: (sessionID) => ctx.sdk.api.session.archive({ sessionID, directory: session.directory }), + archive: (sessionID) => + ctx.sdk.client.session.update({ + sessionID, + directory: session.directory, + time: { archived: Date.now() }, + }), remove: () => setStore( produce((draft) => { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 59474423184a..b348b44d8a87 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -119,7 +119,7 @@ export default function LegacyLayout(props: ParentProps) { const permission = usePermission() const navigate = useNavigate() setNavigate(navigate) - const providers = useProviders() + const providers = useProviders(() => undefined) const dialog = useDialog() const command = useCommand() const theme = useTheme() @@ -872,12 +872,17 @@ export default function LegacyLayout(props: ParentProps) { } async function archiveSession(session: Session) { + if ((await serverSDK().protocol) !== "v1") return const [store, setStore] = serverSync().child(session.directory) const sessions = store.session ?? [] const index = sessions.findIndex((s) => s.id === session.id) const nextSession = sessions[index + 1] ?? sessions[index - 1] - await serverSDK().api.session.archive({ sessionID: session.id, directory: session.directory }) + await serverSDK().client.session.update({ + sessionID: session.id, + directory: session.directory, + time: { archived: Date.now() }, + }) setStore( produce((draft) => { const match = Binary.search(draft.session, session.id, (s) => s.id) @@ -1296,7 +1301,13 @@ export default function LegacyLayout(props: ParentProps) { const name = next === getFilename(project.worktree) ? "" : next if (project.id && project.id !== "global") { - const result = await serverSDK().api.project.update({ projectID: project.id, name }) + const sdk = serverSDK() + if ((await sdk.protocol) !== "v1") return + const result = await sdk.client.project + .update({ projectID: project.id, directory: project.worktree, name }) + .then((response) => response.data) + if (!result) return + // const result = await serverSDK().api.project.update({ projectID: project.id, name }) serverSync().set("project", (items) => items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)), ) @@ -1479,15 +1490,20 @@ export default function LegacyLayout(props: ParentProps) { return } - await Promise.all( - sessions - .filter((session) => session.time.archived === undefined) - .map((session) => - serverSDK() - .api.session.archive({ sessionID: session.id, directory: session.directory }) - .catch(() => undefined), - ), - ) + if ((await serverSDK().protocol) === "v1") + await Promise.all( + sessions + .filter((session) => session.time.archived === undefined) + .map((session) => + serverSDK() + .client.session.update({ + sessionID: session.id, + directory: session.directory, + time: { archived: Date.now() }, + }) + .catch(() => undefined), + ), + ) setBusy(directory, false) dismiss() diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx index d97f5779ec19..570f04463e70 100644 --- a/packages/app/src/pages/new-session.tsx +++ b/packages/app/src/pages/new-session.tsx @@ -126,6 +126,14 @@ export default function NewSessionPage() { keybind: "ctrl+l", onSelect: () => promptInputV2Controller.restoreFocus(), }, + { + id: "project.select", + title: language.t("session.new.project.search"), + category: language.t("command.category.project"), + keybind: "mod+shift+o", + disabled: projectController.empty(), + onSelect: () => projectController.setOpen(true), + }, ]) createEffect(() => { diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index d4dd6ff7efbb..c6e3a5fcbb71 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -6,6 +6,7 @@ import { batch, ErrorBoundary, onCleanup, + Suspense, Show, Match, Switch, @@ -2217,6 +2218,10 @@ export default function Page() { comments.clear() resumeScroll() }, + get edit() { + return editingFollowup() + }, + onEditLoaded: clearFollowupEdit, shouldQueue: queueEnabled, onQueue: queueFollowup, onAbort: () => { @@ -2225,14 +2230,7 @@ export default function Page() { setFollowup("paused", id, true) }, }) - return ( - - ) + return }} } @@ -2300,49 +2298,53 @@ export default function Page() {
- + + +
- hasReview() || reviewV2State.sidebarOpened()} - reviewCount={reviewCount} - reviewPanel={reviewPanelV2} - reviewSidebarToggle={(disabled) => ( - - )} - fileBrowserState={reviewV2State} - activeDiff={activeReviewFile()} - focusReviewDiff={focusReviewDiff} - reviewSnap={ui.reviewSnap} - size={size} - stacked={desktopV2PanelLayout().stacked} - /> + + hasReview() || reviewV2State.sidebarOpened()} + reviewCount={reviewCount} + reviewPanel={reviewPanelV2} + reviewSidebarToggle={(disabled) => ( + + )} + fileBrowserState={reviewV2State} + activeDiff={activeReviewFile()} + focusReviewDiff={focusReviewDiff} + reviewSnap={ui.reviewSnap} + size={size} + stacked={desktopV2PanelLayout().stacked} + /> +
diff --git a/packages/app/src/pages/session/composer/session-composer-controls.ts b/packages/app/src/pages/session/composer/session-composer-controls.ts index 4ae7827e210a..a9b0070bc025 100644 --- a/packages/app/src/pages/session/composer/session-composer-controls.ts +++ b/packages/app/src/pages/session/composer/session-composer-controls.ts @@ -25,35 +25,39 @@ export function createPromptInputController(input: { }) { const layout = useLayout() const local = useLocal() - const providers = useProviders() - const sync = useSync() const sdk = useSDK() + const sync = useSync() + const providers = useProviders(() => sdk().directory) const view = layout.view(input.sessionKey) const agentsQuery = createQuery(() => input.queryOptions.agents(pathKey(sdk().directory))) const globalProvidersQuery = createQuery(() => input.queryOptions.providers(null)) const providersQuery = createQuery(() => input.queryOptions.providers(pathKey(sdk().directory))) - return createMemo(() => ({ - agents: { - available: sync().data.agent, - options: local.agent.list().map((agent) => agent.name), - current: local.agent.current()?.name ?? "", - loading: agentsQuery.isLoading, - visible: local.agent.visible(), - select: local.agent.set, - }, - model: { - selection: input.model ?? local.model, - paid: providers.paid().length > 0, - loading: - (local.agent.visible() && agentsQuery.isLoading) || providersQuery.isLoading || globalProvidersQuery.isLoading, - }, - session: { - id: input.sessionID(), - tabs: layout.tabs(input.sessionKey), - reviewPanel: view.reviewPanel, - }, - })) + return createMemo(() => { + return { + agents: { + available: sync().data.agent, + options: local.agent.list().map((agent) => agent.name), + current: local.agent.current()?.name ?? "", + loading: agentsQuery.isLoading, + visible: local.agent.visible(), + select: local.agent.set, + }, + model: { + selection: input.model ?? local.model, + paid: providers.paid().length > 0, + loading: + (local.agent.visible() && agentsQuery.isLoading) || + providersQuery.isLoading || + globalProvidersQuery.isLoading, + }, + session: { + id: input.sessionID(), + tabs: layout.tabs(input.sessionKey), + reviewPanel: view.reviewPanel, + }, + } + }) } export function createPromptProjectControls() { diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 48b432667e17..dc9153536cf2 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -812,13 +812,14 @@ export function MessageTimeline(props: { const archiveSession = async (sessionID: string) => { const session = sync().session.get(sessionID) if (!session) return + if ((await sdk().protocol) !== "v1") return const sessions = sync().data.session ?? [] const index = sessions.findIndex((s) => s.id === sessionID) const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) await sdk() - .api.session.archive({ sessionID }) + .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } }) .then(() => { sync().set( produce((draft) => { diff --git a/packages/app/src/utils/menu-dismiss-controller.ts b/packages/app/src/utils/menu-dismiss-controller.ts new file mode 100644 index 000000000000..0a3009eb716f --- /dev/null +++ b/packages/app/src/utils/menu-dismiss-controller.ts @@ -0,0 +1,30 @@ +/** Coordinates focus restoration and actions that must run after menu content unmounts. */ +export function createMenuDismissController(content: () => HTMLElement | undefined) { + let restoreTrigger = true + + return { + /** Allows the menu primitive to restore focus to its trigger when closing. */ + allowTriggerRestore() { + restoreTrigger = true + }, + /** Keeps focus at its current or next destination instead of returning it to the trigger. */ + preventTriggerRestore() { + restoreTrigger = false + }, + /** Applies the current restoration policy during the menu primitive's close-focus event. */ + onCloseAutoFocus(event: Event) { + if (!restoreTrigger) event.preventDefault() + }, + /** Runs an action after the menu unmounts and its focus-close work has settled. */ + afterClose(callback: () => void) { + const complete = () => { + if (content()?.isConnected) { + requestAnimationFrame(complete) + return + } + requestAnimationFrame(() => requestAnimationFrame(callback)) + } + requestAnimationFrame(complete) + }, + } +} diff --git a/packages/app/src/utils/server-compat.test.ts b/packages/app/src/utils/server-compat.test.ts index 3f4b8f2205cd..52e5ec6e3bee 100644 --- a/packages/app/src/utils/server-compat.test.ts +++ b/packages/app/src/utils/server-compat.test.ts @@ -53,6 +53,7 @@ function setup( } describe("createCompatibleApi", () => { + /* test("routes V1 archive through the legacy session update", async () => { const { api, requests } = setup("v1") await api.session.archive({ sessionID: "ses_1", directory: "/repo" }) @@ -63,6 +64,7 @@ describe("createCompatibleApi", () => { expect(requests[0]!.method).toBe("PATCH") expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } }) }) + */ test("converts current prompts to the V1 prompt contract", async () => { const { api, requests } = setup("v1") @@ -127,14 +129,6 @@ describe("createCompatibleApi", () => { ]) }) - test("keeps V2 session actions on the current API", async () => { - const { api, requests } = setup("v2") - await api.session.archive({ sessionID: "ses_1" }) - - expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive") - expect(requests[0]!.method).toBe("POST") - }) - test("resolves protocol detection once across implementation methods", async () => { let detections = 0 const resolved = Promise.resolve<"v1" | "v2">("v2") @@ -147,12 +141,22 @@ describe("createCompatibleApi", () => { }) const { api } = setup(protocol) - await api.session.archive({ sessionID: "ses_1" }) + await api.session.list() await api.session.list() expect(detections).toBe(1) }) + /* + test("keeps V2 session actions on the current API", async () => { + const { api, requests } = setup("v2") + await api.session.archive({ sessionID: "ses_1" }) + + expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive") + expect(requests[0]!.method).toBe("POST") + }) + */ + test("uses the global V1 session search endpoint", async () => { const { api, requests } = setup("v1") await api.session.list({ parentID: null, search: "session", limit: 50 }) @@ -160,6 +164,7 @@ describe("createCompatibleApi", () => { expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session") }) + /* test("projects the V1 default branch", async () => { const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } }) @@ -167,6 +172,7 @@ describe("createCompatibleApi", () => { data: { branch: "feature", defaultBranch: "dev" }, }) }) + */ test("translates current file searches to the V1 dirs parameter", async () => { const { api, requests } = setup("v1") @@ -190,4 +196,41 @@ describe("createCompatibleApi", () => { expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/permissions/permission_1") expect(new URL(requests[0]!.url).searchParams.get("directory")).toBe("/other") }) + + test("disposes the V1 instance after connecting a provider", async () => { + const { api, requests } = setup("v1") + + await api.integration.connect.key({ + integrationID: "openrouter", + key: "secret", + location: { directory: "/repo" }, + }) + + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/auth/openrouter", + "/instance/dispose", + "/instance/dispose", + ]) + expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() + }) + + test("disposes the V1 instance after completing provider OAuth", async () => { + const { api, requests } = setup("v1") + + await api.integration.oauth.complete({ + integrationID: "openrouter", + attemptID: "openrouter:0", + code: "code", + location: { directory: "/repo" }, + }) + + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/provider/openrouter/oauth/callback", + "/instance/dispose", + "/instance/dispose", + ]) + expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() + }) }) diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts index 72e74438edca..1df1338b71e5 100644 --- a/packages/app/src/utils/server-compat.ts +++ b/packages/app/src/utils/server-compat.ts @@ -27,7 +27,7 @@ type CompatibleSessionApi = Omit< shell: (input: SessionShellInput & LegacyPrompt) => Promise compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise rename: (input: Parameters[0] & LegacyLocation) => ReturnType - archive: (input: Parameters[0] & LegacyLocation) => ReturnType + // archive: (input: Parameters[0] & LegacyLocation) => ReturnType remove: (input: Parameters[0] & LegacyLocation) => ReturnType } type CompatiblePermissionApi = Omit & { @@ -183,9 +183,9 @@ function createV1Api(input: CompatibleInput): CompatibleApi { async rename(value: Parameters[0] & LegacyLocation) { await legacy(value).session.update({ sessionID: value.sessionID, title: value.title }) }, - async archive(value: Parameters[0] & LegacyLocation) { - await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) - }, + // async archive(value: Parameters[0] & LegacyLocation) { + // await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) + // }, async remove(value: Parameters[0] & LegacyLocation) { await legacy(value).session.delete(value) }, @@ -308,34 +308,34 @@ function createV1Api(input: CompatibleInput): CompatibleApi { if (!result.data) throw new Error("Project not found") return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent }, - async update(value: Parameters[0]) { - const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) - const result = await legacy({ directory: project?.worktree }).project.update({ - ...value, - directory: project?.worktree, - }) - if (!result.data) throw new Error(`Project not found: ${value.projectID}`) - return result.data as Project - }, + // async update(value: Parameters[0]) { + // const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) + // const result = await legacy({ directory: project?.worktree }).project.update({ + // ...value, + // directory: project?.worktree, + // }) + // if (!result.data) throw new Error(`Project not found: ${value.projectID}`) + // return result.data as Project + // }, async directories(value: Parameters[0]) { const result = await legacy(value.location).worktree.list() return (result.data ?? []).map((item) => ({ directory: item })) }, }, - path: { - ...input.current.path, - async get(value?: Parameters[0]) { - const result = await legacy(value?.location).path.get() - if (!result.data) throw new Error("Path unavailable") - return result.data - }, - }, + // path: { + // ...input.current.path, + // async get(value?: Parameters[0]) { + // const result = await legacy(value?.location).path.get() + // if (!result.data) throw new Error("Path unavailable") + // return result.data + // }, + // }, vcs: { ...input.current.vcs, - async get(value?: Parameters[0]) { - const result = await legacy(value?.location).vcs.get() - return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location) - }, + // async get(value?: Parameters[0]) { + // const result = await legacy(value?.location).vcs.get() + // return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location) + // }, async status(value?: Parameters[0]) { const result = await legacy(value?.location).vcs.status() return located(result.data ?? [], value?.location) @@ -401,6 +401,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { providerID: value.integrationID, auth: { type: "api", key: value.key }, }) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() }, }, oauth: { @@ -429,6 +431,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { { providerID: value.integrationID, method, code: value.code }, { throwOnError: true }, ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() }, status: async (value: Parameters[0]) => { const method = Number(value.attemptID.split(":").at(-1)) @@ -436,6 +440,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { { providerID: value.integrationID, method }, { throwOnError: true }, ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() return located( { status: "complete" as const, time: { created: Date.now(), expires: Date.now() } }, value.location, @@ -445,9 +451,9 @@ function createV1Api(input: CompatibleInput): CompatibleApi { }, pty: { ...input.current.pty, - async shells(value?: Parameters[0]) { - return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) - }, + // async shells(value?: Parameters[0]) { + // return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) + // }, async list(value?: Parameters[0]) { return located((await legacy(value?.location).pty.list()).data ?? [], value?.location) }, @@ -479,11 +485,11 @@ function createV1Api(input: CompatibleInput): CompatibleApi { async remove(value: Parameters[0]) { await legacy(value.location).pty.remove({ ptyID: value.ptyID }) }, - async connectToken(value: Parameters[0]) { - const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) - if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) - return located(result.data, value.location) - }, + // async connectToken(value: Parameters[0]) { + // const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) + // if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) + // return located(result.data, value.location) + // }, }, permission: { ...input.current.permission, diff --git a/packages/app/src/utils/session-message.test.ts b/packages/app/src/utils/session-message.test.ts index d998c0c0459e..a69c414e1686 100644 --- a/packages/app/src/utils/session-message.test.ts +++ b/packages/app/src/utils/session-message.test.ts @@ -49,7 +49,7 @@ describe("normalizeSessionMessages", () => { state: { status: "completed", input: { filePath: "note.txt" }, - structured: { title: "note.txt" }, + metadata: { title: "note.txt" }, content: [{ type: "text", text: "hello" }], }, time: { created: 5, ran: 6, completed: 7 }, @@ -170,7 +170,7 @@ describe("normalizeSessionMessages", () => { status: "completed", input: { path: "/repo/README.md", oldString: "old", newString: "new" }, content: [{ type: "text", text: "Edited file successfully" }], - structured: { + metadata: { files: [ { file: "README.md", diff --git a/packages/app/src/utils/session-message.ts b/packages/app/src/utils/session-message.ts index c67c6c717c2a..93d86a66bb25 100644 --- a/packages/app/src/utils/session-message.ts +++ b/packages/app/src/utils/session-message.ts @@ -304,7 +304,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi return { status: "running" as const, input: normalizeToolInput(tool.name, tool.state.input), - metadata: normalizeToolMetadata(tool.name, tool.state.structured), + // metadata: normalizeToolMetadata(tool.name, tool.state.structured), + metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), time: { start }, } } @@ -313,7 +314,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi status: "error" as const, input: normalizeToolInput(tool.name, tool.state.input), error: tool.state.error.message, - metadata: normalizeToolMetadata(tool.name, tool.state.structured), + // metadata: normalizeToolMetadata(tool.name, tool.state.structured), + metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), time: { start, end: tool.time.completed ?? start }, } } @@ -337,7 +339,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi input: normalizeToolInput(tool.name, tool.state.input), output: tool.state.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n"), title: tool.name, - metadata: normalizeToolMetadata(tool.name, tool.state.structured), + // metadata: normalizeToolMetadata(tool.name, tool.state.structured), + metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), time: { start, end: tool.time.completed ?? start }, attachments: attachments.length ? attachments : undefined, } diff --git a/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz b/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz new file mode 100644 index 000000000000..bc1b664f82dc Binary files /dev/null and b/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz differ diff --git a/packages/app/vendor/opencode-ai-client-1.17.13.tgz b/packages/app/vendor/opencode-ai-client-1.17.13.tgz deleted file mode 100644 index 5939f2cb39c2..000000000000 Binary files a/packages/app/vendor/opencode-ai-client-1.17.13.tgz and /dev/null differ diff --git a/packages/cli/package.json b/packages/cli/package.json index 26a621028110..c93fa6625533 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.18.5", + "version": "1.18.7", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 15f3910e27e1..acdaca0ec552 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.18.5", + "version": "1.18.7", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 6047829b07d6..9fe1ce440064 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.18.5", + "version": "1.18.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/src/routes/brand/index.css b/packages/console/app/src/routes/brand/index.css index 8a326515911a..0e39f3742f26 100644 --- a/packages/console/app/src/routes/brand/index.css +++ b/packages/console/app/src/routes/brand/index.css @@ -376,7 +376,6 @@ align-items: center; gap: 16px; opacity: 0; - transition: opacity 0.2s ease; @media (max-width: 40rem) { position: static; diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 62d1d53da85b..906fe302d73a 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.18.5", + "version": "1.18.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 5909638d05d5..9101f0ea4635 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.18.5", + "version": "1.18.7", "$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 2abd08333c24..ecdc69acbeac 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.18.5", + "version": "1.18.7", "dependencies": { "@jsx-email/all": "2.2.3", "@tsconfig/bun": "1.0.9", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index f46ebcea7729..96a615c269a8 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.18.5", + "version": "1.18.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 060c369af664..e615a23e21e5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.5", + "version": "1.18.7", "name": "@opencode-ai/core", "type": "module", "license": "MIT", @@ -108,7 +108,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.11.1", + "gitlab-ai-provider": "6.12.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", diff --git a/packages/core/src/plugin/skill/customize-opencode.md b/packages/core/src/plugin/skill/customize-opencode.md index 6932dbfd54cc..549f15e22791 100644 --- a/packages/core/src/plugin/skill/customize-opencode.md +++ b/packages/core/src/plugin/skill/customize-opencode.md @@ -112,7 +112,7 @@ Every field is optional. "type": "local", "command": ["npx", "-y", "@playwright/mcp"], "enabled": true, - "env": {} + "environment": {} }, "remote-thing": { "type": "remote", @@ -371,7 +371,7 @@ Special object-shaped (not callbacks): `tool: { my_tool: { ... } }`, "type": "local", "command": ["npx", "-y", "@playwright/mcp"], "enabled": true, - "env": { "BROWSER": "chromium" } + "environment": { "BROWSER": "chromium" } }, "github": { "type": "remote", @@ -384,7 +384,8 @@ Special object-shaped (not callbacks): `tool: { my_tool: { ... } }`, } ``` -`command` is an array of strings. `type` is required. Use `enabled: false` to +`command` is an array of strings. `environment` sets environment variables for +a local MCP server. `type` is required. Use `enabled: false` to disable a server inherited from a parent config. String values such as header tokens support `{env:VAR}` interpolation (and `{file:path}`); the shell-style `${VAR}` is not substituted. diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts index 5303dbd955c1..1e1ab9d20f75 100644 --- a/packages/core/src/reference.ts +++ b/packages/core/src/reference.ts @@ -58,7 +58,6 @@ const layer = Layer.effect( finalize: (draft) => Effect.gen(function* () { materialized.clear() - const seen = new Map() for (const [name, source] of draft.list()) { if (source.type === "local") { materialized.set( @@ -82,14 +81,11 @@ const layer = Layer.effect( continue } } - const target = Repository.cachePath(global.repos, repository) - if (seen.has(target) && seen.get(target) !== source.branch) continue - seen.set(target, source.branch) materialized.set( name, new Info({ name, - path: AbsolutePath.make(target), + path: AbsolutePath.make(Repository.cachePath(global.repos, repository, source.branch)), ...(source.description === undefined ? {} : { description: source.description }), ...(source.hidden === undefined ? {} : { hidden: source.hidden }), source, diff --git a/packages/core/src/repository-cache.ts b/packages/core/src/repository-cache.ts index 988236743b39..cab2f631ffb8 100644 --- a/packages/core/src/repository-cache.ts +++ b/packages/core/src/repository-cache.ts @@ -1,3 +1,10 @@ +/** + * Local tracking checkouts for remote Git references, one per remote and + * branch. Each checkout permanently tracks a single ref: the requested branch + * when the cache key has one, otherwise the remote default branch. Content + * follows "newest wins": refresh fetches and hard-resets, so readers may + * observe the checkout move underneath them. + */ import path from "path" import { Context, Effect, Layer, Schema } from "effect" import { FSUtil } from "./fs-util" @@ -135,7 +142,7 @@ const layer: Layer.Layer new FetchFailedError({ repository, message: error.message }))) if (input.branch) { - const requestedBranch = input.branch yield* git.sync - .fetchBranch(existing, { branch: requestedBranch }) + .fetchBranch(existing, { branch: input.branch }) .pipe(Effect.mapError((error) => new FetchFailedError({ repository, message: error.message }))) + } - yield* git.sync.checkoutRemoteBranch(existing, { branch: requestedBranch }).pipe( - Effect.mapError( - (error) => - new CheckoutFailedError({ - repository, - branch: requestedBranch, - message: error.message, - }), - ), - ) + // Checking out the tracked ref before resetting keeps the + // checkout self-healing even if it was left on another + // branch. + const branch = input.branch ?? (yield* git.history.defaultRemoteBranch(existing)) + if (branch) { + yield* git.sync + .checkoutRemoteBranch(existing, { branch }) + .pipe( + Effect.mapError( + (error) => new CheckoutFailedError({ repository, branch, message: error.message }), + ), + ) } + const target = branch ?? (yield* git.history.branch(existing)) yield* git.sync - .resetHard(existing, yield* resetTarget(git, existing, input.branch)) + .resetHard(existing, target ? `origin/${target}` : "HEAD") .pipe(Effect.mapError((error) => new ResetFailedError({ repository, message: error.message }))) } @@ -229,12 +246,6 @@ export const node = makeGlobalNode({ deps: [EffectFlock.node, FSUtil.node, Git.node, Global.node], }) -function statusForRepository(input: { reuse: boolean; refresh?: boolean; branchMatches?: boolean }) { - if (!input.reuse) return "cloned" as const - if (input.branchMatches === false || input.refresh) return "refreshed" as const - return "cached" as const -} - function errorMessage(error: unknown) { return error instanceof globalThis.Error ? error.message : String(error) } @@ -245,17 +256,4 @@ function cacheOperation(effect: Effect.Effect, operation: stri ) } -const resetTarget = Effect.fnUntraced(function* ( - git: Git.Interface, - repository: Git.Repository, - requestedBranch?: string, -) { - if (requestedBranch) return `origin/${requestedBranch}` - const remoteHead = yield* git.history.defaultRemoteBranch(repository) - if (remoteHead) return `origin/${remoteHead}` - const currentBranch = yield* git.history.branch(repository) - if (currentBranch) return `origin/${currentBranch}` - return "HEAD" -}) - export * as RepositoryCache from "./repository-cache" diff --git a/packages/core/src/repository.ts b/packages/core/src/repository.ts index dbc6a8fbcae6..8ee5be600e3a 100644 --- a/packages/core/src/repository.ts +++ b/packages/core/src/repository.ts @@ -118,8 +118,14 @@ export function isRemote(reference: Reference): reference is RemoteReference { return !isFile(reference) } -export function cachePath(root: string, reference: Reference): string { - return path.join(root, ...reference.host.split(":"), ...reference.segments) +/** + * Checkouts are keyed by remote and branch: a branch-specific reference gets + * its own directory so branchless refreshes can never move it. The branch is + * percent-encoded because valid branch names may contain `/`. + */ +export function cachePath(root: string, reference: Reference, branch?: string): string { + const base = path.join(root, ...reference.host.split(":"), ...reference.segments) + return branch ? `${base}@${encodeURIComponent(branch)}` : base } export function cacheIdentity(reference: Reference): string { diff --git a/packages/core/test/reference.test.ts b/packages/core/test/reference.test.ts index db61232310ed..5ab215230acf 100644 --- a/packages/core/test/reference.test.ts +++ b/packages/core/test/reference.test.ts @@ -47,7 +47,7 @@ describe("Reference", () => { expect(yield* references.list()).toEqual([ new Reference.Info({ name: "sdk", - path: AbsolutePath.make(Repository.cachePath(Global.Path.repos, repository)), + path: AbsolutePath.make(Repository.cachePath(Global.Path.repos, repository, "main")), source, }), ]) diff --git a/packages/core/test/repository-cache.test.ts b/packages/core/test/repository-cache.test.ts index 2dd0ce250288..4433583bf787 100644 --- a/packages/core/test/repository-cache.test.ts +++ b/packages/core/test/repository-cache.test.ts @@ -8,7 +8,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Global } from "@opencode-ai/core/global" import { Repository } from "@opencode-ai/core/repository" import { RepositoryCache } from "@opencode-ai/core/repository-cache" -import { git, gitRemote } from "./fixture/git" +import { branch, git, gitRemote } from "./fixture/git" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" @@ -66,6 +66,41 @@ describe("RepositoryCache", () => { ), ) + it.live("keeps branch checkouts isolated from branchless refreshes", () => + withRemote((fixture) => + Effect.gen(function* () { + yield* Effect.promise(() => branch(fixture.source, "feature", "two\n")) + const cache = yield* RepositoryCache.Service + + const featured = yield* cache.ensure({ reference: fixture.reference, branch: "feature" }) + expect(featured.branch).toBe("feature") + expect(featured.localPath.endsWith("repo@feature")).toBe(true) + expect(yield* read(path.join(featured.localPath, "README.md"))).toBe("two\n") + + const refreshed = yield* cache.ensure({ reference: fixture.reference, refresh: true }) + expect(refreshed.localPath).not.toBe(featured.localPath) + expect(yield* read(path.join(refreshed.localPath, "README.md"))).toBe("one\n") + + const cached = yield* cache.ensure({ reference: fixture.reference, branch: "feature" }) + expect(cached.status).toBe("cached") + expect(yield* read(path.join(cached.localPath, "README.md"))).toBe("two\n") + }).pipe(Effect.provide(cacheLayer(fixture.root))), + ), + ) + + it.live("does not mistake an enclosing repository for the cache checkout", () => + withRemote((fixture) => + Effect.gen(function* () { + yield* Effect.promise(() => git(fixture.root, "clone", fixture.remote, path.join(fixture.root, "repos"))) + + const result = yield* (yield* RepositoryCache.Service).ensure({ reference: fixture.reference }) + + expect(result.status).toBe("cloned") + expect(yield* read(path.join(result.localPath, "README.md"))).toBe("one\n") + }).pipe(Effect.provide(cacheLayer(fixture.root))), + ), + ) + it.live("returns typed validation and clone failures", () => withRemote((fixture) => Effect.gen(function* () { diff --git a/packages/core/test/repository.test.ts b/packages/core/test/repository.test.ts index 5b18f8b1d693..1a6af675d12b 100644 --- a/packages/core/test/repository.test.ts +++ b/packages/core/test/repository.test.ts @@ -17,6 +17,12 @@ describe("Repository", () => { label: "owner/repo", }) expect(Repository.cachePath("/cache", reference)).toBe(path.join("/cache", "github.com", "owner", "repo")) + expect(Repository.cachePath("/cache", reference, "main")).toBe( + path.join("/cache", "github.com", "owner", "repo@main"), + ) + expect(Repository.cachePath("/cache", reference, "feature/x")).toBe( + path.join("/cache", "github.com", "owner", "repo@feature%2Fx"), + ) expect(Repository.cacheIdentity(reference)).toBe("github.com/owner/repo") }) diff --git a/packages/desktop/electron-builder.config.test.ts b/packages/desktop/electron-builder.config.test.ts index f62ee65b34e2..762dc2226847 100644 --- a/packages/desktop/electron-builder.config.test.ts +++ b/packages/desktop/electron-builder.config.test.ts @@ -31,6 +31,8 @@ for (const channel of channels) { expect(config.protocols).toEqual({ name: identity.productName, schemes: [DESKTOP_PROTOCOL] }) expect(config.deb?.packageName).toBe(identity.packageName) expect(config.rpm?.packageName).toBe(identity.packageName) + expect(config.deb?.fpm).toContainEqual(expect.stringContaining(`/usr/share/metainfo/${identity.appId}.metainfo.xml`)) + expect(config.rpm?.fpm).toContainEqual(expect.stringContaining(`/usr/share/metainfo/${identity.appId}.metainfo.xml`)) expect(config.extraResources).toContainEqual({ from: "resources/icons/", to: "icons/", diff --git a/packages/desktop/electron-builder.config.ts b/packages/desktop/electron-builder.config.ts index 78e51ca09d55..ca18da3af3f8 100644 --- a/packages/desktop/electron-builder.config.ts +++ b/packages/desktop/electron-builder.config.ts @@ -11,6 +11,9 @@ const packageDir = path.dirname(fileURLToPath(import.meta.url)) const rootDir = path.resolve(packageDir, "../..") const signScript = path.join(rootDir, "script", "sign-windows.ps1") +const metainfoFpm = (appId: string) => + `${path.join(packageDir, "resources", `${appId}.metainfo.xml`)}=/usr/share/metainfo/${appId}.metainfo.xml` + async function signWindows(configuration: { path: string }) { if (process.platform !== "win32") return if (process.env.GITHUB_ACTIONS !== "true") return @@ -111,8 +114,8 @@ function getConfig() { ...base, appId: identity.appId, productName: identity.productName, - deb: { packageName: identity.packageName }, - rpm: { packageName: identity.packageName }, + deb: { packageName: identity.packageName, fpm: [metainfoFpm(identity.appId)] }, + rpm: { packageName: identity.packageName, fpm: [metainfoFpm(identity.appId)] }, } } case "beta": { @@ -120,8 +123,8 @@ function getConfig() { ...base, appId: identity.appId, productName: identity.productName, - deb: { packageName: identity.packageName }, - rpm: { packageName: identity.packageName }, + deb: { packageName: identity.packageName, fpm: [metainfoFpm(identity.appId)] }, + rpm: { packageName: identity.packageName, fpm: [metainfoFpm(identity.appId)] }, } } case "prod": { @@ -130,8 +133,8 @@ function getConfig() { appId: identity.appId, productName: identity.productName, publish: { provider: "github", ...DESKTOP_RELEASE_REPOSITORY, channel: "latest" }, - deb: { packageName: identity.packageName }, - rpm: { packageName: identity.packageName }, + deb: { packageName: identity.packageName, fpm: [metainfoFpm(identity.appId)] }, + rpm: { packageName: identity.packageName, fpm: [metainfoFpm(identity.appId)] }, } } } diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 3c34e9fd247d..9690b82b15e5 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.18.5", + "version": "1.18.7", "type": "module", "license": "MIT", "homepage": "https://github.com/LogicLyra/opencode-classic", diff --git a/packages/desktop/scripts/predev.ts b/packages/desktop/scripts/predev.ts index 37c31d7eedb7..4a9682306de1 100644 --- a/packages/desktop/scripts/predev.ts +++ b/packages/desktop/scripts/predev.ts @@ -1,5 +1,7 @@ import { $ } from "bun" +await $`bun run install-electron` + await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}` await $`cd ../opencode && bun script/build-node.ts` diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index c51c924e35bf..57a3697913ba 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -233,6 +233,11 @@ export function registerIpcHandlers(deps: Deps) { return win?.isFocused() ?? false }) + handleTrusted("get-window-fullscreen", (event: IpcMainInvokeEvent) => { + const win = BrowserWindow.fromWebContents(event.sender) + return win?.isFullScreen() ?? false + }) + handleTrusted("set-window-focus", (event: IpcMainInvokeEvent) => { const win = BrowserWindow.fromWebContents(event.sender) win?.focus() diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index 56c748f63ee9..7e9f8db5cc21 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -211,7 +211,6 @@ export async function checkHealth(url: string, password?: string | null): Promis function createSidecarEnv(userDataPath: string): Record { const env = createIsolatedSidecarEnv(userDataPath) - if (!app.isPackaged) env.OPENCODE_DISABLE_CHANNEL_DB = "1" return env } diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts index 18d803004176..307be44ebf1e 100644 --- a/packages/desktop/src/main/windows.ts +++ b/packages/desktop/src/main/windows.ts @@ -219,6 +219,7 @@ export function createMainWindow(id: string = randomUUID()) { state.manage(win) registerWindow(win, id) + wireFullscreen(win) loadWindow(win, "index.html") wireZoom(win) @@ -481,6 +482,16 @@ function wireZoom(win: BrowserWindow) { }) } +function wireFullscreen(win: BrowserWindow) { + const send = (fullscreen: boolean) => { + if (win.isDestroyed() || win.webContents.isDestroyed()) return + win.webContents.send("window-fullscreen-changed", fullscreen) + } + + win.on("enter-full-screen", () => send(true)) + win.on("leave-full-screen", () => send(false)) +} + function clampZoom(value: number) { return Math.min(Math.max(value, minZoomLevel), maxZoomLevel) } diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index 87eac1fa805e..1a0056dfe711 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -100,6 +100,12 @@ const api: ElectronAPI = { readClipboardImage: () => ipcRenderer.invoke("read-clipboard-image"), showNotification: (title, body) => ipcRenderer.send("show-notification", title, body), getWindowFocused: () => ipcRenderer.invoke("get-window-focused"), + getWindowFullscreen: () => ipcRenderer.invoke("get-window-fullscreen"), + onWindowFullscreenChanged: (cb) => { + const handler = (_: unknown, fullscreen: boolean) => cb(fullscreen) + ipcRenderer.on("window-fullscreen-changed", handler) + return () => ipcRenderer.removeListener("window-fullscreen-changed", handler) + }, setWindowFocus: () => ipcRenderer.invoke("set-window-focus"), showWindow: () => ipcRenderer.invoke("show-window"), relaunch: () => ipcRenderer.send("relaunch"), diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts index 5fbace72c7ca..ea201af63edc 100644 --- a/packages/desktop/src/preload/types.ts +++ b/packages/desktop/src/preload/types.ts @@ -91,6 +91,8 @@ export type ElectronAPI = { readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null> showNotification: (title: string, body?: string) => void getWindowFocused: () => Promise + getWindowFullscreen: () => Promise + onWindowFullscreenChanged: (cb: (fullscreen: boolean) => void) => () => void setWindowFocus: () => Promise showWindow: () => Promise relaunch: () => void diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index e2cf3d899869..df67ff0b0dbc 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -25,6 +25,7 @@ import { initI18n, t } from "./i18n" import { initializationData, initializationReady } from "./initialization" import { DesktopFirstLaunchOnboarding } from "./onboarding" import { resetZoom, setPinchZoomEnabled, webviewZoom, zoomIn, zoomOut } from "./webview-zoom" +import { windowFullscreen } from "./window-fullscreen" import { availableStartupServer, readyWslConnections } from "./wsl/connections" import "./styles.css" import { Splash } from "@opencode-ai/ui/logo" @@ -298,6 +299,8 @@ const createPlatform = (windowState: DesktopWindowState): Platform => { webviewZoom, + windowFullscreen, + getPinchZoomEnabled: () => window.api.getPinchZoomEnabled(), setPinchZoomEnabled, diff --git a/packages/desktop/src/renderer/window-fullscreen.ts b/packages/desktop/src/renderer/window-fullscreen.ts new file mode 100644 index 000000000000..95d91fa2e7e7 --- /dev/null +++ b/packages/desktop/src/renderer/window-fullscreen.ts @@ -0,0 +1,8 @@ +import { createSignal } from "solid-js" + +const [windowFullscreen, setWindowFullscreen] = createSignal(false) + +window.api.onWindowFullscreenChanged(setWindowFullscreen) +void window.api.getWindowFullscreen().then(setWindowFullscreen) + +export { windowFullscreen } diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index d29305faaed9..341291dd265d 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.18.5", + "version": "1.18.7", "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 e56d3ba4f2c2..d14decf085b0 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.18.5", + "version": "1.18.7", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 761f1c758978..7c20f5e29938 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.18.5", + "version": "1.18.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 535ee6faa63d..1c1bcf83ecc2 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.18.5", + "version": "1.18.7", "$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 a39ac136422c..980c2b24a945 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.18.5", + "version": "1.18.7", "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 6afa905bb463..cd62b1eb1331 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.5", + "version": "1.18.7", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 46e788228b1f..6ee8de99f3cb 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.5", + "version": "1.18.7", "name": "opencode", "type": "module", "license": "MIT", @@ -120,7 +120,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.11.1", + "gitlab-ai-provider": "6.12.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", diff --git a/packages/opencode/test/server/httpapi-reference.test.ts b/packages/opencode/test/server/httpapi-reference.test.ts index 9354d527da09..b187bd4eea2e 100644 --- a/packages/opencode/test/server/httpapi-reference.test.ts +++ b/packages/opencode/test/server/httpapi-reference.test.ts @@ -51,7 +51,7 @@ describe("reference HttpApi", () => { }, { name: "effect", - path: path.join(Global.Path.repos, "github.com", "Effect-TS", "effect"), + path: path.join(Global.Path.repos, "github.com", "Effect-TS", "effect@main"), source: { type: "git", repository: "Effect-TS/effect", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index fdeab9b82b3a..058803a25214 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.18.5", + "version": "1.18.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 093439705af3..422f5ac849b9 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.18.5", + "version": "1.18.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index d011dbb4735a..d4ffebe4fdfd 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.18.5", + "version": "1.18.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 0097648c549f..117743688f1c 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.18.5", + "version": "1.18.7", "private": true, "type": "module", "license": "MIT", @@ -40,7 +40,7 @@ }, "dependencies": { "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz", + "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", diff --git a/packages/slack/package.json b/packages/slack/package.json index e4fc453450b1..85d1ede18462 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.18.5", + "version": "1.18.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index b14e49e2d883..c32ac327d7bc 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.18.5", + "version": "1.18.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index e7b8acb07879..022f09ab3a58 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.18.5", + "version": "1.18.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 2b805daf63e9..079410c6fed5 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.18.5", + "version": "1.18.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx b/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx index 0c7ff8bbdd65..0741f7e4ef9e 100644 --- a/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx +++ b/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx @@ -1,9 +1,8 @@ -import { splitProps } from "solid-js" +import { splitProps, type JSX } from "solid-js" -export function ModelSelectorPopover(props: { triggerAs: any; triggerProps?: Record; children: any }) { - const [local] = splitProps(props, ["triggerAs", "triggerProps", "children"]) - const Trigger = local.triggerAs - return {local.children} +export function ModelSelectorPopover(props: { trigger: (props: Record) => JSX.Element }) { + const [local] = splitProps(props, ["trigger"]) + return <>{local.trigger({})} } export const ModelSelectorPopoverV2 = ModelSelectorPopover diff --git a/packages/tui/package.json b/packages/tui/package.json index d450262d85f9..4f9850eba8e6 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.18.5", + "version": "1.18.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index 971c3c89d0f1..d5022aeb48eb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.18.5", + "version": "1.18.7", "type": "module", "license": "MIT", "repository": { diff --git a/packages/ui/src/v2/components/select-v2.tsx b/packages/ui/src/v2/components/select-v2.tsx index 7f9a2b5d3761..57348ed39209 100644 --- a/packages/ui/src/v2/components/select-v2.tsx +++ b/packages/ui/src/v2/components/select-v2.tsx @@ -121,6 +121,7 @@ export function SelectV2(props: SelectV2Props) { {...others} multiple={false} + allowDuplicateSelectionEvents={false} disabled={local.disabled} data-component="select-v2-root" placement={local.placement ?? (inline() ? "bottom-end" : "bottom-start")} diff --git a/packages/ui/src/v2/components/switch-v2.css b/packages/ui/src/v2/components/switch-v2.css index 48d18d2c2503..8ee56319c3f9 100644 --- a/packages/ui/src/v2/components/switch-v2.css +++ b/packages/ui/src/v2/components/switch-v2.css @@ -30,7 +30,7 @@ border: none; background: linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-20) 100%), - var(--v2-background-bg-layer-03); + var(--v2-background-bg-layer-04); box-shadow: var(--v2-elevation-switch-off); transition: background 90ms ease-out, @@ -90,7 +90,7 @@ background: linear-gradient(0deg, var(--v2-overlay-simple-overlay-hover), var(--v2-overlay-simple-overlay-hover)), linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-20) 100%), - var(--v2-background-bg-layer-03); + var(--v2-background-bg-layer-04); } &:hover:not([data-disabled], [data-readonly]) [data-slot="switch-thumb"] { diff --git a/packages/web/package.json b/packages/web/package.json index 0f6de16efa49..f2ff27045025 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.18.5", + "version": "1.18.7", "scripts": { "dev": "node ./node_modules/astro/bin/astro.mjs dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai node ./node_modules/astro/bin/astro.mjs dev", diff --git a/packages/web/src/content/docs/ar/zen.mdx b/packages/web/src/content/docs/ar/zen.mdx index e7b6f0f0ff1b..055eaa56e00f 100644 --- a/packages/web/src/content/docs/ar/zen.mdx +++ b/packages/web/src/content/docs/ar/zen.mdx @@ -107,6 +107,7 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -149,6 +150,7 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/bs/zen.mdx b/packages/web/src/content/docs/bs/zen.mdx index 5daf188ba724..5f432929bad3 100644 --- a/packages/web/src/content/docs/bs/zen.mdx +++ b/packages/web/src/content/docs/bs/zen.mdx @@ -112,6 +112,7 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa. | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -156,6 +157,7 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/da/zen.mdx b/packages/web/src/content/docs/da/zen.mdx index dcfaf243f503..480e922b0944 100644 --- a/packages/web/src/content/docs/da/zen.mdx +++ b/packages/web/src/content/docs/da/zen.mdx @@ -112,6 +112,7 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints. | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -156,6 +157,7 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/de/zen.mdx b/packages/web/src/content/docs/de/zen.mdx index 6014f6e59fe9..171b1e198d33 100644 --- a/packages/web/src/content/docs/de/zen.mdx +++ b/packages/web/src/content/docs/de/zen.mdx @@ -103,6 +103,7 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen. | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -145,6 +146,7 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/es/zen.mdx b/packages/web/src/content/docs/es/zen.mdx index 342bf9eb2a5e..05edc7bca7ca 100644 --- a/packages/web/src/content/docs/es/zen.mdx +++ b/packages/web/src/content/docs/es/zen.mdx @@ -112,6 +112,7 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -156,6 +157,7 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/fr/zen.mdx b/packages/web/src/content/docs/fr/zen.mdx index c2e15a422ca8..23c3e12f5547 100644 --- a/packages/web/src/content/docs/fr/zen.mdx +++ b/packages/web/src/content/docs/fr/zen.mdx @@ -103,6 +103,7 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -145,6 +146,7 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/it/zen.mdx b/packages/web/src/content/docs/it/zen.mdx index bfdd81137ced..4ab797861013 100644 --- a/packages/web/src/content/docs/it/zen.mdx +++ b/packages/web/src/content/docs/it/zen.mdx @@ -112,6 +112,7 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API. | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -156,6 +157,7 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/ja/zen.mdx b/packages/web/src/content/docs/ja/zen.mdx index 5e6119936a19..05289dd63fce 100644 --- a/packages/web/src/content/docs/ja/zen.mdx +++ b/packages/web/src/content/docs/ja/zen.mdx @@ -103,6 +103,7 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動 | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -145,6 +146,7 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/ko/zen.mdx b/packages/web/src/content/docs/ko/zen.mdx index ec383a1d0fb4..75b06226bd6b 100644 --- a/packages/web/src/content/docs/ko/zen.mdx +++ b/packages/web/src/content/docs/ko/zen.mdx @@ -103,6 +103,7 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다. | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -145,6 +146,7 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/nb/zen.mdx b/packages/web/src/content/docs/nb/zen.mdx index 75bc29c18852..c5bd9942b8ed 100644 --- a/packages/web/src/content/docs/nb/zen.mdx +++ b/packages/web/src/content/docs/nb/zen.mdx @@ -112,6 +112,7 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter. | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -156,6 +157,7 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/pl/zen.mdx b/packages/web/src/content/docs/pl/zen.mdx index 742199eb62c2..e60fb6abb1a5 100644 --- a/packages/web/src/content/docs/pl/zen.mdx +++ b/packages/web/src/content/docs/pl/zen.mdx @@ -112,6 +112,7 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API. | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -156,6 +157,7 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/pt-br/zen.mdx b/packages/web/src/content/docs/pt-br/zen.mdx index e7d23ad573e4..243625cdd04a 100644 --- a/packages/web/src/content/docs/pt-br/zen.mdx +++ b/packages/web/src/content/docs/pt-br/zen.mdx @@ -103,6 +103,7 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API. | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -145,6 +146,7 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/ru/zen.mdx b/packages/web/src/content/docs/ru/zen.mdx index 8bf499bfd185..9cec51730bc9 100644 --- a/packages/web/src/content/docs/ru/zen.mdx +++ b/packages/web/src/content/docs/ru/zen.mdx @@ -112,6 +112,7 @@ OpenCode Zen работает как любой другой провайдер | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -156,6 +157,7 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/th/zen.mdx b/packages/web/src/content/docs/th/zen.mdx index 71204dacd6d1..f1769a62f7a2 100644 --- a/packages/web/src/content/docs/th/zen.mdx +++ b/packages/web/src/content/docs/th/zen.mdx @@ -105,6 +105,7 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -147,6 +148,7 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/tr/zen.mdx b/packages/web/src/content/docs/tr/zen.mdx index 3b986893579c..2c26f5705547 100644 --- a/packages/web/src/content/docs/tr/zen.mdx +++ b/packages/web/src/content/docs/tr/zen.mdx @@ -103,6 +103,7 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -145,6 +146,7 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 27c485c777ed..43a391357109 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -112,6 +112,7 @@ You can also access our models through the following API endpoints. | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -156,6 +157,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/zh-cn/zen.mdx b/packages/web/src/content/docs/zh-cn/zen.mdx index 8ff738f8a8c7..ac13e42cee2c 100644 --- a/packages/web/src/content/docs/zh-cn/zen.mdx +++ b/packages/web/src/content/docs/zh-cn/zen.mdx @@ -103,6 +103,7 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -145,6 +146,7 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/packages/web/src/content/docs/zh-tw/zen.mdx b/packages/web/src/content/docs/zh-tw/zen.mdx index cb48093c34e4..c31fa57d3695 100644 --- a/packages/web/src/content/docs/zh-tw/zen.mdx +++ b/packages/web/src/content/docs/zh-tw/zen.mdx @@ -107,6 +107,7 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。 | 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` | +| Kimi K3 | kimi-k3 | `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` | | Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -150,6 +151,7 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | +| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | 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 | diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 4e89e1f08063..6a53c96c5b66 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.18.5", + "version": "1.18.7", "publisher": "sst-dev", "repository": { "type": "git",