From ec828619cae413439f15ea03a1604c99dbc1b55c Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:01:46 -0600 Subject: [PATCH 001/470] fix(app): more defensive todo access --- packages/ui/src/components/message-part.tsx | 24 ++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 534ea8f50..d59f5cfa3 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -984,6 +984,22 @@ ToolRegistry.register({ ToolRegistry.register({ name: "todowrite", render(props) { + const todos = createMemo(() => { + const meta = props.metadata?.todos + if (Array.isArray(meta)) return meta + + const input = props.input.todos + if (Array.isArray(input)) return input + + return [] + }) + + const subtitle = createMemo(() => { + const list = todos() + if (list.length === 0) return "" + return `${list.filter((t: Todo) => t.status === "completed").length}/${list.length}` + }) + return ( t.status === "completed").length}/${props.input.todos.length}` - : "", + subtitle: subtitle(), }} > - +
- + {(todo: Todo) => (
From d34fdac85450c26ae422b00bd151eab797ea7e2e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 9 Jan 2026 12:04:56 +0000 Subject: [PATCH 002/470] ignore: update download stats 2026-01-09 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index d9b4eae18..bed75d338 100644 --- a/STATS.md +++ b/STATS.md @@ -195,3 +195,4 @@ | 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) | | 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) | | 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) | +| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) | From 0433d4d064e38430980797a1be179397aec0febd Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:19:39 -0600 Subject: [PATCH 003/470] fix(app): store terminal and review pane visibility per session --- .../app/src/components/dialog-select-file.tsx | 3 +- .../src/components/session-context-usage.tsx | 3 +- .../src/components/session/session-header.tsx | 20 +++-- packages/app/src/context/layout.tsx | 86 +++++++++++++------ packages/app/src/pages/session.tsx | 16 ++-- 5 files changed, 87 insertions(+), 41 deletions(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 9e3bbeddd..461f8a0c0 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -15,6 +15,7 @@ export function DialogSelectFile() { const params = useParams() const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) + const view = createMemo(() => layout.view(sessionKey())) return ( props.variant ?? "button") const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) + const view = createMemo(() => layout.view(sessionKey())) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const cost = createMemo(() => { @@ -48,7 +49,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { const openContext = () => { if (!params.id) return - layout.review.open() + view().reviewPanel.open() tabs().open("context") tabs().setActive("context") } diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 4958ad2c3..cfc6eb438 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -43,6 +43,8 @@ export function SessionHeader() { }) const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same }) + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const view = createMemo(() => layout.view(sessionKey())) function navigateToProject(directory: string) { navigate(`/${base64Encode(directory)}`) @@ -171,20 +173,24 @@ export function SessionHeader() { title="Toggle review" keybind={command.keybind("review.toggle")} > -
- +
Date: Fri, 9 Jan 2026 15:43:49 +0100 Subject: [PATCH 004/470] fix(desktop): complete symlink support implementation and enable CI for PRs (#7113) --- .github/workflows/nix-desktop.yml | 7 +++++++ packages/desktop/src-tauri/src/cli.rs | 15 +++++++++------ packages/desktop/src-tauri/src/lib.rs | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nix-desktop.yml b/.github/workflows/nix-desktop.yml index 54ce55be0..b7919c062 100644 --- a/.github/workflows/nix-desktop.yml +++ b/.github/workflows/nix-desktop.yml @@ -9,6 +9,13 @@ on: - "nix/**" - "packages/app/**" - "packages/desktop/**" + pull_request: + paths: + - "flake.nix" + - "flake.lock" + - "nix/**" + - "packages/app/**" + - "packages/desktop/**" workflow_dispatch: jobs: diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 6b86cbcd2..8b76d1a7f 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -1,3 +1,5 @@ +use tauri::Manager; + const CLI_INSTALL_DIR: &str = ".opencode/bin"; const CLI_BINARY_NAME: &str = "opencode"; @@ -9,9 +11,10 @@ fn get_cli_install_path() -> Option { }) } -pub fn get_sidecar_path() -> std::path::PathBuf { - tauri::utils::platform::current_exe() - .expect("Failed to get current exe") +pub fn get_sidecar_path(app: &tauri::AppHandle) -> std::path::PathBuf { + // Get binary with symlinks support + tauri::process::current_binary(&app.env()) + .expect("Failed to get current binary") .parent() .expect("Failed to get parent dir") .join("opencode-cli") @@ -26,12 +29,12 @@ fn is_cli_installed() -> bool { const INSTALL_SCRIPT: &str = include_str!("../../../../install"); #[tauri::command] -pub fn install_cli() -> Result { +pub fn install_cli(app: tauri::AppHandle) -> Result { if cfg!(not(unix)) { return Err("CLI installation is only supported on macOS & Linux".to_string()); } - let sidecar = get_sidecar_path(); + let sidecar = get_sidecar_path(&app); if !sidecar.exists() { return Err("Sidecar binary not found".to_string()); } @@ -108,7 +111,7 @@ pub fn sync_cli(app: tauri::AppHandle) -> Result<(), String> { cli_version, app_version ); - install_cli()?; + install_cli(app)?; println!("Synced installed CLI"); diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index cb937401d..bdff73820 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -129,7 +129,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { #[cfg(not(target_os = "windows"))] let (mut rx, child) = { - let sidecar = get_sidecar_path(); + let sidecar = get_sidecar_path(app); let shell = get_user_shell(); app.shell() .command(&shell) From 030b14ac4e037b410d993a5ef79b561b011855f6 Mon Sep 17 00:00:00 2001 From: AJ Ka <110759081+0xajka@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:53:18 -0400 Subject: [PATCH 005/470] fix: prevent model picker overflow with Zen in new sessions (#7495) --- packages/app/src/components/dialog-select-model.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 058cb420e..d54f9369a 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -76,7 +76,7 @@ export const ModelSelectorPopover: Component<{ {props.children} - + Select model setOpen(false)} class="p-1" /> From e82b11275973ca9e2c65482b15f4de90458a4e56 Mon Sep 17 00:00:00 2001 From: Sebastian Herrlinger Date: Fri, 9 Jan 2026 17:26:33 +0100 Subject: [PATCH 006/470] upgrade opentui to v0.1.71, fixing capability detection and therefor kitty keyboard --- bun.lock | 20 ++++++++++---------- packages/opencode/package.json | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bun.lock b/bun.lock index 0b14f193b..7768eab88 100644 --- a/bun.lock +++ b/bun.lock @@ -286,8 +286,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.2", - "@opentui/core": "0.1.70", - "@opentui/solid": "0.1.70", + "@opentui/core": "0.1.71", + "@opentui/solid": "0.1.71", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -1201,21 +1201,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.70", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.70", "@opentui/core-darwin-x64": "0.1.70", "@opentui/core-linux-arm64": "0.1.70", "@opentui/core-linux-x64": "0.1.70", "@opentui/core-win32-arm64": "0.1.70", "@opentui/core-win32-x64": "0.1.70", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-6cPAlbCnaiUUtQtvZNpkr0Xv8AdVAgJuy2VAwIsDN1pIv0zMpa0ZG+mr7afCGygw1eeDRveefrjfgFAB1r0SVw=="], + "@opentui/core": ["@opentui/core@0.1.71", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.71", "@opentui/core-darwin-x64": "0.1.71", "@opentui/core-linux-arm64": "0.1.71", "@opentui/core-linux-x64": "0.1.71", "@opentui/core-win32-arm64": "0.1.71", "@opentui/core-win32-x64": "0.1.71", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-0gmqiBnM80QexPEpxv84QZn4h3IKrvLNn/9wIEzpHBtP7hunI1cJqb9fpMIlYDmf03PDW3KFHx9Oo2v+9jtosQ=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.70", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rM8EnvW1tOAXWnp2Iy2M82I+ViSmRwUagx3v1/ni6N8GCcw/3mE0C6eB3sVlYNXVMwBEgiKpWFn85RCe4+qXQw=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.71", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0H8tYWuBDmBuUyuoSFeLi1KTVAa7d4RpftS5E4XUfyRtS5afr08DxXft15Y4unMnKzRNQGYaU9Rk/JdbAy9MKg=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.70", "", { "os": "darwin", "cpu": "x64" }, "sha512-XdBgW+em8J+YGSUpaKF8/NxPjikJygK3dIkeMAw5xQ2lt7jXKxeM5MMmN/V4MfK3pLMtO56rLJlXaLH/h50uQA=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.71", "", { "os": "darwin", "cpu": "x64" }, "sha512-3fRfmfpH1GgjphMewETsykRj3FvTJaEf2WttzRPHNaX5zeJQEJYjFLCKFYRujAofvB4I1oxdjGhxEvwD+7AZ0A=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.70", "", { "os": "linux", "cpu": "arm64" }, "sha512-oSVWNMSOx0Na0M0LCqtWCxeh4SuLSK5lg8ZwVzsEoimIAxh0snp9nRUo/Qi8yD9BP0DSDmXuM/B3ONtzFaf0dw=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.71", "", { "os": "linux", "cpu": "arm64" }, "sha512-dmLl53gGLEKCef7Cl4v97JmGc5QV9zDjL6djBLS4X88oVY5F0YU8eLW8/3d0DpD/gWQOOOvGvL6fUP1wON1WIg=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.70", "", { "os": "linux", "cpu": "x64" }, "sha512-WUrhukefMghcZ7sAjkxEy50vA6ii0X21xh7m8c4omXyYYfQXyDs25pNExB8cwoCrZEaC8RTlF4lRSNPIXsZKhA=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.71", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhTfXqNqIb62O/GAjc6MCFheIQLwUULdes4oa0eCab14tyfooc7atzNQPNmsug0blBq0EfqlejRfaOp7f2D9Wg=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.70", "", { "os": "win32", "cpu": "arm64" }, "sha512-p1K2VJXGmZqSV7mR61v7KJpT1Zth7DS99wEtaqqfK68OWt33K2XxLmGO0KD142R2JLfXu32NnRmBHxmVx8IjBA=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.71", "", { "os": "win32", "cpu": "arm64" }, "sha512-F2zXQFBs/DncYCNIwFx6ps7/9N3tLJ3OZNZZrxUXmwKrO+kziB1lBehBlW5TzzBIPkNY9UBaQg+iekncwiDEcg=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.70", "", { "os": "win32", "cpu": "x64" }, "sha512-G6b8te1twMeDhjg1oZa0IcUjhOJZFCSdlQt+q5gu5vVtjCrIwAn9o7m5EwNMPakc31pDWUZ7v0ktgv0Xw1AQVA=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.71", "", { "os": "win32", "cpu": "x64" }, "sha512-IJBIrwgQFgPLDNEQVLSVaYZMbGETQlfZ1v/gz/zGtU2NwAHCllmPT1E1AauV/7Ex9Da082/dTrnl351IsipcPQ=="], - "@opentui/solid": ["@opentui/solid@0.1.70", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.70", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-8Cw/w4Of2OJhsFhcp/Wdj8cJRVaGvVsIiUoYiFtyToM01J4en0bg/vnbeZteyuZWeEtA4iz1/rSEQf7Dp+2FIQ=="], + "@opentui/solid": ["@opentui/solid@0.1.71", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.71", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-09mimwa2eTkttE72BMv1SNvH/NSf088Ebz0nmrJ7qpmHjbngn7FwlTXK+98sukUTeR6fQqFOnMnuLQieF6QdiA=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ceaf3b3e8..50cd188ee 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -81,8 +81,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.2", - "@opentui/core": "0.1.70", - "@opentui/solid": "0.1.70", + "@opentui/core": "0.1.71", + "@opentui/solid": "0.1.71", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", From 7df36cf0fab3cde893d0276bc96406c048e82494 Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 9 Jan 2026 16:28:31 +0000 Subject: [PATCH 007/470] Update Nix flake.lock and hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index e260018d2..d7c0812c3 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-KjBAaI9Kv6huOmPvUbtyYsMhbScI91w1lOZyXpIWqI0=" + "nodeModules": "sha256-vuv1AO4iW6cR/+IgPSLzAb1iif1fo9ti5VzSrX4fo5E=" } From f3e8a275b8292609600c5634a9a9c52fa776f678 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 9 Jan 2026 10:39:59 -0600 Subject: [PATCH 008/470] docs: update brew formula references --- README.md | 2 +- packages/console/app/src/routes/download/index.tsx | 4 ++-- packages/console/app/src/routes/index.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 64243fe30..d0ba48740 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ npm i -g opencode-ai@latest # or bun/pnpm/yarn scoop bucket add extras; scoop install extras/opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date) -brew install opencode # macOS and Linux (official brew formula, updated less frequently) +brew install opencode # macOS and Linux (official brew formula, updated less) paru -S opencode-bin # Arch Linux mise use -g opencode # Any OS nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch diff --git a/packages/console/app/src/routes/download/index.tsx b/packages/console/app/src/routes/download/index.tsx index d4de97c68..47555f541 100644 --- a/packages/console/app/src/routes/download/index.tsx +++ b/packages/console/app/src/routes/download/index.tsx @@ -129,9 +129,9 @@ export default function Download() { - diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index 227021b89..a59a8f8db 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -140,7 +140,7 @@ export default function Home() { From 18cf4df6c6a7175334485252c4c772921f07f93f Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 9 Jan 2026 11:52:30 -0500 Subject: [PATCH 009/470] wip: zen --- .../console/app/src/routes/stripe/webhook.ts | 267 +++++++++++++----- 1 file changed, 199 insertions(+), 68 deletions(-) diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts index 6f1637641..3422d9dd6 100644 --- a/packages/console/app/src/routes/stripe/webhook.ts +++ b/packages/console/app/src/routes/stripe/webhook.ts @@ -1,11 +1,13 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import type { APIEvent } from "@solidjs/start/server" -import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js" +import { and, Database, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js" import { BillingTable, PaymentTable, SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js" import { Identifier } from "@opencode-ai/console-core/identifier.js" import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js" import { Actor } from "@opencode-ai/console-core/actor.js" import { Resource } from "@opencode-ai/console-resource" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js" export async function POST(input: APIEvent) { const body = await Billing.stripe().webhooks.constructEventAsync( @@ -39,7 +41,7 @@ export async function POST(input: APIEvent) { .where(eq(BillingTable.customerID, customerID)) }) } - if (body.type === "checkout.session.completed") { + if (body.type === "checkout.session.completed" && body.data.object.mode === "payment") { const workspaceID = body.data.object.metadata?.workspaceID const amountInCents = body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount) const customerID = body.data.object.customer as string @@ -102,85 +104,112 @@ export async function POST(input: APIEvent) { }) }) } - if (body.type === "charge.refunded") { + if (body.type === "checkout.session.completed" && body.data.object.mode === "subscription") { + const workspaceID = body.data.object.custom_fields.find((f) => f.key === "workspaceid")?.text?.value + const amountInCents = body.data.object.amount_total as number const customerID = body.data.object.customer as string - const paymentIntentID = body.data.object.payment_intent as string - if (!customerID) throw new Error("Customer ID not found") - if (!paymentIntentID) throw new Error("Payment ID not found") + const customerEmail = body.data.object.customer_details?.email as string + const invoiceID = body.data.object.invoice as string + const subscriptionID = body.data.object.subscription as string + const promoCode = body.data.object.discounts?.[0]?.promotion_code as string - const workspaceID = await Database.use((tx) => - tx - .select({ - workspaceID: BillingTable.workspaceID, - }) - .from(BillingTable) - .where(eq(BillingTable.customerID, customerID)) - .then((rows) => rows[0]?.workspaceID), - ) if (!workspaceID) throw new Error("Workspace ID not found") - - const amount = await Database.use((tx) => - tx - .select({ - amount: PaymentTable.amount, - }) - .from(PaymentTable) - .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID))) - .then((rows) => rows[0]?.amount), - ) - if (!amount) throw new Error("Payment not found") - - await Database.transaction(async (tx) => { - await tx - .update(PaymentTable) - .set({ - timeRefunded: new Date(body.created * 1000), - }) - .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID))) - - await tx - .update(BillingTable) - .set({ - balance: sql`${BillingTable.balance} - ${amount}`, - }) - .where(eq(BillingTable.workspaceID, workspaceID)) - }) - } - if (body.type === "invoice.payment_succeeded" && body.data.object.billing_reason === "subscription_cycle") { - const invoiceID = body.data.object.id as string - const amountInCents = body.data.object.amount_paid - const customerID = body.data.object.customer as string - const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string - if (!customerID) throw new Error("Customer ID not found") + if (!amountInCents) throw new Error("Amount not found") if (!invoiceID) throw new Error("Invoice ID not found") if (!subscriptionID) throw new Error("Subscription ID not found") + // get payment id from invoice const invoice = await Billing.stripe().invoices.retrieve(invoiceID, { expand: ["payments"], }) const paymentID = invoice.payments?.data[0].payment.payment_intent as string if (!paymentID) throw new Error("Payment ID not found") - const workspaceID = await Database.use((tx) => - tx - .select({ workspaceID: BillingTable.workspaceID }) - .from(BillingTable) - .where(eq(BillingTable.customerID, customerID)) - .then((rows) => rows[0]?.workspaceID), - ) - if (!workspaceID) throw new Error("Workspace ID not found for customer") + // get payment method for the payment intent + const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, { + expand: ["payment_method"], + }) + const paymentMethod = paymentIntent.payment_method + if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded") - await Database.use((tx) => - tx.insert(PaymentTable).values({ - workspaceID, - id: Identifier.create("payment"), - amount: centsToMicroCents(amountInCents), - paymentID, - invoiceID, - customerID, - }), - ) + // get coupon id from promotion code + const couponID = await (async () => { + if (!promoCode) return + const coupon = await Billing.stripe().promotionCodes.retrieve(promoCode) + const couponID = coupon.coupon.id + if (!couponID) throw new Error("Coupon not found for promotion code") + return couponID + })() + + // get user + + await Actor.provide("system", { workspaceID }, async () => { + // look up current billing + const billing = await Billing.get() + if (!billing) throw new Error(`Workspace with ID ${workspaceID} not found`) + + // Temporarily skip this check because during Black drop, user can checkout + // as a new customer + //if (billing.customerID !== customerID) throw new Error("Customer ID mismatch") + + // Temporarily check the user to apply to. After Black drop, we will allow + // look up the user to apply to + const users = await Database.use((tx) => + tx + .select({ id: UserTable.id, email: AuthTable.subject }) + .from(UserTable) + .innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email"))) + .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))), + ) + const user = users.find((u) => u.email === customerEmail) ?? users[0] + if (!user) { + console.error(`Error: User with email ${customerEmail} not found in workspace ${workspaceID}`) + process.exit(1) + } + + // set customer metadata + if (!billing?.customerID) { + await Billing.stripe().customers.update(customerID, { + metadata: { + workspaceID, + }, + }) + } + + await Database.transaction(async (tx) => { + await tx + .update(BillingTable) + .set({ + customerID, + subscriptionID, + subscriptionCouponID: couponID, + paymentMethodID: paymentMethod.id, + paymentMethodLast4: paymentMethod.card?.last4 ?? null, + paymentMethodType: paymentMethod.type, + }) + .where(eq(BillingTable.workspaceID, workspaceID)) + + await tx.insert(SubscriptionTable).values({ + workspaceID, + id: Identifier.create("subscription"), + userID: user.id, + }) + + await tx.insert(PaymentTable).values({ + workspaceID, + id: Identifier.create("payment"), + amount: centsToMicroCents(amountInCents), + paymentID, + invoiceID, + customerID, + enrichment: { + type: "subscription", + couponID, + }, + }) + }) + }) } if (body.type === "customer.subscription.created") { const data = { @@ -377,11 +406,113 @@ export async function POST(input: APIEvent) { if (!workspaceID) throw new Error("Workspace ID not found for subscription") await Database.transaction(async (tx) => { - await tx.update(BillingTable).set({ subscriptionID: null }).where(eq(BillingTable.workspaceID, workspaceID)) + await tx + .update(BillingTable) + .set({ subscriptionID: null, subscriptionCouponID: null }) + .where(eq(BillingTable.workspaceID, workspaceID)) await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID)) }) } + if (body.type === "invoice.payment_succeeded") { + if (body.data.object.billing_reason === "subscription_cycle") { + const invoiceID = body.data.object.id as string + const amountInCents = body.data.object.amount_paid + const customerID = body.data.object.customer as string + const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string + + if (!customerID) throw new Error("Customer ID not found") + if (!invoiceID) throw new Error("Invoice ID not found") + if (!subscriptionID) throw new Error("Subscription ID not found") + + // get coupon id from subscription + const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscriptionID, { + expand: ["discounts"], + }) + const couponID = + typeof subscriptionData.discounts[0] === "string" + ? subscriptionData.discounts[0] + : subscriptionData.discounts[0]?.coupon?.id + + // get payment id from invoice + const invoice = await Billing.stripe().invoices.retrieve(invoiceID, { + expand: ["payments"], + }) + const paymentID = invoice.payments?.data[0].payment.payment_intent as string + if (!paymentID) { + // payment id can be undefined when using coupon + if (!couponID) throw new Error("Payment ID not found") + } + + const workspaceID = await Database.use((tx) => + tx + .select({ workspaceID: BillingTable.workspaceID }) + .from(BillingTable) + .where(eq(BillingTable.customerID, customerID)) + .then((rows) => rows[0]?.workspaceID), + ) + if (!workspaceID) throw new Error("Workspace ID not found for customer") + + await Database.use((tx) => + tx.insert(PaymentTable).values({ + workspaceID, + id: Identifier.create("payment"), + amount: centsToMicroCents(amountInCents), + paymentID, + invoiceID, + customerID, + enrichment: { + type: "subscription", + couponID, + }, + }), + ) + } + } + if (body.type === "charge.refunded") { + const customerID = body.data.object.customer as string + const paymentIntentID = body.data.object.payment_intent as string + if (!customerID) throw new Error("Customer ID not found") + if (!paymentIntentID) throw new Error("Payment ID not found") + + const workspaceID = await Database.use((tx) => + tx + .select({ + workspaceID: BillingTable.workspaceID, + }) + .from(BillingTable) + .where(eq(BillingTable.customerID, customerID)) + .then((rows) => rows[0]?.workspaceID), + ) + if (!workspaceID) throw new Error("Workspace ID not found") + + const amount = await Database.use((tx) => + tx + .select({ + amount: PaymentTable.amount, + }) + .from(PaymentTable) + .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID))) + .then((rows) => rows[0]?.amount), + ) + if (!amount) throw new Error("Payment not found") + + await Database.transaction(async (tx) => { + await tx + .update(PaymentTable) + .set({ + timeRefunded: new Date(body.created * 1000), + }) + .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID))) + + await tx + .update(BillingTable) + .set({ + balance: sql`${BillingTable.balance} - ${amount}`, + }) + .where(eq(BillingTable.workspaceID, workspaceID)) + }) + } })() .then((message) => { return Response.json({ message: message ?? "done" }, { status: 200 }) From cbb3141130105287302de927af6d016ecce68219 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:02:44 -0600 Subject: [PATCH 010/470] fix(app): no custom url param --- packages/app/src/app.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 3caaf99fe..dc7b976fc 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -38,9 +38,6 @@ declare global { } const defaultServerUrl = iife(() => { - const param = new URLSearchParams(document.location.search).get("url") - if (param) return param - if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}` if (import.meta.env.DEV) From 44297ffe786fbf6635aad86090b8d1f398e8592f Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:03:08 -0600 Subject: [PATCH 011/470] fix(app): break words in user message --- packages/ui/src/components/message-part.css | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index dd8e30ba6..7d191e8b4 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -77,7 +77,7 @@ [data-slot="user-message-text"] { white-space: pre-wrap; - word-break: break-all; + word-break: break-word; overflow: hidden; background: var(--surface-base); padding: 8px 12px; @@ -254,6 +254,7 @@ scrollbar-width: none; -ms-overflow-style: none; + &::-webkit-scrollbar { display: none; } @@ -271,6 +272,7 @@ /* Hide scrollbar */ scrollbar-width: none; -ms-overflow-style: none; + &::-webkit-scrollbar { display: none; } @@ -413,26 +415,23 @@ border: 1.5px solid transparent; background: linear-gradient(var(--background-base) 0 0) padding-box, - conic-gradient( - from var(--border-angle), - transparent 0deg, - transparent 0deg, - var(--border-warning-strong, var(--border-warning-selected)) 300deg, - var(--border-warning-base) 360deg - ) - border-box; + conic-gradient(from var(--border-angle), + transparent 0deg, + transparent 0deg, + var(--border-warning-strong, var(--border-warning-selected)) 300deg, + var(--border-warning-base) 360deg) border-box; animation: chase-border 2.5s linear infinite; pointer-events: none; z-index: -1; } - & > *:first-child { + &>*:first-child { border-top-left-radius: 6px; border-top-right-radius: 6px; overflow: hidden; } - & > *:last-child { + &>*:last-child { border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; overflow: hidden; @@ -458,6 +457,7 @@ from { --border-angle: 0deg; } + to { --border-angle: 360deg; } From bce9dc040f66dda76986abf5a20f2ff4de1ca6d9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 9 Jan 2026 17:03:51 +0000 Subject: [PATCH 012/470] chore: generate --- packages/ui/src/components/message-part.css | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 7d191e8b4..9628fec80 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -415,23 +415,26 @@ border: 1.5px solid transparent; background: linear-gradient(var(--background-base) 0 0) padding-box, - conic-gradient(from var(--border-angle), - transparent 0deg, - transparent 0deg, - var(--border-warning-strong, var(--border-warning-selected)) 300deg, - var(--border-warning-base) 360deg) border-box; + conic-gradient( + from var(--border-angle), + transparent 0deg, + transparent 0deg, + var(--border-warning-strong, var(--border-warning-selected)) 300deg, + var(--border-warning-base) 360deg + ) + border-box; animation: chase-border 2.5s linear infinite; pointer-events: none; z-index: -1; } - &>*:first-child { + & > *:first-child { border-top-left-radius: 6px; border-top-right-radius: 6px; overflow: hidden; } - &>*:last-child { + & > *:last-child { border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; overflow: hidden; From 7c2907cbb448b267643bbcf46ef9ba184d338698 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 9 Jan 2026 11:37:54 -0600 Subject: [PATCH 013/470] ci: fix failing pipeline --- .github/workflows/pr-standards.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-standards.yml b/.github/workflows/pr-standards.yml index fb62e660e..c1cf17567 100644 --- a/.github/workflows/pr-standards.yml +++ b/.github/workflows/pr-standards.yml @@ -105,7 +105,7 @@ jobs: query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) { - issuesReferences(first: 1) { + closingIssuesReferences(first: 1) { totalCount } } @@ -119,7 +119,7 @@ jobs: number: pr.number }); - const linkedIssues = result.repository.pullRequest.issuesReferences.totalCount; + const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount; if (linkedIssues === 0) { await addLabel('needs:issue'); From a5edf3a31184543cffae0fe0045323915b88ce62 Mon Sep 17 00:00:00 2001 From: ryanwyler Date: Fri, 9 Jan 2026 10:42:55 -0700 Subject: [PATCH 014/470] fix: resolve broken forked sessions with compactions due to missing parent-child message references (#6445) --- packages/opencode/src/session/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 0776590d6..a204913f7 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -151,12 +151,19 @@ export namespace Session { directory: Instance.directory, }) const msgs = await messages({ sessionID: input.sessionID }) + const idMap = new Map() + for (const msg of msgs) { if (input.messageID && msg.info.id >= input.messageID) break + const newID = Identifier.ascending("message") + idMap.set(msg.info.id, newID) + + const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined const cloned = await updateMessage({ ...msg.info, sessionID: session.id, - id: Identifier.ascending("message"), + id: newID, + ...(parentID && { parentID }), }) for (const part of msg.parts) { From e30a159264713b045faf784f4799d2626ac9b78c Mon Sep 17 00:00:00 2001 From: Ravi Kumar <82090231+Raviguntakala@users.noreply.github.com> Date: Fri, 9 Jan 2026 23:26:19 +0530 Subject: [PATCH 015/470] fix(cli): enable API key prompt for Bedrock in auth login (#7332) --- packages/opencode/src/cli/cmd/auth.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index adbd3f45a..232cdd576 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -341,8 +341,6 @@ export const AuthLoginCommand = cmd({ "Configure via opencode.json options (profile, region, endpoint) or\n" + "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID).", ) - prompts.outro("Done") - return } if (provider === "opencode") { From 0f2124db32606020766d237ce774a3c2e7783ee3 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:57:27 -0600 Subject: [PATCH 016/470] fix(app): no inline js --- packages/app/index.html | 31 +------------------------ packages/app/public/oc-theme-preload.js | 28 ++++++++++++++++++++++ packages/desktop/index.html | 31 +------------------------ packages/desktop/vite.config.ts | 1 + 4 files changed, 31 insertions(+), 60 deletions(-) create mode 100644 packages/app/public/oc-theme-preload.js diff --git a/packages/app/index.html b/packages/app/index.html index 44fa3b989..e0fbe6913 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -14,36 +14,7 @@ - + diff --git a/packages/app/public/oc-theme-preload.js b/packages/app/public/oc-theme-preload.js new file mode 100644 index 000000000..f8c710496 --- /dev/null +++ b/packages/app/public/oc-theme-preload.js @@ -0,0 +1,28 @@ +;(function () { + var themeId = localStorage.getItem("opencode-theme-id") + if (!themeId) return + + var scheme = localStorage.getItem("opencode-color-scheme") || "system" + var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches) + var mode = isDark ? "dark" : "light" + + document.documentElement.dataset.theme = themeId + document.documentElement.dataset.colorScheme = mode + + if (themeId === "oc-1") return + + var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode) + if (css) { + var style = document.createElement("style") + style.id = "oc-theme-preload" + style.textContent = + ":root{color-scheme:" + + mode + + ";--text-mix-blend-mode:" + + (isDark ? "plus-lighter" : "multiply") + + ";" + + css + + "}" + document.head.appendChild(style) + } +})() diff --git a/packages/desktop/index.html b/packages/desktop/index.html index f93e97294..11e9f39a6 100644 --- a/packages/desktop/index.html +++ b/packages/desktop/index.html @@ -13,36 +13,7 @@ - + diff --git a/packages/desktop/vite.config.ts b/packages/desktop/vite.config.ts index 37bc63393..62c3a099a 100644 --- a/packages/desktop/vite.config.ts +++ b/packages/desktop/vite.config.ts @@ -6,6 +6,7 @@ const host = process.env.TAURI_DEV_HOST // https://vite.dev/config/ export default defineConfig({ plugins: [appPlugin], + publicDir: "../app/public", // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors From de286b08f6b4955af4d23c217232fbf3d26f756b Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 9 Jan 2026 12:33:51 -0600 Subject: [PATCH 017/470] ignore: bump plugin version --- packages/opencode/src/global/index.ts | 2 +- packages/opencode/src/plugin/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 133d5bf40..2bd35971c 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -33,7 +33,7 @@ await Promise.all([ fs.mkdir(Global.Path.bin, { recursive: true }), ]) -const CACHE_VERSION = "15" +const CACHE_VERSION = "16" const version = await Bun.file(path.join(Global.Path.cache, "version")) .text() diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index a4353f1f4..f2ee91122 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -11,7 +11,7 @@ import { Flag } from "../flag/flag" export namespace Plugin { const log = Log.create({ service: "plugin" }) - const BUILTIN = ["opencode-copilot-auth@0.0.11", "opencode-anthropic-auth@0.0.5"] + const BUILTIN = ["opencode-copilot-auth@0.0.11", "opencode-anthropic-auth@0.0.8"] const state = Instance.state(async () => { const client = createOpencodeClient({ From 22f51c6b47433f3a649435407a959d12e15637d5 Mon Sep 17 00:00:00 2001 From: Sebastian Herrlinger Date: Fri, 9 Jan 2026 20:18:34 +0100 Subject: [PATCH 018/470] upgrade opentui to v0.1.72 --- bun.lock | 20 ++++++++++---------- packages/opencode/package.json | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bun.lock b/bun.lock index 7768eab88..23efcc827 100644 --- a/bun.lock +++ b/bun.lock @@ -286,8 +286,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.2", - "@opentui/core": "0.1.71", - "@opentui/solid": "0.1.71", + "@opentui/core": "0.1.72", + "@opentui/solid": "0.1.72", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -1201,21 +1201,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.71", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.71", "@opentui/core-darwin-x64": "0.1.71", "@opentui/core-linux-arm64": "0.1.71", "@opentui/core-linux-x64": "0.1.71", "@opentui/core-win32-arm64": "0.1.71", "@opentui/core-win32-x64": "0.1.71", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-0gmqiBnM80QexPEpxv84QZn4h3IKrvLNn/9wIEzpHBtP7hunI1cJqb9fpMIlYDmf03PDW3KFHx9Oo2v+9jtosQ=="], + "@opentui/core": ["@opentui/core@0.1.72", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.72", "@opentui/core-darwin-x64": "0.1.72", "@opentui/core-linux-arm64": "0.1.72", "@opentui/core-linux-x64": "0.1.72", "@opentui/core-win32-arm64": "0.1.72", "@opentui/core-win32-x64": "0.1.72", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-l4WQzubBJ80Q0n77Lxuodjwwm8qj/sOa7IXxEAzzDDXY/7bsIhdSpVhRTt+KevBRlok5J+w/KMKYr8UzkA4/hA=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.71", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0H8tYWuBDmBuUyuoSFeLi1KTVAa7d4RpftS5E4XUfyRtS5afr08DxXft15Y4unMnKzRNQGYaU9Rk/JdbAy9MKg=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.72", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RoU48kOrhLZYDBiXaDu1LXS2bwRdlJlFle8eUQiqJjLRbMIY34J/srBuL0JnAS3qKW4J34NepUQa0l0/S43Q3w=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.71", "", { "os": "darwin", "cpu": "x64" }, "sha512-3fRfmfpH1GgjphMewETsykRj3FvTJaEf2WttzRPHNaX5zeJQEJYjFLCKFYRujAofvB4I1oxdjGhxEvwD+7AZ0A=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.72", "", { "os": "darwin", "cpu": "x64" }, "sha512-hHUQw8i2LWPToRW1rjAiRqmNf34iJPS9ve9CJDygvFs5JOqUxN5yrfLfKfE+1bQjfFDHnpqW1HUk96iLhkPj8Q=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.71", "", { "os": "linux", "cpu": "arm64" }, "sha512-dmLl53gGLEKCef7Cl4v97JmGc5QV9zDjL6djBLS4X88oVY5F0YU8eLW8/3d0DpD/gWQOOOvGvL6fUP1wON1WIg=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.72", "", { "os": "linux", "cpu": "arm64" }, "sha512-63yml0OQ8tVa0JuDF9lBAWiChX6Q+iDO7lKv7c2n0352n/WyPr3iAgq4uSoH49HXuKeAXY/VwHGjvPzjXD/SDA=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.71", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhTfXqNqIb62O/GAjc6MCFheIQLwUULdes4oa0eCab14tyfooc7atzNQPNmsug0blBq0EfqlejRfaOp7f2D9Wg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.72", "", { "os": "linux", "cpu": "x64" }, "sha512-51veiQXNLvzDsFzsEvt71uK7WhiRe2DnvlJSGBSe6aRRHHxjCFYHzYi7t6bitJqtDTUj+EaMPbH81oZ6xy7tyg=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.71", "", { "os": "win32", "cpu": "arm64" }, "sha512-F2zXQFBs/DncYCNIwFx6ps7/9N3tLJ3OZNZZrxUXmwKrO+kziB1lBehBlW5TzzBIPkNY9UBaQg+iekncwiDEcg=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.72", "", { "os": "win32", "cpu": "arm64" }, "sha512-1Ep6OcaYTy1RlLOln+LNN7DL1iNyLwLjG2M8aO0pVJKFvxeD5P7rdRzY065E4uhkHeJIHuduUqxvUjD0dyuwbw=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.71", "", { "os": "win32", "cpu": "x64" }, "sha512-IJBIrwgQFgPLDNEQVLSVaYZMbGETQlfZ1v/gz/zGtU2NwAHCllmPT1E1AauV/7Ex9Da082/dTrnl351IsipcPQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.72", "", { "os": "win32", "cpu": "x64" }, "sha512-5QUv91UkOINlkEaPky3kaxmJvshcJMBAX7LZtIroduaKBGpWRA1aogNhPZzp+30WkvgOU7aOtUktAZuFXb9WdQ=="], - "@opentui/solid": ["@opentui/solid@0.1.71", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.71", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-09mimwa2eTkttE72BMv1SNvH/NSf088Ebz0nmrJ7qpmHjbngn7FwlTXK+98sukUTeR6fQqFOnMnuLQieF6QdiA=="], + "@opentui/solid": ["@opentui/solid@0.1.72", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.72", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-hytoLPboL/MTY/BQUnf/HlBuNXTVONney0X+PIQI82wT7kMx7+HHI2wnowpM3dyvA7l6NfORSud2cs9kIUBFBw=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 50cd188ee..ea754ca61 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -81,8 +81,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.2", - "@opentui/core": "0.1.71", - "@opentui/solid": "0.1.71", + "@opentui/core": "0.1.72", + "@opentui/solid": "0.1.72", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", From f2b2940298c859f1f699837cab687e9a7b29d603 Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 9 Jan 2026 19:20:11 +0000 Subject: [PATCH 019/470] Update Nix flake.lock and hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index d7c0812c3..6a5e8c26a 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-vuv1AO4iW6cR/+IgPSLzAb1iif1fo9ti5VzSrX4fo5E=" + "nodeModules": "sha256-+QM5BDFxzrm1HY5ealjCm7jIO1t/rpW1q4GGLViPMmA=" } From 75df5040ea45ba5e7349ed6e6871793a674906b3 Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:21:01 -0500 Subject: [PATCH 020/470] fix: report config errors instead of silent termination (#7522) --- packages/opencode/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index e1c2cbd1d..6099443e7 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -99,14 +99,16 @@ const cli = yargs(hideBin(process.argv)) .command(GithubCommand) .command(PrCommand) .command(SessionCommand) - .fail((msg) => { + .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") || msg?.startsWith("Not enough non-option arguments") || msg?.startsWith("Invalid values:") ) { + if (err) throw err cli.showHelp("log") } + if (err) throw err process.exit(1) }) .strict() From 982b71e861e538c33382ae34049f312f46f37cd2 Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 9 Jan 2026 16:33:46 -0500 Subject: [PATCH 021/470] disable server unless explicitly opted in (#7529) --- packages/opencode/src/cli/cmd/tui/app.tsx | 18 ++++- .../opencode/src/cli/cmd/tui/context/sdk.tsx | 77 ++++++++++++------- packages/opencode/src/cli/cmd/tui/thread.ts | 63 ++++++++++++++- packages/opencode/src/cli/cmd/tui/worker.ts | 57 ++++++++++---- packages/opencode/src/server/server.ts | 4 + packages/opencode/src/util/rpc.ts | 24 ++++++ 6 files changed, 194 insertions(+), 49 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 50f4f1ae5..aa62c6c58 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -97,7 +97,16 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { }) } -export function tui(input: { url: string; args: Args; directory?: string; onExit?: () => Promise }) { +import type { EventSource } from "./context/sdk" + +export function tui(input: { + url: string + args: Args + directory?: string + fetch?: typeof fetch + events?: EventSource + onExit?: () => Promise +}) { // promise to prevent immediate exit return new Promise(async (resolve) => { const mode = await getTerminalBackgroundColor() @@ -117,7 +126,12 @@ export function tui(input: { url: string; args: Args; directory?: string; onExit - + diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 4bcabaeaf..3339e7b00 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -3,21 +3,66 @@ import { createSimpleContext } from "./helper" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { batch, onCleanup, onMount } from "solid-js" +export type EventSource = { + on: (handler: (event: Event) => void) => () => void +} + export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", - init: (props: { url: string; directory?: string }) => { + init: (props: { url: string; directory?: string; fetch?: typeof fetch; events?: EventSource }) => { const abort = new AbortController() const sdk = createOpencodeClient({ baseUrl: props.url, signal: abort.signal, directory: props.directory, + fetch: props.fetch, }) const emitter = createGlobalEmitter<{ [key in Event["type"]]: Extract }>() + let queue: Event[] = [] + let timer: Timer | undefined + let last = 0 + + const flush = () => { + if (queue.length === 0) return + const events = queue + queue = [] + timer = undefined + last = Date.now() + // Batch all event emissions so all store updates result in a single render + batch(() => { + for (const event of events) { + emitter.emit(event.type, event) + } + }) + } + + const handleEvent = (event: Event) => { + queue.push(event) + const elapsed = Date.now() - last + + if (timer) return + // If we just flushed recently (within 16ms), batch this with future events + // Otherwise, process immediately to avoid latency + if (elapsed < 16) { + timer = setTimeout(flush, 16) + return + } + flush() + } + onMount(async () => { + // If an event source is provided, use it instead of SSE + if (props.events) { + const unsub = props.events.on(handleEvent) + onCleanup(unsub) + return + } + + // Fall back to SSE while (true) { if (abort.signal.aborted) break const events = await sdk.event.subscribe( @@ -26,36 +71,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ signal: abort.signal, }, ) - let queue: Event[] = [] - let timer: Timer | undefined - let last = 0 - - const flush = () => { - if (queue.length === 0) return - const events = queue - queue = [] - timer = undefined - last = Date.now() - // Batch all event emissions so all store updates result in a single render - batch(() => { - for (const event of events) { - emitter.emit(event.type, event) - } - }) - } for await (const event of events.stream) { - queue.push(event) - const elapsed = Date.now() - last - - if (timer) continue - // If we just flushed recently (within 16ms), batch this with future events - // Otherwise, process immediately to avoid latency - if (elapsed < 16) { - timer = setTimeout(flush, 16) - continue - } - flush() + handleEvent(event) } // Flush any remaining events @@ -68,6 +86,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ onCleanup(() => { abort.abort() + if (timer) clearTimeout(timer) }) return { client: sdk, event: emitter, url: props.url } diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 7b6f028fc..b9654d339 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -7,11 +7,39 @@ import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import type { Event } from "@opencode-ai/sdk/v2" +import type { EventSource } from "./context/sdk" declare global { const OPENCODE_WORKER_PATH: string } +type RpcClient = ReturnType> + +function createWorkerFetch(client: RpcClient): typeof fetch { + const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const request = new Request(input, init) + const body = request.body ? await request.text() : undefined + const result = await client.call("fetch", { + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + body, + }) + return new Response(result.body, { + status: result.status, + headers: result.headers, + }) + } + return fn as typeof fetch +} + +function createEventSource(client: RpcClient): EventSource { + return { + on: (handler) => client.on("event", handler), + } +} + export const TuiThreadCommand = cmd({ command: "$0 [project]", describe: "start opencode tui", @@ -80,16 +108,45 @@ export const TuiThreadCommand = cmd({ process.on("SIGUSR2", async () => { await client.call("reload", undefined) }) - const opts = await resolveNetworkOptions(args) - const server = await client.call("server", opts) + const prompt = await iife(async () => { const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined if (!args.prompt) return piped return piped ? piped + "\n" + args.prompt : args.prompt }) + // Check if server should be started (port or hostname explicitly set in CLI or config) + const networkOpts = await resolveNetworkOptions(args) + const shouldStartServer = + process.argv.includes("--port") || + process.argv.includes("--hostname") || + process.argv.includes("--mdns") || + networkOpts.mdns || + networkOpts.port !== 0 || + networkOpts.hostname !== "127.0.0.1" + + // Subscribe to events from worker + await client.call("subscribe", { directory: cwd }) + + let url: string + let customFetch: typeof fetch | undefined + let events: EventSource | undefined + + if (shouldStartServer) { + // Start HTTP server for external access + const server = await client.call("server", networkOpts) + url = server.url + } else { + // Use direct RPC communication (no HTTP) + url = "http://opencode.internal" + customFetch = createWorkerFetch(client) + events = createEventSource(client) + } + const tuiPromise = tui({ - url: server.url, + url, + fetch: customFetch, + events, args: { continue: args.continue, sessionID: args.session, diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index d32612dd5..ea88e45f1 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -5,8 +5,10 @@ import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" import { Rpc } from "@/util/rpc" import { upgrade } from "@/cli/upgrade" -import type { BunWebSocketData } from "hono/bun" import { Config } from "@/config/config" +import { Bus } from "@/bus" +import { GlobalBus } from "@/bus/global" +import type { BunWebSocketData } from "hono/bun" await Log.init({ print: process.argv.includes("--print-logs"), @@ -29,20 +31,47 @@ process.on("uncaughtException", (e) => { }) }) -let server: Bun.Server +// Subscribe to global events and forward them via RPC +GlobalBus.on("event", (event) => { + Rpc.emit("global.event", event) +}) + +let server: Bun.Server | undefined + export const rpc = { - async server(input: { port: number; hostname: string; mdns?: boolean }) { - if (server) await server.stop(true) - try { - server = Server.listen(input) - return { - url: server.url.toString(), - } - } catch (e) { - console.error(e) - throw e + async fetch(input: { url: string; method: string; headers: Record; body?: string }) { + const request = new Request(input.url, { + method: input.method, + headers: input.headers, + body: input.body, + }) + const response = await Server.App().fetch(request) + const body = await response.text() + return { + status: response.status, + headers: Object.fromEntries(response.headers.entries()), + body, } }, + async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) { + if (server) await server.stop(true) + server = Server.listen(input) + return { url: server.url.toString() } + }, + async subscribe(input: { directory: string }) { + return Instance.provide({ + directory: input.directory, + init: InstanceBootstrap, + fn: async () => { + Bus.subscribeAll((event) => { + Rpc.emit("event", event) + }) + // Emit connected event + Rpc.emit("event", { type: "server.connected", properties: {} }) + return { subscribed: true } + }, + }) + }, async checkUpgrade(input: { directory: string }) { await Instance.provide({ directory: input.directory, @@ -59,9 +88,7 @@ export const rpc = { async shutdown() { Log.Default.info("worker shutting down") await Instance.disposeAll() - // TODO: this should be awaited, but ws connections are - // causing this to hang, need to revisit this - server.stop(true) + if (server) server.stop(true) }, } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index c7baec778..f2545a747 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -2826,6 +2826,10 @@ export namespace Server { host: "app.opencode.ai", }, }) + response.headers.set( + "Content-Security-Policy", + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'", + ) return response }) as unknown as Hono, ) diff --git a/packages/opencode/src/util/rpc.ts b/packages/opencode/src/util/rpc.ts index 57c695c48..ebd8be40e 100644 --- a/packages/opencode/src/util/rpc.ts +++ b/packages/opencode/src/util/rpc.ts @@ -13,11 +13,16 @@ export namespace Rpc { } } + export function emit(event: string, data: unknown) { + postMessage(JSON.stringify({ type: "rpc.event", event, data })) + } + export function client(target: { postMessage: (data: string) => void | null onmessage: ((this: Worker, ev: MessageEvent) => any) | null }) { const pending = new Map void>() + const listeners = new Map void>>() let id = 0 target.onmessage = async (evt) => { const parsed = JSON.parse(evt.data) @@ -28,6 +33,14 @@ export namespace Rpc { pending.delete(parsed.id) } } + if (parsed.type === "rpc.event") { + const handlers = listeners.get(parsed.event) + if (handlers) { + for (const handler of handlers) { + handler(parsed.data) + } + } + } } return { call(method: Method, input: Parameters[0]): Promise> { @@ -37,6 +50,17 @@ export namespace Rpc { target.postMessage(JSON.stringify({ type: "rpc.request", method, input, id: requestId })) }) }, + on(event: string, handler: (data: Data) => void) { + let handlers = listeners.get(event) + if (!handlers) { + handlers = new Set() + listeners.set(event, handlers) + } + handlers.add(handler) + return () => { + handlers!.delete(handler) + } + }, } } } From 563b4c33f2bace782403de88e60de4f9167a3c93 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 9 Jan 2026 21:38:04 +0000 Subject: [PATCH 022/470] release: v1.1.10 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 23efcc827..7385c1472 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -98,7 +98,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -125,7 +125,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -149,7 +149,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -173,7 +173,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -202,7 +202,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -231,7 +231,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -247,7 +247,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.8", + "version": "1.1.10", "bin": { "opencode": "./bin/opencode", }, @@ -350,7 +350,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -370,7 +370,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.8", + "version": "1.1.10", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -381,7 +381,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -394,7 +394,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -433,7 +433,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "zod": "catalog:", }, @@ -444,7 +444,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 93b9e7d9b..4c5ea8ec5 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.8", + "version": "1.1.10", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index bd2d71390..74b38fff9 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.8", + "version": "1.1.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 2cc94973e..f3bdb2ca7 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.1.8", + "version": "1.1.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 57ca2a772..6a8138c64 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.8", + "version": "1.1.10", "$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 b9a440bd6..165a97f8b 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.8", + "version": "1.1.10", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index faa057a86..83eee9e6f 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.8", + "version": "1.1.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 7554d51d4..a83eacd52 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.8", + "version": "1.1.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 8ffa7e883..c38343af8 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.8" +version = "1.1.10" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 26f0562ed..937a906af 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.8", + "version": "1.1.10", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ea754ca61..22bbbe0fd 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.8", + "version": "1.1.10", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 9df1a2c06..ff79ef554 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.1.8", + "version": "1.1.10", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index cec83d292..7c3dba045 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.1.8", + "version": "1.1.10", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 730057191..2fcb82773 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.8", + "version": "1.1.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 9784c8707..a84a237b5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.8", + "version": "1.1.10", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 1cbdac236..e284e0e3e 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.8", + "version": "1.1.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index ff9e91b89..bf3e96802 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.1.8", + "version": "1.1.10", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 38dc28868..3cf1f7705 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.1.8", + "version": "1.1.10", "publisher": "sst-dev", "repository": { "type": "git", From 172bbdaced3e87d747f637ec988970ae820a614f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:47:37 -0800 Subject: [PATCH 023/470] feat: codex auth support (#7537) --- packages/opencode/src/auth/index.ts | 2 + packages/opencode/src/plugin/codex.ts | 417 ++++++++++++++++++ packages/opencode/src/plugin/index.ts | 18 +- packages/opencode/src/server/server.ts | 1 + packages/opencode/src/session/llm.ts | 61 ++- .../src/session/prompt/codex_header.txt | 1 + packages/opencode/src/session/system.ts | 5 + 7 files changed, 487 insertions(+), 18 deletions(-) create mode 100644 packages/opencode/src/plugin/codex.ts create mode 100644 packages/opencode/src/session/prompt/codex_header.txt diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index b9c8a78ca..6642a0742 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -3,6 +3,8 @@ import { Global } from "../global" import fs from "fs/promises" import z from "zod" +export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" + export namespace Auth { export const Oauth = z .object({ diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts new file mode 100644 index 000000000..f098a3967 --- /dev/null +++ b/packages/opencode/src/plugin/codex.ts @@ -0,0 +1,417 @@ +import type { Hooks, PluginInput } from "@opencode-ai/plugin" +import { Log } from "../util/log" +import { OAUTH_DUMMY_KEY } from "../auth" + +const log = Log.create({ service: "plugin.codex" }) + +const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann" +const ISSUER = "https://auth.openai.com" +const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses" +const OAUTH_PORT = 1455 + +interface PkceCodes { + verifier: string + challenge: string +} + +async function generatePKCE(): Promise { + const verifier = generateRandomString(43) + const encoder = new TextEncoder() + const data = encoder.encode(verifier) + const hash = await crypto.subtle.digest("SHA-256", data) + const challenge = base64UrlEncode(hash) + return { verifier, challenge } +} + +function generateRandomString(length: number): string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + const bytes = crypto.getRandomValues(new Uint8Array(length)) + return Array.from(bytes) + .map((b) => chars[b % chars.length]) + .join("") +} + +function base64UrlEncode(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer) + const binary = String.fromCharCode(...bytes) + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") +} + +function generateState(): string { + return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer) +} + +function buildAuthorizeUrl(redirectUri: string, pkce: PkceCodes, state: string): string { + const params = new URLSearchParams({ + response_type: "code", + client_id: CLIENT_ID, + redirect_uri: redirectUri, + scope: "openid profile email offline_access", + code_challenge: pkce.challenge, + code_challenge_method: "S256", + id_token_add_organizations: "true", + codex_cli_simplified_flow: "true", + state, + originator: "opencode", + }) + return `${ISSUER}/oauth/authorize?${params.toString()}` +} + +interface TokenResponse { + id_token: string + access_token: string + refresh_token: string + expires_in?: number +} + +async function exchangeCodeForTokens(code: string, redirectUri: string, pkce: PkceCodes): Promise { + const response = await fetch(`${ISSUER}/oauth/token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: redirectUri, + client_id: CLIENT_ID, + code_verifier: pkce.verifier, + }).toString(), + }) + if (!response.ok) { + throw new Error(`Token exchange failed: ${response.status}`) + } + return response.json() +} + +async function refreshAccessToken(refreshToken: string): Promise { + const response = await fetch(`${ISSUER}/oauth/token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: CLIENT_ID, + }).toString(), + }) + if (!response.ok) { + throw new Error(`Token refresh failed: ${response.status}`) + } + return response.json() +} + +const HTML_SUCCESS = ` + + + OpenCode - Codex Authorization Successful + + + +
+

Authorization Successful

+

You can close this window and return to OpenCode.

+
+ + +` + +const HTML_ERROR = (error: string) => ` + + + OpenCode - Codex Authorization Failed + + + +
+

Authorization Failed

+

An error occurred during authorization.

+
${error}
+
+ +` + +interface PendingOAuth { + pkce: PkceCodes + state: string + resolve: (tokens: TokenResponse) => void + reject: (error: Error) => void +} + +let oauthServer: ReturnType | undefined +let pendingOAuth: PendingOAuth | undefined + +async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> { + if (oauthServer) { + return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` } + } + + oauthServer = Bun.serve({ + port: OAUTH_PORT, + fetch(req) { + const url = new URL(req.url) + + if (url.pathname === "/auth/callback") { + const code = url.searchParams.get("code") + const state = url.searchParams.get("state") + const error = url.searchParams.get("error") + const errorDescription = url.searchParams.get("error_description") + + if (error) { + const errorMsg = errorDescription || error + pendingOAuth?.reject(new Error(errorMsg)) + pendingOAuth = undefined + return new Response(HTML_ERROR(errorMsg), { + headers: { "Content-Type": "text/html" }, + }) + } + + if (!code) { + const errorMsg = "Missing authorization code" + pendingOAuth?.reject(new Error(errorMsg)) + pendingOAuth = undefined + return new Response(HTML_ERROR(errorMsg), { + status: 400, + headers: { "Content-Type": "text/html" }, + }) + } + + if (!pendingOAuth || state !== pendingOAuth.state) { + const errorMsg = "Invalid state - potential CSRF attack" + pendingOAuth?.reject(new Error(errorMsg)) + pendingOAuth = undefined + return new Response(HTML_ERROR(errorMsg), { + status: 400, + headers: { "Content-Type": "text/html" }, + }) + } + + const current = pendingOAuth + pendingOAuth = undefined + + exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce) + .then((tokens) => current.resolve(tokens)) + .catch((err) => current.reject(err)) + + return new Response(HTML_SUCCESS, { + headers: { "Content-Type": "text/html" }, + }) + } + + if (url.pathname === "/cancel") { + pendingOAuth?.reject(new Error("Login cancelled")) + pendingOAuth = undefined + return new Response("Login cancelled", { status: 200 }) + } + + return new Response("Not found", { status: 404 }) + }, + }) + + log.info("codex oauth server started", { port: OAUTH_PORT }) + return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` } +} + +function stopOAuthServer() { + if (oauthServer) { + oauthServer.stop() + oauthServer = undefined + log.info("codex oauth server stopped") + } +} + +function waitForOAuthCallback(pkce: PkceCodes, state: string): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout( + () => { + if (pendingOAuth) { + pendingOAuth = undefined + reject(new Error("OAuth callback timeout - authorization took too long")) + } + }, + 5 * 60 * 1000, + ) // 5 minute timeout + + pendingOAuth = { + pkce, + state, + resolve: (tokens) => { + clearTimeout(timeout) + resolve(tokens) + }, + reject: (error) => { + clearTimeout(timeout) + reject(error) + }, + } + }) +} + +export async function CodexAuthPlugin(input: PluginInput): Promise { + return { + auth: { + provider: "openai", + async loader(getAuth, provider) { + const auth = await getAuth() + if (auth.type !== "oauth") return {} + + // Filter models to only allowed Codex models for OAuth + const allowedModels = new Set(["gpt-5.1-codex-max", "gpt-5.1-codex-mini", "gpt-5.2", "gpt-5.2-codex"]) + for (const modelId of Object.keys(provider.models)) { + if (!allowedModels.has(modelId)) { + delete provider.models[modelId] + } + } + + if (!provider.models["gpt-5.2-codex"]) { + provider.models["gpt-5.2-codex"] = { + id: "gpt-5.2-codex", + providerID: "openai", + api: { + id: "gpt-5.2-codex", + url: "https://chatgpt.com/backend-api/codex", + npm: "@ai-sdk/openai", + }, + name: "GPT-5.2 Codex", + capabilities: { + temperature: false, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 400000, output: 128000 }, + status: "active", + options: {}, + headers: {}, + } + } + + // Zero out costs for Codex (included with ChatGPT subscription) + for (const model of Object.values(provider.models)) { + model.cost = { + input: 0, + output: 0, + cache: { read: 0, write: 0 }, + } + } + + return { + apiKey: OAUTH_DUMMY_KEY, + async fetch(requestInput: RequestInfo | URL, init?: RequestInit) { + // Remove dummy API key authorization header + if (init?.headers) { + if (init.headers instanceof Headers) { + init.headers.delete("authorization") + init.headers.delete("Authorization") + } else if (Array.isArray(init.headers)) { + init.headers = init.headers.filter(([key]) => key.toLowerCase() !== "authorization") + } else { + delete init.headers["authorization"] + delete init.headers["Authorization"] + } + } + + const currentAuth = await getAuth() + if (currentAuth.type !== "oauth") return fetch(requestInput, init) + + // Check if token needs refresh + if (!currentAuth.access || currentAuth.expires < Date.now()) { + log.info("refreshing codex access token") + const tokens = await refreshAccessToken(currentAuth.refresh) + await input.client.auth.set({ + path: { id: "codex" }, + body: { + type: "oauth", + refresh: tokens.refresh_token, + access: tokens.access_token, + expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, + }, + }) + currentAuth.access = tokens.access_token + } + + // Build headers + const headers = new Headers() + if (init?.headers) { + if (init.headers instanceof Headers) { + init.headers.forEach((value, key) => headers.set(key, value)) + } else if (Array.isArray(init.headers)) { + for (const [key, value] of init.headers) { + if (value !== undefined) headers.set(key, String(value)) + } + } else { + for (const [key, value] of Object.entries(init.headers)) { + if (value !== undefined) headers.set(key, String(value)) + } + } + } + + // Set authorization header with access token + headers.set("authorization", `Bearer ${currentAuth.access}`) + + // Rewrite URL to Codex endpoint + let url: URL + if (typeof requestInput === "string") { + url = new URL(requestInput) + } else if (requestInput instanceof URL) { + url = requestInput + } else { + url = new URL(requestInput.url) + } + + // If this is a messages/responses request, redirect to Codex endpoint + if (url.pathname.includes("/v1/responses") || url.pathname.includes("/chat/completions")) { + url = new URL(CODEX_API_ENDPOINT) + } + + return fetch(url, { + ...init, + headers, + }) + }, + } + }, + methods: [ + { + label: "ChatGPT Pro/Plus", + type: "oauth", + authorize: async () => { + const { redirectUri } = await startOAuthServer() + const pkce = await generatePKCE() + const state = generateState() + const authUrl = buildAuthorizeUrl(redirectUri, pkce, state) + + const callbackPromise = waitForOAuthCallback(pkce, state) + + return { + url: authUrl, + instructions: "Complete authorization in your browser. This window will close automatically.", + method: "auto" as const, + callback: async () => { + const tokens = await callbackPromise + stopOAuthServer() + return { + type: "success" as const, + refresh: tokens.refresh_token, + access: tokens.access_token, + expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, + } + }, + } + }, + }, + ], + }, + } +} diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index f2ee91122..4912b8f74 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -7,12 +7,16 @@ import { Server } from "../server/server" import { BunProc } from "../bun" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" +import { CodexAuthPlugin } from "./codex" export namespace Plugin { const log = Log.create({ service: "plugin" }) const BUILTIN = ["opencode-copilot-auth@0.0.11", "opencode-anthropic-auth@0.0.8"] + // Built-in plugins that are directly imported (not installed from npm) + const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin] + const state = Instance.state(async () => { const client = createOpencodeClient({ baseUrl: "http://localhost:4096", @@ -20,7 +24,7 @@ export namespace Plugin { fetch: async (...args) => Server.App().fetch(...args), }) const config = await Config.get() - const hooks = [] + const hooks: Hooks[] = [] const input: PluginInput = { client, project: Instance.project, @@ -29,11 +33,23 @@ export namespace Plugin { serverUrl: Server.url(), $: Bun.$, } + + // Load internal plugins first + if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) { + for (const plugin of INTERNAL_PLUGINS) { + log.info("loading internal plugin", { name: plugin.name }) + const init = await plugin(input) + hooks.push(init) + } + } + const plugins = [...(config.plugin ?? [])] if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) { plugins.push(...BUILTIN) } for (let plugin of plugins) { + // ignore old codex plugin since it is supported first party now + if (plugin.includes("opencode-openai-codex-auth")) continue log.info("loading plugin", { path: plugin }) if (!plugin.startsWith("file://")) { const lastAtIndex = plugin.lastIndexOf("@") diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index f2545a747..32d7a1795 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -74,6 +74,7 @@ export namespace Server { const app = new Hono() export const App: () => Hono = lazy( () => + // TODO: Break server.ts into smaller route files to fix type inference app .onError((err, c) => { log.error("failed", { diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 0db453a22..c5792c16d 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -1,3 +1,5 @@ +import os from "os" +import { Installation } from "@/installation" import { Provider } from "@/provider/provider" import { Log } from "@/util/log" import { @@ -19,6 +21,7 @@ import { Plugin } from "@/plugin" import { SystemPrompt } from "./system" import { Flag } from "@/flag/flag" import { PermissionNext } from "@/permission/next" +import { Auth } from "@/auth" export namespace LLM { const log = Log.create({ service: "llm" }) @@ -82,12 +85,24 @@ export namespace LLM { } const provider = await Provider.getProvider(input.model.providerID) + const auth = await Auth.get(input.model.providerID) + const isCodex = provider.id === "openai" && auth?.type === "oauth" + const variant = !input.small && input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : {} const base = input.small ? ProviderTransform.smallOptions(input.model) : ProviderTransform.options(input.model, input.sessionID, provider.options) - const options = pipe(base, mergeDeep(input.model.options), mergeDeep(input.agent.options), mergeDeep(variant)) + const options: Record = pipe( + base, + mergeDeep(input.model.options), + mergeDeep(input.agent.options), + mergeDeep(variant), + ) + if (isCodex) { + options.instructions = SystemPrompt.instructions() + options.store = false + } const params = await Plugin.trigger( "chat.params", @@ -108,16 +123,14 @@ export namespace LLM { }, ) - l.info("params", { - params, - }) - - const maxOutputTokens = ProviderTransform.maxOutputTokens( - input.model.api.npm, - params.options, - input.model.limit.output, - OUTPUT_TOKEN_MAX, - ) + const maxOutputTokens = isCodex + ? undefined + : ProviderTransform.maxOutputTokens( + input.model.api.npm, + params.options, + input.model.limit.output, + OUTPUT_TOKEN_MAX, + ) const tools = await resolveTools(input) @@ -157,6 +170,13 @@ export namespace LLM { maxOutputTokens, abortSignal: input.abort, headers: { + ...(isCodex + ? { + originator: "opencode", + "User-Agent": `opencode/${Installation.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`, + session_id: input.sessionID, + } + : undefined), ...(input.model.providerID.startsWith("opencode") ? { "x-opencode-project": Instance.project.id, @@ -169,12 +189,19 @@ export namespace LLM { }, maxRetries: input.retries ?? 0, messages: [ - ...system.map( - (x): ModelMessage => ({ - role: "system", - content: x, - }), - ), + ...(isCodex + ? [ + { + role: "user", + content: system.join("\n\n"), + } as ModelMessage, + ] + : system.map( + (x): ModelMessage => ({ + role: "system", + content: x, + }), + )), ...input.messages, ], model: wrapLanguageModel({ diff --git a/packages/opencode/src/session/prompt/codex_header.txt b/packages/opencode/src/session/prompt/codex_header.txt new file mode 100644 index 000000000..70c2c6555 --- /dev/null +++ b/packages/opencode/src/session/prompt/codex_header.txt @@ -0,0 +1 @@ +You are a coding agent running in the opencode, a terminal-based coding assistant. opencode is an open source project. You are expected to be precise, safe, and helpful. diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index fe8c32f03..fff908088 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -14,6 +14,7 @@ import PROMPT_GEMINI from "./prompt/gemini.txt" import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt" import PROMPT_CODEX from "./prompt/codex.txt" +import PROMPT_CODEX_INSTRUCTIONS from "./prompt/codex_header.txt" import type { Provider } from "@/provider/provider" import { Flag } from "@/flag/flag" @@ -23,6 +24,10 @@ export namespace SystemPrompt { return [] } + export function instructions() { + return PROMPT_CODEX_INSTRUCTIONS.trim() + } + export function provider(model: Provider.Model) { if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX] if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3")) From 5c74bff8e13b2083a56404559072e1d422dd1760 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 9 Jan 2026 23:48:13 +0000 Subject: [PATCH 024/470] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index ff79ef554..ae10f6140 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 7c3dba045..9cab27432 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From b3e6b7a985972c385518b18f1283a9b90f43b2dd Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 9 Jan 2026 19:00:49 -0500 Subject: [PATCH 025/470] wip: black --- packages/console/core/script/lookup-user.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts index 33a18ebfc..15cbe80f8 100644 --- a/packages/console/core/script/lookup-user.ts +++ b/packages/console/core/script/lookup-user.ts @@ -38,10 +38,21 @@ if (identifier.startsWith("wrk_")) { workspaceID: UserTable.workspaceID, workspaceName: WorkspaceTable.name, role: UserTable.role, + subscribed: SubscriptionTable.timeCreated, }) .from(UserTable) .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID)) - .where(eq(UserTable.accountID, accountID)), + .innerJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id)) + .where(eq(UserTable.accountID, accountID)) + .then((rows) => + rows.map((row) => ({ + userID: row.userID, + workspaceID: row.workspaceID, + workspaceName: row.workspaceName, + role: row.role, + subscribed: formatDate(row.subscribed), + })), + ), ) // Get all payments for these workspaces From 8b287caaad7453c958225b8665ee67776f8fdeec Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 9 Jan 2026 18:06:54 -0600 Subject: [PATCH 026/470] tweak codex instructions prompt for codex plan --- .../src/session/prompt/codex_header.txt | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) diff --git a/packages/opencode/src/session/prompt/codex_header.txt b/packages/opencode/src/session/prompt/codex_header.txt index 70c2c6555..9ba3b6c17 100644 --- a/packages/opencode/src/session/prompt/codex_header.txt +++ b/packages/opencode/src/session/prompt/codex_header.txt @@ -1 +1,318 @@ You are a coding agent running in the opencode, a terminal-based coding assistant. opencode is an open source project. You are expected to be precise, safe, and helpful. + +Your capabilities: + +- Receive user prompts and other context provided by the harness, such as files in the workspace. +- Communicate with the user by streaming thinking & responses, and by making & updating plans. +- Emit function calls to run terminal commands and apply edits. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the "Sandbox and approvals" section. + +Within this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI). + +# How you work + +## Personality + +Your default personality and tone is concise, direct, and friendly. You communicate efficiently, always keeping the user clearly informed about ongoing actions without unnecessary detail. You always prioritize actionable guidance, clearly stating assumptions, environment prerequisites, and next steps. Unless explicitly asked, you avoid excessively verbose explanations about your work. + +# AGENTS.md spec +- Repos often contain AGENTS.md files. These files can appear anywhere within the repository. +- These files are a way for humans to give you (the agent) instructions or tips for working within the container. +- Some examples might be: coding conventions, info about how code is organized, or instructions for how to run or test code. +- Instructions in AGENTS.md files: + - The scope of an AGENTS.md file is the entire directory tree rooted at the folder that contains it. + - For every file you touch in the final patch, you must obey instructions in any AGENTS.md file whose scope includes that file. + - Instructions about code style, structure, naming, etc. apply only to code within the AGENTS.md file's scope, unless the file states otherwise. + - More-deeply-nested AGENTS.md files take precedence in the case of conflicting instructions. + - Direct system/developer/user instructions (as part of a prompt) take precedence over AGENTS.md instructions. +- The contents of the AGENTS.md file at the root of the repo and any directories from the CWD up to the root are included with the developer message and don't need to be re-read. When working in a subdirectory of CWD, or a directory outside the CWD, check for any AGENTS.md files that may be applicable. + +## Responsiveness + +### Preamble messages + +Before making tool calls, send a brief preamble to the user explaining what you’re about to do. When sending preamble messages, follow these principles and examples: + +- **Logically group related actions**: if you’re about to run several related commands, describe them together in one preamble rather than sending a separate note for each. +- **Keep it concise**: be no more than 1-2 sentences, focused on immediate, tangible next steps. (8–12 words for quick updates). +- **Build on prior context**: if this is not your first tool call, use the preamble message to connect the dots with what’s been done so far and create a sense of momentum and clarity for the user to understand your next actions. +- **Keep your tone light, friendly and curious**: add small touches of personality in preambles feel collaborative and engaging. +- **Exception**: Avoid adding a preamble for every trivial read (e.g., `cat` a single file) unless it’s part of a larger grouped action. + +**Examples:** + +- “I’ve explored the repo; now checking the API route definitions.” +- “Next, I’ll patch the config and update the related tests.” +- “I’m about to scaffold the CLI commands and helper functions.” +- “Ok cool, so I’ve wrapped my head around the repo. Now digging into the API routes.” +- “Config’s looking tidy. Next up is editing helpers to keep things in sync.” +- “Finished poking at the DB gateway. I will now chase down error handling.” +- “Alright, build pipeline order is interesting. Checking how it reports failures.” +- “Spotted a clever caching util; now hunting where it gets used.” + +## Planning + +You have access to an `todowrite` tool which tracks steps and progress and renders them to the user. Using the tool helps demonstrate that you've understood the task and convey how you're approaching it. Plans can help to make complex, ambiguous, or multi-phase work clearer and more collaborative for the user. A good plan should break the task into meaningful, logically ordered steps that are easy to verify as you go. + +Note that plans are not for padding out simple work with filler steps or stating the obvious. The content of your plan should not involve doing anything that you aren't capable of doing (i.e. don't try to test things that you can't test). Do not use plans for simple or single-step queries that you can just do or answer immediately. + +Do not repeat the full contents of the plan after an `todowrite` call — the harness already displays it. Instead, summarize the change made and highlight any important context or next step. + +Before running a command, consider whether or not you have completed the +previous step, and make sure to mark it as completed before moving on to the +next step. It may be the case that you complete all steps in your plan after a +single pass of implementation. If this is the case, you can simply mark all the +planned steps as completed. Sometimes, you may need to change plans in the +middle of a task: call `todowrite` with the updated plan and make sure to provide an `explanation` of the rationale when doing so. + +Use a plan when: + +- The task is non-trivial and will require multiple actions over a long time horizon. +- There are logical phases or dependencies where sequencing matters. +- The work has ambiguity that benefits from outlining high-level goals. +- You want intermediate checkpoints for feedback and validation. +- When the user asked you to do more than one thing in a single prompt +- The user has asked you to use the plan tool (aka "TODOs") +- You generate additional steps while working, and plan to do them before yielding to the user + +### Examples + +**High-quality plans** + +Example 1: + +1. Add CLI entry with file args +2. Parse Markdown via CommonMark library +3. Apply semantic HTML template +4. Handle code blocks, images, links +5. Add error handling for invalid files + +Example 2: + +1. Define CSS variables for colors +2. Add toggle with localStorage state +3. Refactor components to use variables +4. Verify all views for readability +5. Add smooth theme-change transition + +Example 3: + +1. Set up Node.js + WebSocket server +2. Add join/leave broadcast events +3. Implement messaging with timestamps +4. Add usernames + mention highlighting +5. Persist messages in lightweight DB +6. Add typing indicators + unread count + +**Low-quality plans** + +Example 1: + +1. Create CLI tool +2. Add Markdown parser +3. Convert to HTML + +Example 2: + +1. Add dark mode toggle +2. Save preference +3. Make styles look good + +Example 3: + +1. Create single-file HTML game +2. Run quick sanity check +3. Summarize usage instructions + +If you need to write a plan, only write high quality plans, not low quality ones. + +## Task execution + +You are a coding agent. Please keep going until the query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. Autonomously resolve the query to the best of your ability, using the tools available to you, before coming back to the user. Do NOT guess or make up an answer. + +You MUST adhere to the following criteria when solving queries: + +- Working on the repo(s) in the current environment is allowed, even if they are proprietary. +- Analyzing code for vulnerabilities is allowed. +- Showing user code and tool call details is allowed. +- Use the `edit` tool to edit files + +If completing the user's task requires writing or modifying files, your code and final answer should follow these coding guidelines, though user instructions (i.e. AGENTS.md) may override these guidelines: + +- Fix the problem at the root cause rather than applying surface-level patches, when possible. +- Avoid unneeded complexity in your solution. +- Do not attempt to fix unrelated bugs or broken tests. It is not your responsibility to fix them. (You may mention them to the user in your final message though.) +- Update documentation as necessary. +- Keep changes consistent with the style of the existing codebase. Changes should be minimal and focused on the task. +- Use `git log` and `git blame` to search the history of the codebase if additional context is required. +- NEVER add copyright or license headers unless specifically requested. +- Do not waste tokens by re-reading files after calling `edit` on them. The tool call will fail if it didn't work. The same goes for making folders, deleting folders, etc. +- Do not `git commit` your changes or create new git branches unless explicitly requested. +- Do not add inline comments within code unless explicitly requested. +- Do not use one-letter variable names unless explicitly requested. +- NEVER output inline citations like "【F:README.md†L5-L14】" in your outputs. The CLI is not able to render these so they will just be broken in the UI. Instead, if you output valid filepaths, users will be able to click on them to open the files in their editor. + +## Sandbox and approvals + +The Codex CLI harness supports several different sandboxing, and approval configurations that the user can choose from. + +Filesystem sandboxing prevents you from editing files without user approval. The options are: + +- **read-only**: You can only read files. +- **workspace-write**: You can read files. You can write to files in your workspace folder, but not outside it. +- **danger-full-access**: No filesystem sandboxing. + +Network sandboxing prevents you from accessing network without approval. Options are + +- **restricted** +- **enabled** + +Approvals are your mechanism to get user consent to perform more privileged actions. Although they introduce friction to the user because your work is paused until the user responds, you should leverage them to accomplish your important work. Do not let these settings or the sandbox deter you from attempting to accomplish the user's task. Approval options are + +- **untrusted**: The harness will escalate most commands for user approval, apart from a limited allowlist of safe "read" commands. +- **on-failure**: The harness will allow all commands to run in the sandbox (if enabled), and failures will be escalated to the user for approval to run again without the sandbox. +- **on-request**: Commands will be run in the sandbox by default, and you can specify in your tool call if you want to escalate a command to run without sandboxing. (Note that this mode is not always available. If it is, you'll see parameters for it in the `shell` command description.) +- **never**: This is a non-interactive mode where you may NEVER ask the user for approval to run commands. Instead, you must always persist and work around constraints to solve the task for the user. You MUST do your utmost best to finish the task and validate your work before yielding. If this mode is pared with `danger-full-access`, take advantage of it to deliver the best outcome for the user. Further, in this mode, your default testing philosophy is overridden: Even if you don't see local patterns for testing, you may add tests and scripts to validate your work. Just remove them before yielding. + +When you are running with approvals `on-request`, and sandboxing enabled, here are scenarios where you'll need to request approval: + +- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /tmp) +- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files. +- You are running sandboxed and need to run a command that requires network access (e.g. installing packages) +- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. +- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for +- (For all of these, you should weigh alternative paths that do not require approval.) + +Note that when sandboxing is set to read-only, you'll need to request approval for any command that isn't a read. + +You will be told what filesystem sandboxing, network sandboxing, and approval mode are active in a developer or user message. If you are not told about this, assume that you are running with workspace-write, network sandboxing ON, and approval on-failure. + +## Validating your work + +If the codebase has tests or the ability to build or run, consider using them to verify that your work is complete. + +When testing, your philosophy should be to start as specific as possible to the code you changed so that you can catch issues efficiently, then make your way to broader tests as you build confidence. If there's no test for the code you changed, and if the adjacent patterns in the codebases show that there's a logical place for you to add a test, you may do so. However, do not add tests to codebases with no tests. + +Similarly, once you're confident in correctness, you can suggest or use formatting commands to ensure that your code is well formatted. If there are issues you can iterate up to 3 times to get formatting right, but if you still can't manage it's better to save the user time and present them a correct solution where you call out the formatting in your final message. If the codebase does not have a formatter configured, do not add one. + +For all of testing, running, building, and formatting, do not attempt to fix unrelated bugs. It is not your responsibility to fix them. (You may mention them to the user in your final message though.) + +Be mindful of whether to run validation commands proactively. In the absence of behavioral guidance: + +- When running in non-interactive approval modes like **never** or **on-failure**, proactively run tests, lint and do whatever you need to ensure you've completed the task. +- When working in interactive approval modes like **untrusted**, or **on-request**, hold off on running tests or lint commands until the user is ready for you to finalize your output, because these commands take time to run and slow down iteration. Instead suggest what you want to do next, and let the user confirm first. +- When working on test-related tasks, such as adding tests, fixing tests, or reproducing a bug to verify behavior, you may proactively run tests regardless of approval mode. Use your judgement to decide whether this is a test-related task. + +## Ambition vs. precision + +For tasks that have no prior context (i.e. the user is starting something brand new), you should feel free to be ambitious and demonstrate creativity with your implementation. + +If you're operating in an existing codebase, you should make sure you do exactly what the user asks with surgical precision. Treat the surrounding codebase with respect, and don't overstep (i.e. changing filenames or variables unnecessarily). You should balance being sufficiently ambitious and proactive when completing tasks of this nature. + +You should use judicious initiative to decide on the right level of detail and complexity to deliver based on the user's needs. This means showing good judgment that you're capable of doing the right extras without gold-plating. This might be demonstrated by high-value, creative touches when scope of the task is vague; while being surgical and targeted when scope is tightly specified. + +## Sharing progress updates + +For especially longer tasks that you work on (i.e. requiring many tool calls, or a plan with multiple steps), you should provide progress updates back to the user at reasonable intervals. These updates should be structured as a concise sentence or two (no more than 8-10 words long) recapping progress so far in plain language: this update demonstrates your understanding of what needs to be done, progress so far (i.e. files explores, subtasks complete), and where you're going next. + +Before doing large chunks of work that may incur latency as experienced by the user (i.e. writing a new file), you should send a concise message to the user with an update indicating what you're about to do to ensure they know what you're spending time on. Don't start editing or writing large files before informing the user what you are doing and why. + +The messages you send before tool calls should describe what is immediately about to be done next in very concise language. If there was previous work done, this preamble message should also include a note about the work done so far to bring the user along. + +## Presenting your work and final message + +Your final message should read naturally, like an update from a concise teammate. For casual conversation, brainstorming tasks, or quick questions from the user, respond in a friendly, conversational tone. You should ask questions, suggest ideas, and adapt to the user’s style. If you've finished a large amount of work, when describing what you've done to the user, you should follow the final answer formatting guidelines to communicate substantive changes. You don't need to add structured formatting for one-word answers, greetings, or purely conversational exchanges. + +You can skip heavy formatting for single, simple actions or confirmations. In these cases, respond in plain sentences with any relevant next step or quick option. Reserve multisection structured responses for results that need grouping or explanation. + +The user is working on the same computer as you, and has access to your work. As such there's no need to show the full contents of large files you have already written unless the user explicitly asks for them. Similarly, if you've created or modified files using `edit`, there's no need to tell users to "save the file" or "copy the code into a file"—just reference the file path. + +If there's something that you think you could help with as a logical next step, concisely ask the user if they want you to do so. Good examples of this are running tests, committing changes, or building out the next logical component. If there’s something that you couldn't do (even with approval) but that the user might want to do (such as verifying changes by running the app), include those instructions succinctly. + +Brevity is very important as a default. You should be very concise (i.e. no more than 10 lines), but can relax this requirement for tasks where additional detail and comprehensiveness is important for the user's understanding. + +### Final answer structure and style guidelines + +You are producing plain text that will later be styled by the CLI. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value. + +**Section Headers** + +- Use only when they improve clarity — they are not mandatory for every answer. +- Choose descriptive names that fit the content +- Keep headers short (1–3 words) and in `**Title Case**`. Always start headers with `**` and end with `**` +- Leave no blank line before the first bullet under a header. +- Section headers should only be used where they genuinely improve scannability; avoid fragmenting the answer. + +**Bullets** + +- Use `-` followed by a space for every bullet. +- Merge related points when possible; avoid a bullet for every trivial detail. +- Keep bullets to one line unless breaking for clarity is unavoidable. +- Group into short lists (4–6 bullets) ordered by importance. +- Use consistent keyword phrasing and formatting across sections. + +**Monospace** + +- Wrap all commands, file paths, env vars, and code identifiers in backticks (`` `...` ``). +- Apply to inline examples and to bullet keywords if the keyword itself is a literal file/command. +- Never mix monospace and bold markers; choose one based on whether it’s a keyword (`**`) or inline code/path (`` ` ``). + +**File References** +When referencing files in your response, make sure to include the relevant start line and always follow the below rules: + * Use inline code to make file paths clickable. + * Each reference should have a standalone path. Even if it's the same file. + * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix. + * Line/column (1‑based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1). + * Do not use URIs like file://, vscode://, or https://. + * Do not provide range of lines + * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 + +**Structure** + +- Place related bullets together; don’t mix unrelated concepts in the same section. +- Order sections from general → specific → supporting info. +- For subsections (e.g., “Binaries” under “Rust Workspace”), introduce with a bolded keyword bullet, then list items under it. +- Match structure to complexity: + - Multi-part or detailed results → use clear headers and grouped bullets. + - Simple results → minimal headers, possibly just a short list or paragraph. + +**Tone** + +- Keep the voice collaborative and natural, like a coding partner handing off work. +- Be concise and factual — no filler or conversational commentary and avoid unnecessary repetition +- Use present tense and active voice (e.g., “Runs tests” not “This will run tests”). +- Keep descriptions self-contained; don’t refer to “above” or “below”. +- Use parallel structure in lists for consistency. + +**Don’t** + +- Don’t use literal words “bold” or “monospace” in the content. +- Don’t nest bullets or create deep hierarchies. +- Don’t output ANSI escape codes directly — the CLI renderer applies them. +- Don’t cram unrelated keywords into a single bullet; split for clarity. +- Don’t let keyword lists run long — wrap or reformat for scannability. + +Generally, ensure your final answers adapt their shape and depth to the request. For example, answers to code explanations should have a precise, structured explanation with code references that answer the question directly. For tasks with a simple implementation, lead with the outcome and supplement only with what’s needed for clarity. Larger changes can be presented as a logical walkthrough of your approach, grouping related steps, explaining rationale where it adds value, and highlighting next actions to accelerate the user. Your answers should provide the right level of detail while being easily scannable. + +For casual greetings, acknowledgements, or other one-off conversational messages that are not delivering substantive information or structured results, respond naturally without section headers or bullet formatting. + +# Tool Guidelines + +## Shell commands + +When using the shell, you must adhere to the following guidelines: + +- When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much faster than alternatives like `grep`. (If the `rg` command is not found, then use alternatives.) +- Read files in chunks with a max chunk size of 250 lines. Do not use python scripts to attempt to output larger chunks of a file. Command line output will be truncated after 10 kilobytes or 256 lines of output, regardless of the command used. + +## `todowrite` + +A tool named `todowrite` is available to you. You can use it to keep an up‑to‑date, step‑by‑step plan for the task. + +To create a new plan, call `todowrite` with a short list of 1‑sentence steps (no more than 5-7 words each) with a `status` for each step (`pending`, `in_progress`, or `completed`). + +When steps have been completed, use `todowrite` to mark each finished step as +`completed` and the next step you are working on as `in_progress`. There should +always be exactly one `in_progress` step until everything is done. You can mark +multiple items as complete in a single `todowrite` call. + +If all steps are complete, ensure you call `todowrite` to mark all steps as `completed`. From 075fa2c0e81eb6497b78c91e008f0ac1396f369b Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 9 Jan 2026 18:26:32 -0600 Subject: [PATCH 027/470] fix: instance dispose issue --- packages/opencode/src/cli/cmd/tui/thread.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index b9654d339..5e50d38de 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -34,9 +34,15 @@ function createWorkerFetch(client: RpcClient): typeof fetch { return fn as typeof fetch } -function createEventSource(client: RpcClient): EventSource { +function createEventSource(client: RpcClient, directory: string): EventSource { return { - on: (handler) => client.on("event", handler), + on: (handler) => + client.on("event", (event) => { + handler(event) + if (event.type === "server.instance.disposed") { + client.call("subscribe", { directory }).catch(() => {}) + } + }), } } @@ -140,7 +146,7 @@ export const TuiThreadCommand = cmd({ // Use direct RPC communication (no HTTP) url = "http://opencode.internal" customFetch = createWorkerFetch(client) - events = createEventSource(client) + events = createEventSource(client, cwd) } const tuiPromise = tui({ From 8b8a358de1005970c3316c0a9ff0088ad1474232 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 9 Jan 2026 18:45:03 -0600 Subject: [PATCH 028/470] update docs and auth methods for openai --- packages/console/app/src/routes/index.tsx | 6 ++++++ packages/console/app/src/routes/temp.tsx | 3 +++ packages/opencode/src/cli/cmd/auth.ts | 1 + .../cli/cmd/tui/component/dialog-provider.tsx | 1 + packages/opencode/src/plugin/codex.ts | 4 ++++ packages/web/src/components/Lander.astro | 1 + packages/web/src/content/docs/providers.mdx | 20 ++++++++++++------- 7 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index a59a8f8db..830bd9c95 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -195,6 +195,12 @@ export default function Home() { Claude Pro Log in with Anthropic to use your Claude Pro or Max account
+
  • + [*] +
    + ChatGPT Plus/Pro Log in with OpenAI to use your ChatGPT Plus or Pro account +
    +
  • [*]
    diff --git a/packages/console/app/src/routes/temp.tsx b/packages/console/app/src/routes/temp.tsx index 28883fca7..68bda67da 100644 --- a/packages/console/app/src/routes/temp.tsx +++ b/packages/console/app/src/routes/temp.tsx @@ -91,6 +91,9 @@ export default function Home() {
  • Claude Pro Log in with Anthropic to use your Claude Pro or Max account
  • +
  • + ChatGPT Plus/Pro Log in with OpenAI to use your ChatGPT Plus or Pro account +
  • Use any model Supports 75+ LLM providers through{" "} Models.dev, including local models diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 232cdd576..3dd7bcc35 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -294,6 +294,7 @@ export const AuthLoginCommand = cmd({ hint: { opencode: "recommended", anthropic: "Claude Max or API key", + openai: "ChatGPT Plus/Pro or API key", }[x.id], })), ), diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 72b12d99b..8c4d9e446 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -36,6 +36,7 @@ export function createDialogProviderOptions() { description: { opencode: "(Recommended)", anthropic: "(Claude Max or API key)", + openai: "ChatGPT Plus/Pro or API key", }[provider.id], category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", async onSelect() { diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index f098a3967..c83f27861 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -411,6 +411,10 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { } }, }, + { + label: "Manually enter API Key", + type: "api", + }, ], }, } diff --git a/packages/web/src/components/Lander.astro b/packages/web/src/components/Lander.astro index 605cea638..0e522158d 100644 --- a/packages/web/src/components/Lander.astro +++ b/packages/web/src/components/Lander.astro @@ -78,6 +78,7 @@ if (image) {
  • Multi-session: Start multiple agents in parallel on the same project.
  • Shareable links: Share a link to any sessions for reference or to debug.
  • Claude Pro: Log in with Anthropic to use your Claude Pro or Max account.
  • +
  • ChatGPT Plus/Pro: Log in with OpenAI to use your ChatGPT Plus or Pro account.
  • Use any model: Supports 75+ LLM providers through Models.dev, including local models.
  • diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 58aa47ede..882500b96 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1071,29 +1071,35 @@ To use Ollama Cloud with OpenCode: ### OpenAI -1. Head over to the [OpenAI Platform console](https://platform.openai.com/api-keys), click **Create new secret key**, and copy the key. +We recommend signing up for [ChatGPT Plus or Pro](https://chatgpt.com/pricing). -2. Run the `/connect` command and search for OpenAI. +1. Once you've signed up, run the `/connect` command and select OpenAI. ```txt /connect ``` -3. Enter the API key for the provider. +2. Here you can select the **ChatGPT Plus/Pro** option and it'll open your browser + and ask you to authenticate. ```txt - ┌ API key + ┌ Select auth method │ - │ - └ enter + │ ChatGPT Plus/Pro + │ Manually enter API Key + └ ``` -4. Run the `/models` command to select the one you want. +3. Now all the OpenAI models should be available when you use the `/models` command. ```txt /models ``` +##### Using API keys + +If you already have an API key, you can select **Manually enter API Key** and paste it in your terminal. + --- ### OpenCode Zen From a44d4acb3f76a6f62de73313cdfd66b064366eb5 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 9 Jan 2026 18:59:57 -0600 Subject: [PATCH 029/470] tweak: adjust codex styling and fix hint --- .../cli/cmd/tui/component/dialog-provider.tsx | 2 +- packages/opencode/src/plugin/codex.ts | 119 ++++++++++++------ 2 files changed, 85 insertions(+), 36 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 8c4d9e446..8b586fce3 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -36,7 +36,7 @@ export function createDialogProviderOptions() { description: { opencode: "(Recommended)", anthropic: "(Claude Max or API key)", - openai: "ChatGPT Plus/Pro or API key", + openai: "(ChatGPT Plus/Pro or API key)", }[provider.id], category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", async onSelect() { diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index c83f27861..2101d5bc3 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -98,45 +98,94 @@ async function refreshAccessToken(refreshToken: string): Promise return response.json() } -const HTML_SUCCESS = ` +const HTML_SUCCESS = ` - - OpenCode - Codex Authorization Successful - - - -
    -

    Authorization Successful

    -

    You can close this window and return to OpenCode.

    -
    - - + + OpenCode - Codex Authorization Successful + + + +
    +

    Authorization Successful

    +

    You can close this window and return to OpenCode.

    +
    + + ` -const HTML_ERROR = (error: string) => ` +const HTML_ERROR = (error: string) => ` - - OpenCode - Codex Authorization Failed - - - -
    -

    Authorization Failed

    -

    An error occurred during authorization.

    -
    ${error}
    -
    - + + OpenCode - Codex Authorization Failed + + + +
    +

    Authorization Failed

    +

    An error occurred during authorization.

    +
    ${error}
    +
    + ` interface PendingOAuth { From e342795bd064d734e6e91ca411b2fcd26397d072 Mon Sep 17 00:00:00 2001 From: Spoon <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Sat, 10 Jan 2026 02:01:13 +0100 Subject: [PATCH 030/470] docs: add url based instructions to web docs (#7216) --- packages/web/src/content/docs/rules.mdx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/web/src/content/docs/rules.mdx b/packages/web/src/content/docs/rules.mdx index aa5590bb5..2d02ff47f 100644 --- a/packages/web/src/content/docs/rules.mdx +++ b/packages/web/src/content/docs/rules.mdx @@ -92,6 +92,17 @@ Example: } ``` +You can also use remote URLs to load instructions from the web. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "instructions": ["https://raw.githubusercontent.com/my-org/shared-rules/main/style.md"] +} +``` + +Remote instructions are fetched with a 5 second timeout. + All instruction files are combined with your `AGENTS.md` files. --- From b03172d723ad570b8c82f010b1511de89d630d9d Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 9 Jan 2026 19:15:50 -0600 Subject: [PATCH 031/470] fix: ensure gpt-5.2-codex has variants --- packages/opencode/src/plugin/codex.ts | 10 ++++++++-- packages/opencode/src/provider/transform.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 2101d5bc3..d0f025b06 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -1,6 +1,7 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import { Log } from "../util/log" import { OAUTH_DUMMY_KEY } from "../auth" +import { ProviderTransform } from "../provider/transform" const log = Log.create({ service: "plugin.codex" }) @@ -321,7 +322,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { } if (!provider.models["gpt-5.2-codex"]) { - provider.models["gpt-5.2-codex"] = { + const model = { id: "gpt-5.2-codex", providerID: "openai", api: { @@ -337,13 +338,18 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { toolcall: true, input: { text: true, audio: false, image: true, video: false, pdf: false }, output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, }, cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, limit: { context: 400000, output: 128000 }, - status: "active", + status: "active" as const, options: {}, headers: {}, + release_date: "2025-12-18", + variants: {} as Record>, } + model.variants = ProviderTransform.variants(model) + provider.models["gpt-5.2-codex"] = model } // Zero out costs for Codex (included with ChatGPT subscription) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 080da6378..08f329ec1 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -318,7 +318,7 @@ export namespace ProviderTransform { // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai if (id === "gpt-5-pro") return {} const openaiEfforts = iife(() => { - if (id.includes("codex")) return WIDELY_SUPPORTED_EFFORTS + if (id.includes("codex") && !id.includes("5.2")) return WIDELY_SUPPORTED_EFFORTS const arr = [...WIDELY_SUPPORTED_EFFORTS] if (id.includes("gpt-5-") || id === "gpt-5") { arr.unshift("minimal") From 8a43c249347c8b3bad0e500d0054b70f46582252 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Jan 2026 01:25:30 +0000 Subject: [PATCH 032/470] release: v1.1.11 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 7385c1472..d7ca5d83e 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -98,7 +98,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -125,7 +125,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -149,7 +149,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -173,7 +173,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -202,7 +202,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -231,7 +231,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -247,7 +247,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.10", + "version": "1.1.11", "bin": { "opencode": "./bin/opencode", }, @@ -350,7 +350,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -370,7 +370,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.10", + "version": "1.1.11", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -381,7 +381,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -394,7 +394,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -433,7 +433,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "zod": "catalog:", }, @@ -444,7 +444,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 4c5ea8ec5..d3bf8e2ea 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.10", + "version": "1.1.11", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 74b38fff9..701802fc9 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.10", + "version": "1.1.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f3bdb2ca7..e55ea92f4 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.1.10", + "version": "1.1.11", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 6a8138c64..919f57a92 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.10", + "version": "1.1.11", "$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 165a97f8b..f858b1f90 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.10", + "version": "1.1.11", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 83eee9e6f..945ef8acb 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.10", + "version": "1.1.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index a83eacd52..3517e9fb7 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.10", + "version": "1.1.11", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index c38343af8..d77d0ba06 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.10" +version = "1.1.11" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.10/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 937a906af..fc1c51ecf 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.10", + "version": "1.1.11", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 22bbbe0fd..7d2c5f04b 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.10", + "version": "1.1.11", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index ae10f6140..65a8da460 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.1.10", + "version": "1.1.11", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 9cab27432..4e73da7d1 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.1.10", + "version": "1.1.11", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 2fcb82773..6d4afbb61 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.10", + "version": "1.1.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index a84a237b5..f563ef57d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.10", + "version": "1.1.11", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index e284e0e3e..9e38897fb 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.10", + "version": "1.1.11", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index bf3e96802..03541fb47 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.1.10", + "version": "1.1.11", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 3cf1f7705..dd51e0ef1 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.1.10", + "version": "1.1.11", "publisher": "sst-dev", "repository": { "type": "git", From 6da60bd5d9f9764d8549aefa3d1e0f8ba1b47973 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:05:13 -0600 Subject: [PATCH 033/470] fix(app): more defensive dom access --- packages/ui/src/components/list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 60161f6dc..1283b3023 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -67,7 +67,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) if (!props.current) return const key = props.key(props.current) requestAnimationFrame(() => { - const element = scrollRef()!.querySelector(`[data-key="${key}"]`) + const element = scrollRef()?.querySelector(`[data-key="${key}"]`) element?.scrollIntoView({ block: "center" }) }) }) From 58eccf7f5358ad58e47f2c5d0ad8c5aafd20f2ea Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:52:54 -0600 Subject: [PATCH 034/470] chore: prep --- packages/console/app/src/asset/black/hero.png | Bin 0 -> 28790 bytes .../console/app/src/routes/black/index.css | 195 ++++++++++++++++++ .../console/app/src/routes/black/index.tsx | 174 ++++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 packages/console/app/src/asset/black/hero.png create mode 100644 packages/console/app/src/routes/black/index.css create mode 100644 packages/console/app/src/routes/black/index.tsx diff --git a/packages/console/app/src/asset/black/hero.png b/packages/console/app/src/asset/black/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..967f4ac6e506b58561f2c0d450cceee249b03d45 GIT binary patch literal 28790 zcmeFZMOa)*)Hb>smtetyM{p;&CrE(c?v~&LcWnZ}EkJM$?(W*bEfCxSbRf99cmK`# zzxzCQbOzs@+`-wCZrIe4cfGZ$YE>2CDoQdqSmam$0N}{UN~r+=9^xy8BL*_!qsT<~ z4dNqIML|Ov@ri?jLqbA=f`WpMj*f|mNk~YDkB|TC*|XHtR7OTdK0ZD!E-o4x8ZR#| zYinyEAt6ywQC(eKGcz+|V`F=J`|$8^IyyQ^O3J{%Kq)Dyn3$MXuU^H)#mUObYH4Y) zva-H;^Cl@NDI_FBO-;?s&CS5TfSsM)+1VKk1}i8iC@U-b`uZy6Y>NYcL#dq9TMf^} z{U4tZb^!r74<)1}NRMCtcaO;>^vypl2jL9_8k+y>CsTFQ#@Rh zM>zra#uRV=KggL6LIyp`c@Cd@r}Dp-L;N2+{|4TF@cbJ(n*YJ`A3Xnxz`wryk3Ijv z^REc}>&ySx^B+9_ion0V{Et2V!Sk;O{OimA*z+Ge|BArBzWo0kd$#O72vNH^pPZq0 z3p<&1Kh3F?#ek*YkHfFEbl2uzCN5|SyC03%geob92~?d8qCc)X|KAT(ELYaLgDQlN z2B)Wb=DoD(KQAF3_*&kBcpa=Ic!dW7v(Ztmp8lEW$>YzSqJIiS!^$Mf z9#{CjGT)gsbxVgzxY!rPqvdzcO@v{tS^66-cG>Q*Dn=}*QsSj%#^KbSIQxS6rIr_c z?GfB%L~P#9JOAC2uLC7$ufQcrypnr)GZ%0&bEXWj#NkNIR4u#q1#j!QYD66b0#+U& zJV#H!qsUD|)~$E~C43dL2;hAWF@h;}VnS_ov3#7g&g=*gRg!uURl7wT;~!nW6$o)( zI?bRrcU5yc8>-XNKct#3*t>qpw~i;mrm%ee+Dk7<IqVSl}dex^O<5B?#WjPh+_ zwy`iB5ADK+Q6ld!$eM!*ENCiCdeY}d~ zJeae}Dp@tB92&Tz4PH}P{?XQruyLAoAwalng6xsQt9YfH}T00Na+ z6zJ0D`7A+gHrG+RR*^V7@0Z61shhrY#GrZQj*R`aM1SWe=Ty=qI*OH+L6%%c>~5vs z)ZADA?QuLvMHD&lYLXMk3IM9b0PT}k+pI5;x<)HW?k$e&ebPfd12NhRpMA;fii)M) z%qUG==0^F!$I;z?8%%~y+p7-_!UuHHLQivXi`o`i%$ax?4RU2JX3IZQcgHsH@on5E zeYGGZtxDu`SI?GKz?1LW_g|^MOyevacTGc}jfv`5ct7pz|Lfysu)huw1Rl(ALBdWp z?u55-rbH20LGf$$J@le?i$I1Tsne>$HJO-#{0%Y?u#e>FDPnx}_O;?)@21oe9{CF` z?kD^Y`l79$6N%$&cucKV0q+fV7UqBcdV&ZtMr>Z7P!T0CB~>|2PQ<>T)p7AeQW0+7 zXz$slmT1wPhasY=0Qxn6rL}HoCat+aDt+bs>iJYb`q@YZx=Q=Dk)Vf% zs>Xe}J&|cR+Al=(iBPN0_fzG^=O&)mYNXQF9(=O1C`FBBT-gsjWW(~{FWCJ{!JLUH z@65zH0gwYgluQwjl)t9=TcsSOP#qKXc;HGd2tE0aW=Aqv`+JtsA@ou( zb<#?`JLe=%0K%pP)aquirO~OcA-Iq!2I=~hr$&{tAQn#ZN!5k*Tox=b>uKXwc{6Rn zyS8vXP4Z@dx)W3{@Eybh0M9Hi>nZocMqaU7+iYwsJ+b6gv^G)!Vkgu(%vT<|OcFOM zMB|>eB8@B<4uu;pMbEAj>4I!cPT*A}kXhiiY%ljwN^=8{G)=ChME!M&IGrF|Ce5y7 zBFz}nHy22v3bc9r1)Pu@MSXfA;*iwIbY+3dvX4D`fP!Xs=ZiL22>PsDjy zaaIAn#dT$51mnI|qv(C`a&zwHk0_F+^}EQSJQzyNo|~%42V>UH12=4_$MZzwW-D+! za)sI~@=91mL=QQ?uiToHulGGmr*aF@Yz_x(m77%;*=hmrxMjibAAg%w0DiLI8kEV} z2**Uj1^T3S)YW&dG%@z$v$w0={(e1wu>YC6+_RL zmbSXq#+G|blk2PZdtX>gaJ<*y58aaqvz@(vY!ZS_iXOp`WeVuyUHwQx;6c}3P06r7 zkbJUx8^38-Il>+TPf=u8^3qlVQQ#OP1T8Tr5ti3@t2(6nz^inS{cIoFoG(-PKC||} zDjWN>4tE`fmR~nbFW!A}=Gl&gFIT%Bq5a}3=7aqcb!+(?HpBBOHtkr!oXFK4BN5vL z8DZiG1Ax1ek;YVDQ4LM@JMgvpi+zh1Dok+(uWW5oE7iDvvA3ot^#hY zpN%w&)k1SR5A^Y*@b?uJ7V3F#!S`LtI<;uOoa#jc>ryfw$^6_LO{cuMR)~MUwYf%@ z*r_7#LOV~8wXz}H;i*q7sWGqHkL35ZuHBO+aMi`_UJ#}}7F5+s-60N<68NLmdBu1y z9PYY8AEv+Sw=6C^$c?iPMSwRn7@qNNhr>Ov2uQCoQ@5 z7b)5NyjcBKaA$l_QUVV7e?%i6$Fj@RC1bDJ^Q-N%kIv8)JuHZx$(IAZ56R>Bv!# zV#;xh&(3o{*`k^1viyuZkP?FUaVMo6I>WQ`YbkzQCH`tDk@8=&D!p*;nAKB$RUkCo z^|CipsDyV8ewSP@=z7<6bCSUMm=8VCOse>Xp*9V~G$ArJfyBB15Bv<*29 z*GkCMr#d$g7fdW?i8@+CzKzR1v*J`xTZo!%IlPc{8QhddjGO}k0(<~}U@Joe@5J#1 z$aDU6N&fO7e6_uPena*X^^IL;-`@W7xs5Zoe38-a5xsFnc?+PvN0NB6YypwOF+k0# z8Qgte01;04l24ip-0r@<3R3srr4yz&_aPyzv3RJ`l2{&VqD504=usQwD{}Rsyd9Z} zZ2=yOqARH!n^vY(394_G&w`v?VMgDQ6f5{|nkIS6(R8po#x17#4kOLwE;-uc*wuy( zm1rG0qm|>=r(!X#gX*z}>R7LB68;L`wW3R$Bl!Jl;%!R=Kg-D<%Fk|Ap*b|hNu$6v z$&<%HKokRFjJsi7)lMk^pDRXznl8whR|pN~$Hzc@0ijs?h_1G)T7|lD){!goD~t@( zM)j-X+WN1B_TrCLzcEI3q*Denc?g{g>Yza1>dLaIQSCs-@7tEb=PY`x>5>e+eGQ|! z%y-x9)FaF{-7vhlu!3ZCy|01oKoBB7ptTG7QKFPy!Zb^=w zQw?k+@isz?>o0s?~G)N zv<6*$DhT%xk#1?VUwXQ@R(<8u&S1iX?!7?(jvi7Kqk1{BOEmenAT+6%M$7BH5$dFt(Vl3s{OC50MgRsA#NCXfOl&s0M=i1DD zlIU+p|A`oUzKwLF-t;+E4)xD!?r-&8$DA?cw8UQ}pzy9LtKg1elQT8+z(b}N^%v99 z@lCBsP7cg~aAwg!rKh+JroaXh4$?9Iq@Zs>VVh)yb+oOV@v-9vMsJZ*6!NJm%Wr*h znE_5T<#of8u{!(#uwV)V!vM(D6Nv4(|62o$JV?@ zwfA4{g^Tu|&|*IGDDRTcji-3C)%-ZH0lrH>`=zD~bl_uB0AXhtCZhFg0fGGFQ02R* zr_=`n!)^N;&G$a4LGEbr0kJF^3Hz{2l_bZbXweBB7Cr^oHSLs~oaib;??zR?LS#`132@DAZ^8IQCk+}HZx zw=GkRnH>FwGi9__&+s+)h=LL(>f2@wof0-SgSxIKb8$S@z!QUfM^X-?+%F_kvHZH;9kdNoR zvrAE#l59twcIdxEh?c^DNL`c_gLGU&l6LNY?Qc#A^&mu-py_}*>4M#i5_7;t7Zv>K z_2PnqRy1b60?K(@!@6a8;_fLJKwY6ei@fq?m;&y)HsUg^YM~p!=6%aeq6oRYPauW3 zkZD)UdalO74?FkHcRcQujrigs@xjCi+nV1)R$F<(zCPitn|%%#FVDwL-mgaTgzr|e zctR-3284SI{3qANXv}{>6DV4b*SqNAD?NqBx{|%?IyEl3CFnSM-d`jw3&N5i!*{DR z0Y4W}%}dmEdZ<#G=ENIc`)feabRKal&vKed?0iaVOwr;L!J4LIs#A3%@lGv zn0(W6DKN~ZU);)q5(Ua~XP2Q#>}Q<-M)f;>yP4%JjGa`wqmw7T^wo6A`<~yI)754^ zuok1Rb2XwSSCshuXcTwrm5W=5jPo%Jxt>Y>GT!tz|EF(dm$w7Sik}*#VW^0mUqYnn zDTc+g>FM!$ssK`5k@Ywytc@?ibT5Zi{ni!g&YzKt-G4bM5k#6|duH!UhAk9DBgMm; zIPR#(LJ2=Jlevr2{kCCR@G|~=-}`lSJ?b)#g}mIHrm${_1@8ToP7bRX$ zqobOeZ-2}(@qk-V*J{D&uv9z1E%iskAb|$Th^eOXjdlTnd3Bofk82u=5L~-YmOpVA z6D3NMN~zU*Zgp7;O!K(7G(|l<;vplnxhbYTD_t5T*a$j5tBB^}xJPfC71xZ3nrOCq zEG{Tbb9+}9E3$>roJM!OKj72rU9PgAnGy6G!oJU2yz4&J(skx3<3MMS)2MH3LD8#v z0T}BVxAlC?VH9z`Ug+|i>9?|cxYrtB>{8@}wMZU`N@nh<3cHZO|9AI=4Cm@P z59}}e>+MyE%1aK9#A+5t<%{z-DRyos<1l8Zh8 z@`@-n0wozUHPj8~rde6)`Dv&z#_zvDK47i;Yt?9tn|gOrS7a3i`1(=(q}C8{ZHjrh zk(W+$EQzTqvLAFJJ~Q?atxdLW^)>8fB#v?a1LI!b{+~pPf}?V~_gT|H7FT|^5_Nt@ z#{FI23eJ|ie?%)DTsRaiVDUo`zy!D;RVy%fFkToV9jCinXzE}*4ccFFm>G}HOH}~v z;l_U}zz=YMgeo`HE3&U`C5zp%KZLAvN}{fJO^-dm9cs69ZcXY%VZ`F{*p-QC9cEmK z-gm!)?{NZIjZnMpU~ntmB|rFDE%1BpliE|;3NS~yT^ty3bOG89P-3`f?Gu~mPLNrP z;##P@@ARTcH)N3EZo%a=@LS9NhTG)~Ie`D?NSe*%*CwWM5tiYsLEAF0e1(jF9zq$1k4H~B-;*z( z`LtH#w@=?omQzQY4CeQJMNFalsg@Y2=A+^!{#>Qbaxt+*h*$cr0|4nWJzW_E*+HT8 zl*QrJBF;I@_W26bgB_CN@0Ki2e-lpidwqXyXsw3=O=m72jgfv%@zX6LdNjd(Cqv0{ zgX8QVj^yeMjjDcGP^Qx46PuZ7gE{Tl>KR|(R=?W>Fn7{eU2w}sAu;2aOt_JG6FU~< zW10A>J>%g-XK~5n(T?jWfR+Ax2To}PEo;%4hpqH#{yT~R>V-IX^Q08e zS+ZEV6%->ht;5A5d7uV6-W>`$$<}pR^_VcSu$PuLO`O44Z#C`L z?atpE!Hwl;4AhdsD$=G_N^Z65G%-6xA5W&Zk_D7Z0V4Ief;zGfuB=2BN9KY`= ztqrA~oCu)+Y2U9o3KVD-v4W`!%cM^xDbg|gdl*_>FmWPRVf_b5P?gpw4&|+drr+2X z#?cFNzF!{W>D`^&t?Mcl#Hi8iEb@5Kns$m#EM*Z{|A3>uAPAaYsOUFHZJHhWnfG=| zitzKYb4_BQ3nG8bxFAd&eqmq1KRMh(fcoSD-vRo|V5ucPr!^hNL{qQnL7g(X>yt-D z|7u%Stan;W;JZnU!`O7wgL+gyMW-`E7oWez2&MtYx-xR?k@`st*RVW#W=` z?X|;N%{R{FQ-^84B^>0iM(gkj_Gz|R7%$(HRXnB=NCyfOHIgj&gaqkWl@94Rnt0-k zumzXUytIKjBu(1d1IHF9J8x~)mU63bmZ$)bEQV=bxt7$l1@&9MvA zY#R65869*j&S|3BduQeT_3wKu&yT@(!mFv(De++|wkxTqk-6~8-Xo&OZ>^;Xwe`@< z7kxiHwa(Y_dS$o->khvqgu5qx5P> zrsjK;AyGCOxw0zpx4IcWQ&Y5yr+D|?t~fgmi}8%3$r`FRfrocd!?CVQk1LfXH0ZS- zD23y&I`)I>R=c!xuZMOT1_ZDBDrkW(VzGXgQEZ2VK23(;2@mc;=W2N}WO=-I`| z^ZmYvh{+W42`p|ttp7SGYDOdu@ zbWHd7xh=Hf*3vg9*5?+VqL;bc$-M+KtNd}Z>a~9it0bz)A&QBx6|gz#Tw49XcPmIw z8VPjW)vIN<-KhIvKv$>6&ZBnkc9l8{jald99g}Z!Y59EW_h>)Mx`;b#c>}!-|6N)O z;;~Za-q2Nq#2kk7Jz&(CV086q0P3nN^qy~$)1(8yFQI~uq*p9L(M>>}sV&P-Adi>w9yAbllCXZFVb8McJP;&-4!@ zcC=PE%Qw667h?-p`Ny+9t6<;~JkOkntVcn0Zqwht%QsF#rt6IY9azv!{Z56s`FFBo zTF#ey3oCQsI6>WIBtPkO{}_%{=;`b&o#FZv8+z&!VJ=QqHmh-MZb)Y1`2gvnTETib zIFGQCcgTn3qi839iO}p_ee$8GOt!NIpQ)WuaeTA2Fb){CTO%2e8C4j6-G4MX@7Xu~ z3H`lEX~!GD|H)P#H4$F2@n?^sMKM%J11ucA|tM250_rtcuE_1 z)$o2?pD}&m+5E@8_X7$Pow?mHdEiHil4s^<%}?bywR3N|Y4|it`?6}g_en-ESS+*r zbq@>LYERje1}DRA*X0NE)0Ot-CAeE|wjc7A6hhgXSWrs%PBU%MWfoKtFiOd)=t$wp z=E!fx@x6skMOt$6896a%G;k=mYNs6owj5&hTGChBRflgP%?|9_8I=iMYQo2SD364W zcSTj<2cHKoWR2-5)WiU8-r($FI7b7WyOEz`BYbbY+iR5L(K}1R_NzZ<`_TsFi!*dn z!y_pmB*$a1QCzR|G?vLou}Qc*+sFGhvUDhFw#wb^mtW~Z&jInM!X?hB*P?x&$2z+A z?iCc~613`#sp=$4VRN_Bc*qtn^;w!V9`;@dSyVy?9&n^ryCWTk+Z4iYtNbwjDCw_B zAD;ytNYvXp9ME3CA|o&2}4T`K5e^|!fv)j z5e>8#udpNZ!*Agd`imj`!rYb!MO8t}|4hMi7^a?I_8z?ci*)h^)QtOK7(FvjSf(pb z>hoe$%w9Q9_<6S>!Yy@M(=j)t$`LKFR_pnBd7zkwv!_xm?CRE_eP<%Y5Pw|pfq_6d z2tu~rgB`IHkX8)8v$H-7Oj|97&+_r~Uvc3hpyd2kLU?!PhF)WHRF$zU3M4Bjq?{`w zaaCm&k8TU}Rn%RjP z(xlz80&p=PlEX)Q*ki>R^XayUN{3n8^tc@M#SBII-ra(UF+aGvzo6F_#0nSO<$Z!B zQmr;IXX5s^-~(L?ZR<%5rq7>1ReK4`GA^DHkzM+IKSzCltvami%%fedHR_pr`Ge=Q z(*ABw<+A~Rh$)({C}q7Z z`qpP@_Pgs;zR;1zgMjWv2eZHL(66ISFi`*l7nyEzw9RF_Lhl|F;hxDh;7iME4^%o# zEY-DBwTKM;LeyP3_~k8jaLKt({14`dmd-Ml%%&6C^QV{0@H`PyJo;BY!+Y`Jo*)v0 zfO}Fs2Jj#L9|`=u_MwDDvY}iL;oFVGPa0LW!~`hdx}8&M1L_zLW8oz@%-`j9$AZiVT`8 zupPdYM}vHwi+kl;6VsX`lKB8*Avzo^9iCx`mIGxe-Pv`rmSokQ7f8cDNP6*dqdks1 zPaKArNdq*mqL5`y=lMzoIo{O1&>q18dHQzm22n1$+>u+3S-g)~ba%uJ7GARcy)lBK zl=yMFcKWZk|A$JZqoAr|BrAj*j6Yo%kY!|au_Pb!NZ`Kz@>Ai03cdZ7@lDCtH`pbO zX@mcBro`)<-kyFA`AEls{Ese$?H$!WBB6-J&^0gyM4nIBSs_^75cD9UoyJ+Kti|9= zxvbLYWin0t`v#m=^Fli~XL_$=SgofpU2RsZcISN;>f_AJ=J+`?2L?o6i^@yuay7yL zgc#q(s8b*ZzpqE18?5os!6)L2VvzYsIAdWlCyZQ{<&bvx$Jyr}oOEWpKX{a$E zOMhJWs|KeQ-erH68fESNYZHu$z~IbwYgxUk!F~dKWIU11zhU}=Uds|IY=c}?(G$Y=R{uLqF#8X%`6=1a)1Vk1hh82KFCT{2uDt5~l zYIe4EZOo9RvqtXXuPxf$1j?zfO&pqB+}NlfH(JHpMM!H0r<;T6f>{#lqpo(4!S(myc}LhEXL{ok$xRo%d-$6sJ5b3sr4A# zH__Aqq>>C1(J5?RoxS@KargnNvr?mJw~BqN&UY!B+HZPQYL!)IUa7~N0Uqs&x5#qA=_`O=Mpf9^PWq4a zMB7RMYejD_YO2Bxzv9Ap7h*aWuzmv)z$KHmY>*x0_*5=UOCk)Sl66YBTGMcpgc zV66oS^w%aw#L_Lc(9g-0|Lm@!H>_9DS2bc&{qs-%I;JJzS+^7e@DijgXV_r<7Q587 z;f3?)ca;p7pzDY73S|Dj&Gt_%SKe&r-X?XOXq+dT?(sy=w>rqxeSGF3&q ze<@~rk=fDqN0ktx8)%o*4=JW~l~nuqq-@AS@h=?S&Xfk{uJo zRAHgy$kX>3KA&2fi?^3Q&u7bIP!zYG!maH*r9lX!ey9Saus3$>$Y)8HH&&e9WZip< zOwyu2!C&n9LaYT742>}=(-%I;a5MA>B~+9Wi*iq$U7{S#mFkpYx6&+57HBrESr*q; zbff>XiLgW|d3V4^8er}Gh0Om9BY`he3#EgX4v$f(NU8QWzf3|!jspqzNo^$#2;^x# zqmo94otEU-US-tc3(wofc%f0k>hw)|@$1omu;%Tpuu>@!(y?9$$=HV)nD8=n@QwXy zh!)@97#Zw`u2adqtD9L7gcI${Twz?mlmUR-oo;*{#~+R7vA^?&K_x@Vhhn<#FH3Qs zYkuon1N=F0miY3KA##nL(?kf$O=1)PN?A}B|nK5wzKsANW0RhfTZ zW9M|GSD1wz`+|l>L+d$E^@F)k<3V|8atODy*(4VZt_ezb%sigyh`9aDk62mG%Ik)a z+3WA7F@*@dU5}l-@pC};N#HG$T_vS@l^Ll_d8#A>?)wpeO%SP5uW*%KP_t%|wV*el zuxq@v{?TjGM7k0D)WWvU`a8_}88b^QEI52drw`$ywf^GL4?2IY3AnuFS*81 zo#!+S`o%*t%trcQt#XUuV%)Vy2s|OmxP<2^B<%HIOtK4pR*WD~v?gNynvDWg)-G$T zovwp*^Xkg`tii#g#XU;Q8XE4Y^`Y{+yo{mYYyZma zui!2hn|ME&ay)_n?W-RpLYi<(hQ`g7XhQU-lmpFhUm9gynZPo_K8c3`>pxPK(1(oo zSMVCP%-<$(#Mbo*RKxsH$x6{>ZmL|9NP@e0#t)BGNZkl0n@QpJwD6HzF^=a}h6hh5 zlhF2EFwma$o-g7k;bPTCx0rLBcx9y4+{S~c{nn&*+DkRxNA3?|Fd(mo`L)7nT)%fe zyX;wO+j4O{?=`IB#LyL)@a02 za$D80{gcI;gOAqp1+oV@W(>W2N9T3CMU4^39y*^S=4NEKu05I3J60C? ze|axNoyWS~VkwYmlj^V3vgbGb=atGA?HvzNkf&WH>43%qzY)(Sak6WcmOh7AI_u9P z)F^HUI8wXhmS#z9YbMcOV!?}t7lDRn85ht+syCNj=)JKtdGW#Q$^)Lwaiu4Ij*HNUsskt`$7F3kW2ZM{ubHgK0=MP! zbWJJ<4GS~=wJ|3@wPxMP>Cj)(uPqfj`Pzq5Vy5c!K^WTghkV z7&*e@gHdAb)h?!$vVzUS!b6LOg_17GtB{Z(TSctijjMkszD_C4!RdqH;vQ**S zxKo5pZq!GOwWPJ;Mbs&gmfQWjG#rTP3m_Svw6WmZCna~+(9CVn%ek8lherdHs|W%f z@ZrB+4f};5wA96Jz9BQz=+L)`*p(8rYXCVY0{vHLmfsxYH)TCiKQtu!J}L(ry#%@_ z?(o4=FGeeLt+Vh0mxiA%BcP6ODBP(E`zYS4n4_a-$L^6TPbuljx`5Xn~Y4$a7%3}j$J7)Vc{JN z2wA~W{7+e#EAqVJu}1=nK&&DYv$6)7-qJ3{rs03qBnpqzLe$_227R^cv^jnYv)r+4 zC^Yngstir#01^~v)TtfQ?iU`{CZqlE-TSRa01=+MpkG_;Z%96)(pi)MR^l@mjW}P) zH2qf9$jJywx$PvfTA(tC1w_hx_1CDDt4vj+BEKUVI-YrhQU6>Zdyh@Ieeci*2Qjw)dO9KHO?uPv-4 zp_@5Ih2;1mD9q8{5KLrhMAoO|KF__OFxpLA2;#W6=L71hIguPOyY(u+&D3Bto%2$4 z^~^kOnR|Xg=5}}q*rHHhXKq(jUZz7Di?a=_OZ(q~vJ@rBM9YRMCTe#XEzO8amyi#( z#-eLf5tOV>b&wbJi?bxjoY}Q`G>ZBNvim+A>Z8?oS|T!cB8&pX5!5$z|HfUt*uqP$ z_5FLs@j4_C;Ul^2_?eyc@ww)~L5bcYZ=t{U&`e&6A1k{#1ib@RQetM}UgLi1Y|%t2 zL*6iOll3#r(Ip5_#(f>pSO8@@`ZbkdVIuk1dm+Rakh^iP|B(NI)7WQCsAHwqoxaIK zDjwv;b0E3evP@o?tO93eS*SwuPD*g#Xn%PkK8=$I%|rr+lQKoF(pAXdE6XYzXXz{` z{TJhsTVlr`{14JweSPP_O?)Z8p`$f>*LygoBZfkN2W1AJ+;$+YsM#vzpwO7re^5!Lcpm9B<5w#W{az4<07j!~B zTTH!kWL)|Ci2&qKtWF^ymK$Z}s1HmC(J($^c|*7^Za($9wO$!DAM-fR@M9`K!wBJD z0uSNRCH8^$f=-Jf!otF~*)WfX?UVWFyA}uq?~2RPL`@TAyPND@^KX!!>P zNu($kt;LN_h26lGVQZy*Z96~9wmye=3OSP~GH$gXPMj%{Zwfo(l_%xoVuR4_Zdrd$ z2oDU0mc z4y%cZRlM$3B=nbjo^rJaE*6y?@{puOIO{2c~ zh|LuPX!^wYmeUyMAu`8~95C(l;=b|5zN*YFrvszA*slBi@Ac!IgaiZ=J~Kua@-*~W zHQJ2UH5XN{Tu9NK8$0OB|EzwtEXw6J!E0LS3{MOq9z}$|@M4%QS#DkcsU^hjXvV;A zd7%uJeLd$@4j+>={b))}-3PC=(4&Who*-1Zoe;EH8=88J*i)RSb}hm7+KKTtR-VKm zIJ1)D4m+BzGK{I8sx2>_72N6=U~!wNkpI*>EA6ZI{R?S%-I9F1LqaF+?6JS?$$CZ6 z1qT_{-bK*B%Cg3nh3Qda?d1-I06fQc_harj>WNeg$boJCmxvuU#ru!n?dg7ep4F5- zu~J0XvlvqE{frYN);sxSH5xoEoz~M|&!5Hf^l18a&|7l(_FjJk<=O=Iu(#{S zw?qZjrTHxWnQfEbc5trbr|Vgd5h!(E%Q!!AHd7e(Rj9BcH7m}tROG+JbtvF2SDZB~ zdSL5a8K)g78@YQjUt6>m{JHGs?Y@Soar7DCJE?t2m(_N$(Wo5iGE3Lu1Ho8$Jxn?_ ztJ}s0|1DAm=T55r36P?pp-cmC7CH~126ZRAG1c~m(S1G4HqScD)78*rnutx2jy`!h?y6TN z-Kk2}B%aX+)`NEx*V=~(huKsNuY=j@b5j7CwOu zeC2s_#}>?OYmO(WH%ktsdrkG!u@VfWk~hhMpoz()oFiai3XswW!g%MWR!1>F)yA#< zKp2FsAI=vI*^Df%J-nEXES=JNS%yRpL4R5uzOSL;e-ju!Lp0#*<5avfZ~$*DAYeSz z3PWUEqK+3Jf5(}$aM97dvdM6{=P@GbKGw%V4(O_q)~mtaVu31k%8y)#q&pGTYCk?o zySK}_YanVlswBDb3Q`D(tL6ob>4%)hhiM^jkGq#dLGVaNcxSU=9v5kDYO{ROACd_NQh^_u!t*`3O7k89DDp*a;j>iz zd7dzv(yeEzLBXF;JZB=w%L1cv+2^Ue?t>8d^f{=lfeJLnVfoHU>D0IxBkD0^yU$A@ z6mJoPRo#q!1+o4^CCOT(sx>#uoY2)Fcl#je~qjLOhtSvfd_oAMGj!{?a3zs+hR$qzSrSEj!5!~Os zsy&6Dq}Fu4acD1jLGz;RK~TdxJ;1WM^g1cp5q1kR(?2_SsGu}ylR;1cCU1YFzkzN8~y5r0h zL)mo=am2i(-Dal|TJG?LYiloR={>miDiEz7%e0;$8v!WJ^|HhPKoo}jH>+&}r z&F|KTh>NZle0GSc9B|wW2-uE(;y4p32wotl>NTGe_UC);;`W(3y+SNpzp}Q5A#`scfWHhah{)eV*^v6h|kYp2Vv$GGk){0rWaoUppCd&dz zyI{h3+TZBd2qj|}0cRwm)`qNcf{~qFYc~5x773P-l7MeRZ|Q}HfZxeS0pr(N9+aQ@ zi=HLp#9nNJI-jPOE47Yz&73jwWxIxJ4)?2AzD#e;0zK})B5;Zh-i$o^Ow1e0cg##O z1}oDsCFM+4e3yfMEk{Z5^K;uq{l-8T`dV087W!0O+L4S5MnF{`{Sj=L-6F91dzZZZ zhz08`JcRk46d?~GP>`!=BKb>xu~+kF^|cL_PfKEg4CSTXxFH4dr_$d@tcbEcO-ISr zaip>_2%H?XB<_hvMjm))k~K>6HU9h?*?Xxw@yu!jN zvBh;;INWTwzbd$0J#Uu{XBN77#Jy-IA*~88KvBWs(wvK_q~szYiX$yv0`O&jU&_XJ z!Xz4kfP`Li;>|C1rc?A%1{8?4Y(G3fkZdO{N?sR@G}2#@;jt_W<#|g>N9GqU--^Q< zd&L)?Z@%l7`T%0$xey=%^lPCfDMhDpQX?x$81#(`r+Q(EjqFT!nH`_Jji*!D>{qAn z;*DrEXK-eR^m-#R=opZoEMfO90A3VuSq>k_E)!LGW2n>VYB4q3xpW&#%;$$;x@^_9NIH~TN=dptBK3zH z8aQ(Liqw3D&)DmgD$#wGQR&@dX%e7-s{+64Kis~m$WH5jLxUAyiyfL}O2T`c%o#zR zX7iOX6u6cSI~lkLzSb7xs$1oa+<8375|E8aD-ntWEfMUY2#@F-ZOo16e=`#3j%+{PidVBt8otOzta6}5NF|v- z%Jg(bF5SAXDy^paT=I!DHxmlP5-Xn`PEHx8k+>13u@_1I5+NG%HH?uzE>+(0O)Rx( z5V3#BuIZx}jv;TDsWAe!Hi;Zu^P?$b6%~2IP(a7n&*Y@xFW;$M{KobWpa#}2Ww%Dk z3TjhF4TJyhv3(UoQBud&@=ok<#^coMsE zIGw=2kKn=UpqEs|jjI-~O`GnoG<0_!>ldSBAV&{BU>^_XGljM4gfs*rz*yorFKN(B z?Lg;@s$q{Ga*2*oua55Wv6LAu$=YWvKlQ&uQT}sp(5`+w)*@*zr+6kb4#94XKj$;d zUjHD)A!MVNaUrOt7gXFBw66Dk3;5=m>ZzPBORg=l>sOnnQ}HN8dIyS%m(*l*J53IM zPvh|_9HL}_AJp$&;k}-^H9Rvb6@)($y=%TL?}E?t85X35<>T@;x#9Igwo8K5^>o~y zsGo9IH}{60RhkrQ27RB?O6Jd4J?r=t1bq6-7beDiUs9I3W%yj%gSxyo?Ku!TtyM=P zN!r74F(*P(?!`ORj{j@GX7Q+zEI2>-ERdtRwP?4iGW~wvX!iEPc&=smkzFCO6%7qF zcwEIP)h2^T)eUh=&u0$1OE<#0Cl_aDH(TyU+6wyE-qJnadv);+1F}xE+P$ZH8_`r^ z@V29>7gp8Cq0~S#Zx7Fb^`x2G?JaJ4nOF1I?&{B3?uyIRH7Yvz&5$=H-d%9~+@rC) zdn6M_zJ(`+*}nQqEMzo7*R{XD*3j4RR~b`SZS0KhXG9 zs76o`YV~3Hf9dRPuDiuclW&rf<=#E5clf5i2W(D^AYdTDB593rbXzRa zD!bo)QJc`t*7pQ0Ry3-Mj`7}5mD4XyZS>fdF>t!6azbtKYPnCdBt{+Yu2bU=i1?Ci+c; zzR?`zk%6VB9AB+6+;vHOQd|@{ioSQ&j0!2u7Y#XJu)Dh8JJSjgTMAwBX*1;NG#RnH zprK#0*twt$p_vBH=e&G9!S&h0=0od6*I?mFR>7KD6D&cX9hU^*w?R^`8MARn(&vh2 zB7aS|X21>t#*Jqe)|F1N0Th}KGSe^FGY1WVlGh?)*_7&99S?}%}|5Ly-g zz+ULAhB)JQo0+y*f-fB7OXL~2r3HVfaFZ”!+% zJKVoTz(@G`S!ThK=KiZME9fK{guVn$TNlO}?DwH3-#^r!?`Fq0rEl3hUN^s6MP6&@6cVcVwDw7BNg|cWf_v=7dH}gTYO{2^K!1c&7E}&v7gA@aiWa8OSp~H>3iC zg9UtiQFj|VtT7%et1SNJr?c+4a7mGdP$EZGHl15b7hEHA7cnaSW180H%5{8V;+fsI z{L%8nF}6=X3o#*|l%`aMMu(#S5n1!PUvwMJb^aeMvMiMuKb_{?7E#@0aSpxs?D0WM zt5AL6R$^xjx5(g5SpG7}bNjJF$YZ4&0er~Foay#Jmi0XN;}n(k>sN@~$_3h6cM`q? zgVWm|xUo2^-SeSzD@40;`xY(#T=%!ffDn4x%p)po_%$r&2ef>321u#d+abefRS~Y| z#-gmm?$~ac?_wVj(4?~*%RJVt)wR#dAI-covDwThB0HU@A4*kX6MWQFOZ;16_3LV# zZVaksqv4B*)NghP3eHORnF3q>z~yQF5~}>=k>a$v^JOihZUqED(S@&af79bjf{4D) z!QoDAwM#$Pr1zeY@wXN^C>MS;te-G+1hN?R9PNNvw?X9NU-^5+voCNU$ViSj`k{Ef zNVnQdJPX*lJH%MZy|4qgUXmvcJ(C`CC*EUceU3N`i2)&9=kckWT2{c)-W zd4PD^7N=ty2FVX=mHR)2g!h9BXr39lIvSq!_gTd(Bd}ycm|3`BvwbUXWpG9x`hVIv z&!{H0EshT$!oBo>7myN4Kzau$O1t!`7*yZ^qErhYy-5HIT?CXSU813e-Zi;^^deFN zN|ch&q;~@I!hP@4`@nkZe&&3fz0QAT{bsLo&g|KHcRqN{*luEpHHqoi-z zGi#(o0-$MnlVzmW;n170Oi5npvFU9uV8G-x&ag-YVDbyHqlPc?Tc|cr_EVl6dDW@L zO-_E(G&{UD*X!9`pC_X2!_5=jsY10sk^~-XQum8*<*)P!uJu;VN=q5f zY6;Czxl^31bChd!VL*tepMQ6^_QFjyVt(QM`Qd}5Vdrxr>`$47RVbU(ETxT?!(qsJ z>Jz86$Ij{$cty2X=(-wEttk&FtzrcI1&lr*2cw$bd^OKbz^E;FNv+p^o#S9I)gJ%2 zbt#1=75S>ot<59PVJ$-}7AyU-s>vv({A9<(Zl=v|042dhY}2K|*}Q>y1(q=MpIT@R z#CyJu>g(s;USuh?hrZUHucoMw)nFxUTsQCU^^gv5{EJJs zOd%vvk0KzC?-u!k_}au*ySd$Lr)W`BLG^;(38Cf)#ZF%5hU-EjEkH?;zU_q`%^B_t zA5u@g!g*UtK>(9}?O*E>`tW-(VvJ?gl7`0}vky|cQiBz_W3-Il&pdN7nlX^wlLbEt z$gmVTQv~pZKHi?>j!auvmMzE&A$ zXtr9=yCCwrNRhh;o42ilWTw-qI+$$fc@<5#-D4xq@BbxfrKEld@ZJP^!A~qvqaN?d z23*W2Z{Y?L2$&l)Ic@xAN-!~t`YGRusJ&gC^0h*C;ibOT?Sw~~(w218CDd*bCtB=cx8&Xg?z4S}KkmmtdQa z?9Ek!jA{$;VB$f?*?$$LTtaL8*sEZ`^twnH-@C@7lvA556RN}+ND7f>1As9s;`*7ouVt1P&06|mT#g)rf9N9_1v3bqfRTH*nYDu>Rp|H zp}*e5ANUe7mZv8ndgmL{bB%69g1ojd$ws3)C}2@Y!6y0!>2=d(ruLjZ(4FqD-ripK zie_AlLe8WH9=McN#NF9cT`0ShHoWJuCEAoD{D=TfDQh_-gW0xY($8_=8S+VNBKi=K;g`wcf(+#ZJ>>--q^*%L z^IDPLMi=tt=TN@ky$^aNc$M%DPkyay?mnLg!t*zd-@Z8_i(a)b#4el<=t8J)el4I9 zE?K7w3m)iUQRe|+m$A;y&O1yTnuTIMrH7x_&0_h&JL5%$^Q$u@)>G-OHofe+L7O2J zALeDawSInlKZjXqJ@&JK{-OPKls>3N`+%hXelDjEQNs}+FE3BGyNi7*XHVG)sz&oo zS>AqU75FEq(gOEVwlTivp2#L9p!C(_o3l_N_-MJRZ3=j!SyXvU+?9w2=r}s&dKfMnJ$n2LFJvJz-pe2lFEuMDA^DXFzFzJFx&a z<=PKAv@l>lz8v0bepPWjIpoi5OYx{c7NID$@}f zj9uk1W~rqF*7g$HJ@>nll)aVsXUPfQ~(o?8-42jq{Z!|?n!p5 z{@?Y7KlbsHSMshoMy$SNx18%3b^4q-gp*O5axJn0Y7CJ9%KtLBS8pD+X7z9FnWmeq5()mP0*} zcVB`@@D7msG)WJ%QINb>?4xVVyIP}Ni%OTO72=TrWONK!1z9t)4jzR)_?RxRfv82@ zgia=%=K$(M43ot&V%#|G3d?Pv;$AeSV+G0E{G0&xvoZPZUDfpHQ6a0P@9;!5*M^lK zTnf35P`#I7av?>injXKB{MEY51_3>L{)Wh7f3J1(*vMOq`v9Qs8wv&=nxdRz;2DBF zH0wdbYAYSF<0ckMYSNHtduN+&Nm(y#Ak5$KO0n4Vl9$al-eI|mKdNSJ;Zcp}_exk{ z3!T4;9=4T5DW78iUT+S?+_h5Hb#_EK@m}T9iRss>oIj^*((G3jKYE#RRq5HS^D`Ab zr2A{+!nfw-g^J2Q>C$e_BV1Cv*O{~5J*u;OZ1&|X=}>ym(Qj8rVK5r~4fZhw;Q}YH zJF>2H#_j1ER919?^(qvAx`=SXu~}bX$oHy)OIr^z0&TbY@pQhp0 zuUV(XaXvp~1q1^x+*KY6=PDSi+`rl=>pE^ERk_T~Nj?u;A;?H}RIDVIk7bJ{ITnAp zGb++6b$j4*<$!kDK|dT9Bw|sZaK3NEvT;l1gm3J&%%rg0hpwT-{GZ-PeU@f> zN*fm8z?Oxq_fSgF3$L5qEnLk&B~HTB5n}4*#0Vx@fO?e|>{XE%UI&AU_QnX;FOcQ- z-ZVmA)l0fW`1 zT}T*Rl6&b%(MWT+s(u{A2CKVhk?%|V0H2Wddh(i~M3HeCua#JCa!fG;z6*C0h;lkP4efB>*c!e@;9Q%!)Pc zf#55!^U)M{vNJuRadLLSxjW$G0uxYfH`QY;k-Smx3#=RZJN8L_nEx44!Jd47Uz!Cc zXp8>&XyN7lJ6Ram$0$YobD5%Y8k&>TGC=bf9iRKpb;SPLP#$;%J`hCxbDiJM@SFzk z49{tFiq7zy;W?GSsm3$!Im2@*fm4lV-gAcMR05|O&%Eah,XHJ*9T8J<%KoN7Gt zo-;hB5;)a(<~^t4F$(rPsptpz=D`Q%94Ehp|Ie{UtxZXfwH((xsZ#+Ab github()) + const starCount = createMemo(() => + githubData()?.stars + ? new Intl.NumberFormat("en-US", { + notation: "compact", + compactDisplay: "short", + }).format(githubData()!.stars!) + : config.github.starsFormatted.compact, + ) + return ( +
    + opencode +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + +
    +
    +
    +

    + Access all the world's best coding models +

    +

    Including Claude, GPT, Gemini, and more

    +
    + + Subscribe $200/mo + +

    Fair usage limits apply

    +
    +
    + +
    + ) +} From 7c06ef24772bb787232fd560c25d08b1dcf31af9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 10 Jan 2026 02:07:24 +0000 Subject: [PATCH 035/470] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 65a8da460..ffd083cbe 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 4e73da7d1..03a0baa0a 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From e30562d5f4e7bf4b76c6453fa3904d0dc753081d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:18:42 -0600 Subject: [PATCH 036/470] chore: prep --- .../console/app/src/routes/black/index.css | 26 +++++++++++++++++++ .../console/app/src/routes/black/index.tsx | 24 +++++++++++++---- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/packages/console/app/src/routes/black/index.css b/packages/console/app/src/routes/black/index.css index 737cfca90..eb0ec87d3 100644 --- a/packages/console/app/src/routes/black/index.css +++ b/packages/console/app/src/routes/black/index.css @@ -105,6 +105,32 @@ transform: scale(0.98); } } + [data-slot="back-soon"] { + color: rgba(255, 255, 255, 0.59); + text-align: center; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 20.8px */ + } + [data-slot="follow-us"] { + display: inline-flex; + height: 40px; + padding: 0 12px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.17); + color: rgba(255, 255, 255, 0.59); + font-family: "JetBrains Mono Nerd Font"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + text-decoration: none; + } + [data-slot="fine-print"] { color: rgba(255, 255, 255, 0.39); text-align: center; diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index 6fa2cbb5f..c83ccd251 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -2,7 +2,7 @@ import { A, createAsync } from "@solidjs/router" import "./index.css" import { Title } from "@solidjs/meta" import { github } from "~/lib/github" -import { createMemo } from "solid-js" +import { createMemo, Match, Switch } from "solid-js" import { config } from "~/config" export default function Black() { @@ -15,6 +15,10 @@ export default function Black() { }).format(githubData()!.stars!) : config.github.starsFormatted.compact, ) + + // TODO: Frank, toggle this based on availability + const available = false + return (
    opencode @@ -143,10 +147,20 @@ export default function Black() {

    Including Claude, GPT, Gemini, and more

    - - Subscribe $200/mo - -

    Fair usage limits apply

    + + + + Subscribe $200/mo + +

    Fair usage limits apply

    +
    + +

    We’ll be back soon with more availability.

    + + Follow @opencode + +
    +
    From 8c3cc0d44777b10edc4a03366f2c3de58d1a8f72 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:09:26 -0600 Subject: [PATCH 037/470] chore: prep --- bun.lock | 1 + packages/console/app/package.json | 1 + .../app/src/routes/black/workspace.css | 214 ++++++++++++++++ .../app/src/routes/black/workspace.tsx | 229 ++++++++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 packages/console/app/src/routes/black/workspace.css create mode 100644 packages/console/app/src/routes/black/workspace.tsx diff --git a/bun.lock b/bun.lock index d7ca5d83e..9aba2f10f 100644 --- a/bun.lock +++ b/bun.lock @@ -87,6 +87,7 @@ "chart.js": "4.5.1", "nitro": "3.0.1-alpha.1", "solid-js": "catalog:", + "solid-list": "0.3.0", "vite": "catalog:", "zod": "catalog:", }, diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 701802fc9..0af730e9f 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -26,6 +26,7 @@ "chart.js": "4.5.1", "nitro": "3.0.1-alpha.1", "solid-js": "catalog:", + "solid-list": "0.3.0", "vite": "catalog:", "zod": "catalog:" }, diff --git a/packages/console/app/src/routes/black/workspace.css b/packages/console/app/src/routes/black/workspace.css new file mode 100644 index 000000000..bf9d94878 --- /dev/null +++ b/packages/console/app/src/routes/black/workspace.css @@ -0,0 +1,214 @@ +[data-page="black"] { + background: #000; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: stretch; + font-family: var(--font-mono); + color: #fff; + + [data-component="header-gradient"] { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 288px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%); + } + + [data-component="header"] { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 40px; + flex-shrink: 0; + + /* [data-component="header-logo"] { */ + /* } */ + } + + [data-component="content"] { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + flex-grow: 1; + + [data-slot="hero-black"] { + margin-top: 110px; + + @media (min-width: 768px) { + margin-top: 150px; + } + } + + [data-slot="select-workspace"] { + display: flex; + margin-top: -24px; + width: 100%; + max-width: 480px; + height: 305px; + padding: 32px 20px 0 20px; + flex-direction: column; + align-items: flex-start; + gap: 24px; + + border: 1px solid #303030; + background: #0a0a0a; + box-shadow: + 0 100px 80px 0 rgba(0, 0, 0, 0.04), + 0 41.778px 33.422px 0 rgba(0, 0, 0, 0.05), + 0 22.336px 17.869px 0 rgba(0, 0, 0, 0.06), + 0 12.522px 10.017px 0 rgba(0, 0, 0, 0.08), + 0 6.65px 5.32px 0 rgba(0, 0, 0, 0.09), + 0 2.767px 2.214px 0 rgba(0, 0, 0, 0.13); + + [data-slot="select-workspace-title"] { + flex-shrink: 0; + align-self: stretch; + color: rgba(255, 255, 255, 0.59); + text-align: center; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 25.6px */ + } + + [data-slot="workspaces"] { + width: 100%; + padding: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + align-self: stretch; + outline: none; + overflow-y: auto; + flex: 1; + min-height: 0; + + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } + + [data-slot="workspace"] { + width: 100%; + display: flex; + padding: 8px 12px; + align-items: center; + gap: 8px; + align-self: stretch; + cursor: pointer; + + [data-slot="selected-icon"] { + visibility: hidden; + color: rgba(255, 255, 255, 0.39); + font-family: "IBM Plex Mono"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 25.6px */ + } + + a { + color: rgba(255, 255, 255, 0.92); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 25.6px */ + text-decoration: none; + } + } + + [data-slot="workspace"]:hover, + [data-slot="workspace"][data-active="true"] { + background: #161616; + + [data-slot="selected-icon"] { + visibility: visible; + } + } + } + } + } + + [data-component="footer"] { + display: flex; + flex-direction: column; + width: 100%; + justify-content: center; + align-items: center; + gap: 24px; + flex-shrink: 0; + + @media (min-width: 768px) { + height: 120px; + } + + [data-slot="footer-content"] { + display: flex; + gap: 24px; + align-items: center; + justify-content: center; + + @media (min-width: 768px) { + gap: 40px; + } + + span, + a { + color: rgba(255, 255, 255, 0.39); + font-family: "JetBrains Mono Nerd Font"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + text-decoration: none; + } + + [data-slot="github-stars"] { + color: rgba(255, 255, 255, 0.25); + font-family: "JetBrains Mono Nerd Font"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + [data-slot="anomaly"] { + display: none; + + @media (min-width: 768px) { + display: block; + } + } + } + [data-slot="anomaly-alt"] { + color: rgba(255, 255, 255, 0.39); + font-family: "JetBrains Mono Nerd Font"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + text-decoration: none; + margin-bottom: 24px; + + a { + color: rgba(255, 255, 255, 0.39); + font-family: "JetBrains Mono Nerd Font"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + text-decoration: none; + } + + @media (min-width: 768px) { + display: none; + } + } + } +} diff --git a/packages/console/app/src/routes/black/workspace.tsx b/packages/console/app/src/routes/black/workspace.tsx new file mode 100644 index 000000000..0eca8cd11 --- /dev/null +++ b/packages/console/app/src/routes/black/workspace.tsx @@ -0,0 +1,229 @@ +import { A, createAsync, useNavigate } from "@solidjs/router" +import "./workspace.css" +import { Title } from "@solidjs/meta" +import { github } from "~/lib/github" +import { createEffect, createMemo, For, onMount } from "solid-js" +import { config } from "~/config" +import { createList } from "solid-list" + +export default function BlackWorkspace() { + const navigate = useNavigate() + const githubData = createAsync(() => github()) + const starCount = createMemo(() => + githubData()?.stars + ? new Intl.NumberFormat("en-US", { + notation: "compact", + compactDisplay: "short", + }).format(githubData()!.stars!) + : config.github.starsFormatted.compact, + ) + + // TODO: Frank, replace with real workspaces + const workspaces = [ + { name: "Workspace 1", id: "wrk_123" }, + { name: "Workspace 2", id: "wrk_456" }, + { name: "Workspace 3", id: "wrk_789" }, + { name: "Workspace 4", id: "wrk_111" }, + { name: "Workspace 5", id: "wrk_222" }, + { name: "Workspace 6", id: "wrk_333" }, + { name: "Workspace 7", id: "wrk_444" }, + { name: "Workspace 8", id: "wrk_555" }, + ] + + let listRef: HTMLUListElement | undefined + + const { active, setActive, onKeyDown } = createList({ + items: () => workspaces.map((w) => w.id), + initialActive: workspaces[0]?.id ?? null, + handleTab: true, + }) + + onMount(() => { + listRef?.focus() + }) + + createEffect(() => { + const id = active() + if (!id || !listRef) return + const el = listRef.querySelector(`[data-id="${id}"]`) + el?.scrollIntoView({ block: "nearest" }) + }) + + return ( +
    + opencode +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + +
    +
    +

    Select a workspace for this plan

    +
      { + if (e.key === "Enter" && active()) { + navigate(`/black/workspace/${active()}`) + } else if (e.key === "Tab") { + e.preventDefault() + onKeyDown(e) + } else { + onKeyDown(e) + } + }} + > + + {(workspace) => ( +
    • setActive(workspace.id)} + onClick={() => navigate(`/black/workspace/${workspace.id}`)} + > + [*] + {workspace.name} +
    • + )} +
      +
    +
    +
    + +
    + ) +} From 7cba1ff7937130eb6087e7e766e8712f9c4f68c1 Mon Sep 17 00:00:00 2001 From: Github Action Date: Sat, 10 Jan 2026 03:10:45 +0000 Subject: [PATCH 038/470] Update Nix flake.lock and hashes --- flake.lock | 6 +++--- nix/hashes.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 644277dbb..b0f4d3f6b 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1767364772, - "narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=", + "lastModified": 1767966113, + "narHash": "sha256-mSTsvXa4WveSRJexsmCbm9dY17B1fKp7NLpJxllpQw4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa", + "rev": "5f02c91314c8ba4afe83b256b023756412218535", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index 6a5e8c26a..41583ec35 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-+QM5BDFxzrm1HY5ealjCm7jIO1t/rpW1q4GGLViPMmA=" + "nodeModules": "sha256-aIP+P0hSnQ50+lhnKvfRzQ8DlOhLjrrwarckxI3LBmE=" } From e92a2ec9dbadab56951192ccc3c20c55754f8100 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 10 Jan 2026 12:04:24 +0000 Subject: [PATCH 039/470] ignore: update download stats 2026-01-10 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index bed75d338..fad6b6452 100644 --- a/STATS.md +++ b/STATS.md @@ -196,3 +196,4 @@ | 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) | | 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) | | 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) | +| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) | From dfe3e7930404992165fca91908f55c26ee388598 Mon Sep 17 00:00:00 2001 From: Vlad Temian Date: Sat, 10 Jan 2026 15:44:21 +0200 Subject: [PATCH 040/470] docs(ecosystem): add micode and octto plugins (#7327) --- packages/web/src/content/docs/ecosystem.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx index c071152c9..44a73de69 100644 --- a/packages/web/src/content/docs/ecosystem.mdx +++ b/packages/web/src/content/docs/ecosystem.mdx @@ -40,6 +40,8 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw | [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interactive plan review with visual annotation and private/offline sharing | | [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Extend opencode /commands into a powerful orchestration system with granular flow control | | [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Schedule recurring jobs using launchd (Mac) or systemd (Linux) with cron syntax | +| [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → Implement workflow with session continuity | +| [octto](https://github.com/vtemian/octto) | Interactive browser UI for AI brainstorming with multi-question forms | --- From 1662e149b30d68afa366ed67c10afe78f748bfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20DE=20MATOS?= Date: Sat, 10 Jan 2026 14:50:24 +0100 Subject: [PATCH 041/470] fix: add ChatGPT-Account-Id header for organization subscriptions (#7603) --- packages/opencode/src/auth/index.ts | 1 + packages/opencode/src/plugin/codex.ts | 72 ++++++++++-- packages/opencode/test/plugin/codex.test.ts | 123 ++++++++++++++++++++ packages/plugin/src/index.ts | 2 + 4 files changed, 186 insertions(+), 12 deletions(-) create mode 100644 packages/opencode/test/plugin/codex.test.ts diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 6642a0742..3fd283053 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -12,6 +12,7 @@ export namespace Auth { refresh: z.string(), access: z.string(), expires: z.number(), + accountId: z.string().optional(), enterpriseUrl: z.string().optional(), }) .meta({ ref: "OAuth" }) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index d0f025b06..4e2b28379 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -42,6 +42,46 @@ function generateState(): string { return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer) } +export interface IdTokenClaims { + chatgpt_account_id?: string + organizations?: Array<{ id: string }> + email?: string + "https://api.openai.com/auth"?: { + chatgpt_account_id?: string + } +} + +export function parseJwtClaims(token: string): IdTokenClaims | undefined { + const parts = token.split(".") + if (parts.length !== 3) return undefined + try { + return JSON.parse(Buffer.from(parts[1], "base64url").toString()) + } catch { + return undefined + } +} + +export function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined { + return ( + claims.chatgpt_account_id || + claims["https://api.openai.com/auth"]?.chatgpt_account_id || + claims.organizations?.[0]?.id + ) +} + +export function extractAccountId(tokens: TokenResponse): string | undefined { + if (tokens.id_token) { + const claims = parseJwtClaims(tokens.id_token) + const accountId = claims && extractAccountIdFromClaims(claims) + if (accountId) return accountId + } + if (tokens.access_token) { + const claims = parseJwtClaims(tokens.access_token) + return claims ? extractAccountIdFromClaims(claims) : undefined + } + return undefined +} + function buildAuthorizeUrl(redirectUri: string, pkce: PkceCodes, state: string): string { const params = new URLSearchParams({ response_type: "code", @@ -380,10 +420,14 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { const currentAuth = await getAuth() if (currentAuth.type !== "oauth") return fetch(requestInput, init) + // Cast to include accountId field + const authWithAccount = currentAuth as typeof currentAuth & { accountId?: string } + // Check if token needs refresh if (!currentAuth.access || currentAuth.expires < Date.now()) { log.info("refreshing codex access token") const tokens = await refreshAccessToken(currentAuth.refresh) + const newAccountId = extractAccountId(tokens) || authWithAccount.accountId await input.client.auth.set({ path: { id: "codex" }, body: { @@ -391,9 +435,11 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { refresh: tokens.refresh_token, access: tokens.access_token, expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, + ...(newAccountId && { accountId: newAccountId }), }, }) currentAuth.access = tokens.access_token + authWithAccount.accountId = newAccountId } // Build headers @@ -415,20 +461,20 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { // Set authorization header with access token headers.set("authorization", `Bearer ${currentAuth.access}`) - // Rewrite URL to Codex endpoint - let url: URL - if (typeof requestInput === "string") { - url = new URL(requestInput) - } else if (requestInput instanceof URL) { - url = requestInput - } else { - url = new URL(requestInput.url) + // Set ChatGPT-Account-Id header for organization subscriptions + if (authWithAccount.accountId) { + headers.set("ChatGPT-Account-Id", authWithAccount.accountId) } - // If this is a messages/responses request, redirect to Codex endpoint - if (url.pathname.includes("/v1/responses") || url.pathname.includes("/chat/completions")) { - url = new URL(CODEX_API_ENDPOINT) - } + // Rewrite URL to Codex endpoint + const parsed = + requestInput instanceof URL + ? requestInput + : new URL(typeof requestInput === "string" ? requestInput : requestInput.url) + const url = + parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions") + ? new URL(CODEX_API_ENDPOINT) + : parsed return fetch(url, { ...init, @@ -456,11 +502,13 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { callback: async () => { const tokens = await callbackPromise stopOAuthServer() + const accountId = extractAccountId(tokens) return { type: "success" as const, refresh: tokens.refresh_token, access: tokens.access_token, expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, + accountId, } }, } diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts new file mode 100644 index 000000000..74d28ac9d --- /dev/null +++ b/packages/opencode/test/plugin/codex.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, test } from "bun:test" +import { + parseJwtClaims, + extractAccountIdFromClaims, + extractAccountId, + type IdTokenClaims, +} from "../../src/plugin/codex" + +function createTestJwt(payload: object): string { + const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url") + const body = Buffer.from(JSON.stringify(payload)).toString("base64url") + return `${header}.${body}.sig` +} + +describe("plugin.codex", () => { + describe("parseJwtClaims", () => { + test("parses valid JWT with claims", () => { + const payload = { email: "test@example.com", chatgpt_account_id: "acc-123" } + const jwt = createTestJwt(payload) + const claims = parseJwtClaims(jwt) + expect(claims).toEqual(payload) + }) + + test("returns undefined for JWT with less than 3 parts", () => { + expect(parseJwtClaims("invalid")).toBeUndefined() + expect(parseJwtClaims("only.two")).toBeUndefined() + }) + + test("returns undefined for invalid base64", () => { + expect(parseJwtClaims("a.!!!invalid!!!.b")).toBeUndefined() + }) + + test("returns undefined for invalid JSON payload", () => { + const header = Buffer.from("{}").toString("base64url") + const invalidJson = Buffer.from("not json").toString("base64url") + expect(parseJwtClaims(`${header}.${invalidJson}.sig`)).toBeUndefined() + }) + }) + + describe("extractAccountIdFromClaims", () => { + test("extracts chatgpt_account_id from root", () => { + const claims: IdTokenClaims = { chatgpt_account_id: "acc-root" } + expect(extractAccountIdFromClaims(claims)).toBe("acc-root") + }) + + test("extracts chatgpt_account_id from nested https://api.openai.com/auth", () => { + const claims: IdTokenClaims = { + "https://api.openai.com/auth": { chatgpt_account_id: "acc-nested" }, + } + expect(extractAccountIdFromClaims(claims)).toBe("acc-nested") + }) + + test("prefers root over nested", () => { + const claims: IdTokenClaims = { + chatgpt_account_id: "acc-root", + "https://api.openai.com/auth": { chatgpt_account_id: "acc-nested" }, + } + expect(extractAccountIdFromClaims(claims)).toBe("acc-root") + }) + + test("extracts from organizations array as fallback", () => { + const claims: IdTokenClaims = { + organizations: [{ id: "org-123" }, { id: "org-456" }], + } + expect(extractAccountIdFromClaims(claims)).toBe("org-123") + }) + + test("returns undefined when no accountId found", () => { + const claims: IdTokenClaims = { email: "test@example.com" } + expect(extractAccountIdFromClaims(claims)).toBeUndefined() + }) + }) + + describe("extractAccountId", () => { + test("extracts from id_token first", () => { + const idToken = createTestJwt({ chatgpt_account_id: "from-id-token" }) + const accessToken = createTestJwt({ chatgpt_account_id: "from-access-token" }) + expect( + extractAccountId({ + id_token: idToken, + access_token: accessToken, + refresh_token: "rt", + }), + ).toBe("from-id-token") + }) + + test("falls back to access_token when id_token has no accountId", () => { + const idToken = createTestJwt({ email: "test@example.com" }) + const accessToken = createTestJwt({ + "https://api.openai.com/auth": { chatgpt_account_id: "from-access" }, + }) + expect( + extractAccountId({ + id_token: idToken, + access_token: accessToken, + refresh_token: "rt", + }), + ).toBe("from-access") + }) + + test("returns undefined when no tokens have accountId", () => { + const token = createTestJwt({ email: "test@example.com" }) + expect( + extractAccountId({ + id_token: token, + access_token: token, + refresh_token: "rt", + }), + ).toBeUndefined() + }) + + test("handles missing id_token", () => { + const accessToken = createTestJwt({ chatgpt_account_id: "acc-123" }) + expect( + extractAccountId({ + id_token: "", + access_token: accessToken, + refresh_token: "rt", + }), + ).toBe("acc-123") + }) + }) +}) diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index bf9b6e8c2..46ad8512c 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -114,6 +114,7 @@ export type AuthOuathResult = { url: string; instructions: string } & ( refresh: string access: string expires: number + accountId?: string } | { key: string } )) @@ -133,6 +134,7 @@ export type AuthOuathResult = { url: string; instructions: string } & ( refresh: string access: string expires: number + accountId?: string } | { key: string } )) From e20535655b94bc95241e286758c97d744f6efd97 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 10 Jan 2026 13:50:58 +0000 Subject: [PATCH 042/470] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 1 + packages/sdk/openapi.json | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9cb7222aa..e423fecea 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2062,6 +2062,7 @@ export type OAuth = { refresh: string access: string expires: number + accountId?: string enterpriseUrl?: string } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 1bc7bd5c2..367985e5d 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -10463,6 +10463,9 @@ "expires": { "type": "number" }, + "accountId": { + "type": "string" + }, "enterpriseUrl": { "type": "string" } From 449270aacc54f43c0cdb3a88f3aaf13f41b50232 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Jan 2026 13:56:12 +0000 Subject: [PATCH 043/470] release: v1.1.12 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 9aba2f10f..8e8dab33e 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -99,7 +99,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -126,7 +126,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -150,7 +150,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -174,7 +174,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -203,7 +203,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -232,7 +232,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -248,7 +248,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.11", + "version": "1.1.12", "bin": { "opencode": "./bin/opencode", }, @@ -351,7 +351,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -371,7 +371,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.11", + "version": "1.1.12", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -382,7 +382,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -395,7 +395,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -434,7 +434,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "zod": "catalog:", }, @@ -445,7 +445,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index d3bf8e2ea..09083bdac 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.11", + "version": "1.1.12", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 0af730e9f..05fb0c8bf 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.11", + "version": "1.1.12", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index e55ea92f4..074932a91 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.1.11", + "version": "1.1.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 919f57a92..33d1f5b3a 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.11", + "version": "1.1.12", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index f858b1f90..ea7ac4f56 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.11", + "version": "1.1.12", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 945ef8acb..66dcab94e 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.11", + "version": "1.1.12", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 3517e9fb7..368243dd9 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.11", + "version": "1.1.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index d77d0ba06..ed340fc36 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.11" +version = "1.1.12" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.11/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index fc1c51ecf..8597f2c80 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.11", + "version": "1.1.12", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 7d2c5f04b..ba0722045 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.11", + "version": "1.1.12", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index ffd083cbe..c62b65606 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.1.11", + "version": "1.1.12", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 03a0baa0a..8585cbf1e 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.1.11", + "version": "1.1.12", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 6d4afbb61..23d329166 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.11", + "version": "1.1.12", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index f563ef57d..df45e4ba8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.11", + "version": "1.1.12", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 9e38897fb..c5ef19a61 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.11", + "version": "1.1.12", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 03541fb47..8f06a6cee 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.1.11", + "version": "1.1.12", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index dd51e0ef1..7ec45c3c8 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.1.11", + "version": "1.1.12", "publisher": "sst-dev", "repository": { "type": "git", From a593ed4c9b6c66e2d0c12518de7699f3458d8cf5 Mon Sep 17 00:00:00 2001 From: madflow Date: Sat, 10 Jan 2026 15:44:12 +0100 Subject: [PATCH 044/470] fix: disable telemetry in the php lsp server (#7649) --- packages/opencode/src/lsp/server.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index c818e2b3e..24da77edc 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1528,7 +1528,11 @@ export namespace LSPServer { }) return { process: proc, - initialization: {}, + initialization: { + telemetry: { + enabled: false, + }, + }, } }, } From 58186004dbf221df4c4d155c83493d2bb0679246 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 10 Jan 2026 14:44:47 +0000 Subject: [PATCH 045/470] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index c62b65606..489a582a4 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 8585cbf1e..b9b150ccb 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From afb1cad26da1490d534e2c641b6bd69d9d9acd7d Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Sat, 10 Jan 2026 11:52:11 -0300 Subject: [PATCH 046/470] fix(desktop): Allow Selecting Card Errors (#7506) --- packages/ui/src/components/message-part.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 9628fec80..fba36dc27 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -387,7 +387,8 @@ [data-component="edit-content"], [data-component="write-content"], [data-component="todos"], -[data-component="diagnostics"] { +[data-component="diagnostics"], +.error-card { -webkit-user-select: text; user-select: text; } From b6b009775582d2cebe475b82fe0130f140f6572a Mon Sep 17 00:00:00 2001 From: zerone0x Date: Sat, 10 Jan 2026 22:53:52 +0800 Subject: [PATCH 047/470] fix(ui): allow text editing shortcuts in search dialogs on macOS (#7419) --- packages/ui/src/hooks/use-filtered-list.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx index 242d16a25..1b3be4b4c 100644 --- a/packages/ui/src/hooks/use-filtered-list.tsx +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -82,6 +82,8 @@ export function useFilteredList(props: FilteredListProps) { const selected = flat()[selectedIndex] if (selected) props.onSelect?.(selected, selectedIndex) } else { + // Skip list navigation for text editing shortcuts (e.g., Option+Arrow, Option+Backspace on macOS) + if (event.altKey || event.metaKey) return list.onKeyDown(event) } } From 1a642a79a661b54f3cf1996e39f69fcb04c2a748 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:55:20 +0100 Subject: [PATCH 048/470] fix(desktop): remove split operation in serverDisplayName function (#7397) --- packages/app/src/context/server.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 06c37b592..4cfe9cc27 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -19,7 +19,6 @@ export function serverDisplayName(url: string) { return url .replace(/^https?:\/\//, "") .replace(/\/+$/, "") - .split("/")[0] } function projectsKey(url: string) { From a8f23fb54812c4cbd60a8b1057d234200a7f6e02 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 10 Jan 2026 14:55:55 +0000 Subject: [PATCH 049/470] chore: generate --- packages/app/src/context/server.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 4cfe9cc27..8945cd37e 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -16,9 +16,7 @@ export function normalizeServerUrl(input: string) { export function serverDisplayName(url: string) { if (!url) return "" - return url - .replace(/^https?:\/\//, "") - .replace(/\/+$/, "") + return url.replace(/^https?:\/\//, "").replace(/\/+$/, "") } function projectsKey(url: string) { From 02b7eb59f8ce9d80a9a80210c0a8ca9de716962e Mon Sep 17 00:00:00 2001 From: Andrew Thal <467872+athal7@users.noreply.github.com> Date: Sat, 10 Jan 2026 08:56:48 -0600 Subject: [PATCH 050/470] feat: support configuring default server URL for desktop (#7363) --- .gitignore | 1 + packages/app/src/app.tsx | 5 +- .../src/components/dialog-select-server.tsx | 51 ++++- packages/app/src/context/platform.tsx | 6 + packages/desktop/src-tauri/Cargo.lock | 1 + packages/desktop/src-tauri/Cargo.toml | 1 + packages/desktop/src-tauri/src/lib.rs | 180 ++++++++++++++++-- packages/desktop/src/index.tsx | 9 + 8 files changed, 230 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index fc175568d..75fa054a5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ target # Local dev files opencode-dev logs/ +*.bun-build diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index dc7b976fc..bf6da3b4d 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -33,13 +33,14 @@ const Loading = () =>
    , }) + const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.()) + const isDesktop = platform.platform === "desktop" const items = createMemo(() => { const current = server.url @@ -173,6 +175,53 @@ export function DialogSelectServer() {
    + + +
    +
    +

    Default server

    +

    + Connect to this server on app launch instead of starting a local server. Requires restart. +

    +
    +
    + No server selected} + > + + + } + > +
    + {serverDisplayName(defaultUrl()!)} +
    + + +
    +
    +
    ) diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 7fcbb620a..b0822e707 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -37,6 +37,12 @@ export type Platform = { /** Fetch override */ fetch?: typeof fetch + + /** Get the configured default server URL (desktop only) */ + getDefaultServerUrl?(): Promise + + /** Set the default server URL to use on app startup (desktop only) */ + setDefaultServerUrl?(url: string | null): Promise } export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index c533bf9e9..92953ea19 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2795,6 +2795,7 @@ dependencies = [ "futures", "gtk", "listeners", + "reqwest", "semver", "serde", "serde_json", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index af8adc119..c8eb0846c 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -38,6 +38,7 @@ listeners = "0.3" tauri-plugin-os = "2" futures = "0.3.31" semver = "1.0.27" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index bdff73820..3f0131834 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -13,12 +13,16 @@ use tauri::{ path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, WebviewWindow, }; +use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; use tokio::net::TcpSocket; use crate::window_customizer::PinchZoomDisablePlugin; +const SETTINGS_STORE: &str = "opencode.settings.dat"; +const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; + #[derive(Clone)] struct ServerState { child: Arc>>, @@ -88,6 +92,41 @@ async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), Stri .map_err(|_| "Failed to get server status".to_string())? } +#[tauri::command] +async fn get_default_server_url(app: AppHandle) -> Result, String> { + let store = app + .store(SETTINGS_STORE) + .map_err(|e| format!("Failed to open settings store: {}", e))?; + + let value = store.get(DEFAULT_SERVER_URL_KEY); + match value { + Some(v) => Ok(v.as_str().map(String::from)), + None => Ok(None), + } +} + +#[tauri::command] +async fn set_default_server_url(app: AppHandle, url: Option) -> Result<(), String> { + let store = app + .store(SETTINGS_STORE) + .map_err(|e| format!("Failed to open settings store: {}", e))?; + + match url { + Some(u) => { + store.set(DEFAULT_SERVER_URL_KEY, serde_json::Value::String(u)); + } + None => { + store.delete(DEFAULT_SERVER_URL_KEY); + } + } + + store + .save() + .map_err(|e| format!("Failed to save settings: {}", e))?; + + Ok(()) +} + fn get_sidecar_port() -> u32 { option_env!("OPENCODE_PORT") .map(|s| s.to_string()) @@ -193,6 +232,30 @@ async fn is_server_running(port: u32) -> bool { .is_ok() } +async fn check_server_health(url: &str) -> bool { + let health_url = format!("{}/health", url.trim_end_matches('/')); + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(3)) + .build(); + + let Ok(client) = client else { + return false; + }; + + client + .get(&health_url) + .send() + .await + .map(|r| r.status().is_success()) + .unwrap_or(false) +} + +fn get_configured_server_url(app: &AppHandle) -> Option { + let store = app.store(SETTINGS_STORE).ok()?; + let value = store.get(DEFAULT_SERVER_URL_KEY)?; + value.as_str().map(String::from) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); @@ -219,7 +282,9 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ kill_sidecar, install_cli, - ensure_server_started + ensure_server_started, + get_default_server_url, + set_default_server_url ]) .setup(move |app| { let app = app.handle().clone(); @@ -266,41 +331,114 @@ pub fn run() { { let app = app.clone(); tauri::async_runtime::spawn(async move { - let should_spawn_sidecar = !is_server_running(port).await; + // Check for configured default server URL + let configured_url = get_configured_server_url(&app); - let (child, res) = if should_spawn_sidecar { - let child = spawn_sidecar(&app, port); + let (child, res, server_url) = if let Some(ref url) = configured_url { + println!("Configured default server URL: {}", url); - let timestamp = Instant::now(); - let res = loop { - if timestamp.elapsed() > Duration::from_secs(7) { - break Err(format!( - "Failed to spawn OpenCode Server. Logs:\n{}", - get_logs(app.clone()).await.unwrap() - )); + // Try to connect to the configured server + let mut healthy = false; + let mut should_fallback = false; + + loop { + if check_server_health(url).await { + healthy = true; + println!("Connected to configured server: {}", url); + break; } - tokio::time::sleep(Duration::from_millis(10)).await; + let res = app.dialog() + .message(format!("Could not connect to configured server:\n{}\n\nWould you like to retry or start a local server instead?", url)) + .title("Connection Failed") + .buttons(MessageDialogButtons::OkCancelCustom("Retry".to_string(), "Start Local".to_string())) + .blocking_show_with_result(); + + match res { + MessageDialogResult::Custom(name) if name == "Retry" => { + continue; + }, + _ => { + should_fallback = true; + break; + } + } + } + + if healthy { + (None, Ok(()), Some(url.clone())) + } else if should_fallback { + // Fall back to spawning local sidecar + let child = spawn_sidecar(&app, port); + + let timestamp = Instant::now(); + let res = loop { + if timestamp.elapsed() > Duration::from_secs(7) { + break Err(format!( + "Failed to spawn OpenCode Server. Logs:\n{}", + get_logs(app.clone()).await.unwrap() + )); + } - if is_server_running(port).await { - // give the server a little bit more time to warm up tokio::time::sleep(Duration::from_millis(10)).await; - break Ok(()); - } + if is_server_running(port).await { + tokio::time::sleep(Duration::from_millis(10)).await; + break Ok(()); + } + }; + + println!("Server ready after {:?}", timestamp.elapsed()); + (Some(child), res, None) + } else { + (None, Err("User cancelled".to_string()), None) + } + } else { + // No configured URL, spawn local sidecar as before + let should_spawn_sidecar = !is_server_running(port).await; + + let (child, res) = if should_spawn_sidecar { + let child = spawn_sidecar(&app, port); + + let timestamp = Instant::now(); + let res = loop { + if timestamp.elapsed() > Duration::from_secs(7) { + break Err(format!( + "Failed to spawn OpenCode Server. Logs:\n{}", + get_logs(app.clone()).await.unwrap() + )); + } + + tokio::time::sleep(Duration::from_millis(10)).await; + + if is_server_running(port).await { + tokio::time::sleep(Duration::from_millis(10)).await; + break Ok(()); + } + }; + + println!("Server ready after {:?}", timestamp.elapsed()); + + (Some(child), res) + } else { + (None, Ok(())) }; - println!("Server ready after {:?}", timestamp.elapsed()); - - (Some(child), res) - } else { - (None, Ok(())) + (child, res, None) }; app.state::().set_child(child); if res.is_ok() { let _ = window.eval("window.__OPENCODE__.serverReady = true;"); + + // If using a configured server URL, inject it + if let Some(url) = server_url { + let escaped_url = url.replace('\\', "\\\\").replace('"', "\\\""); + let _ = window.eval(format!( + "window.__OPENCODE__.serverUrl = \"{escaped_url}\";", + )); + } } let _ = tx.send(res); diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 941ea8df7..ffb178672 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -257,6 +257,15 @@ const platform: Platform = { // @ts-expect-error fetch: tauriFetch, + + getDefaultServerUrl: async () => { + const result = await invoke("get_default_server_url").catch(() => null) + return result + }, + + setDefaultServerUrl: async (url: string | null) => { + await invoke("set_default_server_url", { url }) + }, } createMenu() From d5738f542c222bb5832a21f54e8348db88a3f7ba Mon Sep 17 00:00:00 2001 From: Kenny Date: Sat, 10 Jan 2026 11:57:18 -0500 Subject: [PATCH 051/470] fix(grep): follow symlinks by default in ripgrep searches (#7501) --- packages/opencode/src/file/ripgrep.ts | 9 ++++++++- packages/opencode/src/tool/grep.ts | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 841f5f305..834cbee1e 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -367,8 +367,15 @@ export namespace Ripgrep { return lines.join("\n") } - export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) { + export async function search(input: { + cwd: string + pattern: string + glob?: string[] + limit?: number + follow?: boolean + }) { const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"] + if (input.follow !== false) args.push("--follow") if (input.glob) { for (const g of input.glob) { diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 4cbc5347f..b7cba1674 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -33,7 +33,7 @@ export const GrepTool = Tool.define("grep", { const searchPath = params.path || Instance.directory const rgPath = await Ripgrep.filepath() - const args = ["-nH", "--field-match-separator=|", "--regexp", params.pattern] + const args = ["-nH", "--hidden", "--follow", "--field-match-separator=|", "--regexp", params.pattern] if (params.include) { args.push("--glob", params.include) } From 794c5981a5ef898a07dc7ef420face09f0446a85 Mon Sep 17 00:00:00 2001 From: Alejandro Chinchilla <80235041+Alejandro-CSt@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:22:44 -0600 Subject: [PATCH 052/470] fix: exclude 'none' variant for gpt-5.2-codex (#7682) --- packages/opencode/src/provider/transform.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 08f329ec1..38b2c9aa1 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -318,7 +318,10 @@ export namespace ProviderTransform { // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai if (id === "gpt-5-pro") return {} const openaiEfforts = iife(() => { - if (id.includes("codex") && !id.includes("5.2")) return WIDELY_SUPPORTED_EFFORTS + if (id.includes("codex")) { + if (id.includes("5.2")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"] + return WIDELY_SUPPORTED_EFFORTS + } const arr = [...WIDELY_SUPPORTED_EFFORTS] if (id.includes("gpt-5-") || id === "gpt-5") { arr.unshift("minimal") From b41fbda68fe8abfd42be93cf63c78251c677d43c Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 10 Jan 2026 13:57:47 -0500 Subject: [PATCH 053/470] wip: black --- packages/console/core/script/lookup-user.ts | 8 +- packages/console/core/script/remove-black.ts | 78 ++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 packages/console/core/script/remove-black.ts diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts index 15cbe80f8..b06fb5654 100644 --- a/packages/console/core/script/lookup-user.ts +++ b/packages/console/core/script/lookup-user.ts @@ -41,8 +41,8 @@ if (identifier.startsWith("wrk_")) { subscribed: SubscriptionTable.timeCreated, }) .from(UserTable) - .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID)) - .innerJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id)) + .rightJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID)) + .leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id)) .where(eq(UserTable.accountID, accountID)) .then((rows) => rows.map((row) => ({ @@ -113,6 +113,8 @@ async function printWorkspace(workspaceID: string) { .select({ balance: BillingTable.balance, customerID: BillingTable.customerID, + subscriptionID: BillingTable.subscriptionID, + subscriptionCouponID: BillingTable.subscriptionCouponID, }) .from(BillingTable) .where(eq(BillingTable.workspaceID, workspace.id)) @@ -149,6 +151,7 @@ async function printWorkspace(workspaceID: string) { ), ) + /* await printTable("Usage", (tx) => tx .select({ @@ -174,6 +177,7 @@ async function printWorkspace(workspaceID: string) { })), ), ) + */ } function formatMicroCents(value: number | null | undefined) { diff --git a/packages/console/core/script/remove-black.ts b/packages/console/core/script/remove-black.ts new file mode 100644 index 000000000..0803c8f83 --- /dev/null +++ b/packages/console/core/script/remove-black.ts @@ -0,0 +1,78 @@ +import { Billing } from "../src/billing.js" +import { and, Database, eq } from "../src/drizzle/index.js" +import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js" + +const workspaceID = process.argv[2] + +if (!workspaceID) { + console.error("Usage: bun remove-black.ts ") + process.exit(1) +} + +console.log(`Removing subscription from workspace ${workspaceID}`) + +// Look up the workspace billing +const billing = await Database.use((tx) => + tx + .select({ + customerID: BillingTable.customerID, + subscriptionID: BillingTable.subscriptionID, + }) + .from(BillingTable) + .where(eq(BillingTable.workspaceID, workspaceID)) + .then((rows) => rows[0]), +) + +if (!billing) { + console.error(`Error: No billing record found for workspace ${workspaceID}`) + process.exit(1) +} + +if (!billing.subscriptionID) { + console.error(`Error: Workspace ${workspaceID} does not have a subscription`) + process.exit(1) +} + +console.log(` Customer ID: ${billing.customerID}`) +console.log(` Subscription ID: ${billing.subscriptionID}`) + +// Clear workspaceID from Stripe customer metadata +if (billing.customerID) { + //await Billing.stripe().customers.update(billing.customerID, { + // metadata: { + // workspaceID: "", + // }, + //}) + //console.log(`Cleared workspaceID from Stripe customer metadata`) +} + +await Database.transaction(async (tx) => { + // Clear subscription-related fields from billing table + await tx + .update(BillingTable) + .set({ + // customerID: null, + subscriptionID: null, + subscriptionCouponID: null, + // paymentMethodID: null, + // paymentMethodLast4: null, + // paymentMethodType: null, + }) + .where(eq(BillingTable.workspaceID, workspaceID)) + + // Delete from subscription table + await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID)) + + // Delete from payments table + await tx + .delete(PaymentTable) + .where( + and( + eq(PaymentTable.workspaceID, workspaceID), + eq(PaymentTable.enrichment, { type: "subscription" }), + eq(PaymentTable.amount, 20000000000), + ), + ) +}) + +console.log(`Successfully removed subscription from workspace ${workspaceID}`) From 2d2a0449619e19f83407a846f110c8d6d70d4f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Dobruna?= Date: Sat, 10 Jan 2026 22:24:15 +0100 Subject: [PATCH 054/470] fix(desktop): build failing - missing import (#7697) --- packages/desktop/src-tauri/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 3f0131834..8405d911c 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -16,6 +16,7 @@ use tauri::{ use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; +use tauri_plugin_store::StoreExt; use tokio::net::TcpSocket; use crate::window_customizer::PinchZoomDisablePlugin; From f882cca98a2b245b695880499dfc4101d4ed924c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 10 Jan 2026 22:26:24 +0100 Subject: [PATCH 055/470] fix(desktop): add missing StoreExt import and fix unused mut warning (#7707) --- packages/desktop/src-tauri/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 8405d911c..5ed03fc66 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -302,6 +302,7 @@ pub fn run() { .unwrap_or(LogicalSize::new(1920, 1080)); // Create window immediately with serverReady = false + #[allow(unused_mut)] let mut window_builder = WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into())) .title("OpenCode") From 50ed4c6b5de8aa0e5d0739f933f7afd2ecaa95f4 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Sat, 10 Jan 2026 19:01:23 -0300 Subject: [PATCH 056/470] feat(deskop): Add Copy to Messages (#7658) --- packages/ui/src/components/message-part.css | 13 ++++++++++++ packages/ui/src/components/message-part.tsx | 16 +++++++++++++++ packages/ui/src/components/session-turn.css | 16 +++++++++++++++ packages/ui/src/components/session-turn.tsx | 22 ++++++++++++++++++++- 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index fba36dc27..3cdd93cb9 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -76,12 +76,25 @@ } [data-slot="user-message-text"] { + position: relative; white-space: pre-wrap; word-break: break-word; overflow: hidden; background: var(--surface-base); padding: 8px 12px; border-radius: 4px; + + [data-slot="user-message-copy-wrapper"] { + position: absolute; + top: 7px; + right: 7px; + opacity: 0; + transition: opacity 0.15s ease; + } + + &:hover [data-slot="user-message-copy-wrapper"] { + opacity: 1; + } } .text-text-strong { diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index d59f5cfa3..644690ed2 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -38,6 +38,8 @@ import { Markdown } from "./markdown" import { ImagePreview } from "./image-preview" import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path" import { checksum } from "@opencode-ai/util/encode" +import { Tooltip } from "./tooltip" +import { IconButton } from "./icon-button" import { createAutoScroll } from "../hooks" interface Diagnostic { @@ -278,6 +280,7 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) { const dialog = useDialog() + const [copied, setCopied] = createSignal(false) const textPart = createMemo( () => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined, @@ -307,6 +310,14 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp dialog.show(() => ) } + const handleCopy = async () => { + const content = text() + if (!content) return + await navigator.clipboard.writeText(content) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + return (
    0}> @@ -341,6 +352,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
    +
    + + + +
    diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 9b7aa7364..581935b3e 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -225,6 +225,22 @@ } } + [data-slot="session-turn-summary-section"] { + position: relative; + + [data-slot="session-turn-summary-copy"] { + position: absolute; + top: 0; + right: 0; + opacity: 0; + transition: opacity 0.15s ease; + } + + &:hover [data-slot="session-turn-summary-copy"] { + opacity: 1; + } + } + [data-slot="session-turn-accordion"] { width: 100%; } diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index f69d414be..075da218b 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -11,7 +11,7 @@ import { useDiffComponent } from "../context/diff" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { Binary } from "@opencode-ai/util/binary" -import { createEffect, createMemo, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js" +import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js" import { createResizeObserver } from "@solid-primitives/resize-observer" import { DiffChanges } from "./diff-changes" import { Typewriter } from "./typewriter" @@ -21,6 +21,8 @@ import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" import { FileIcon } from "./file-icon" import { Icon } from "./icon" +import { IconButton } from "./icon-button" +import { Tooltip } from "./tooltip" import { Card } from "./card" import { Dynamic } from "solid-js/web" import { Button } from "./button" @@ -328,6 +330,15 @@ export function SessionTurn( const hasDiffs = createMemo(() => message()?.summary?.diffs?.length) const hideResponsePart = createMemo(() => !working() && !!responsePartId()) + const [responseCopied, setResponseCopied] = createSignal(false) + const handleCopyResponse = async () => { + const content = response() + if (!content) return + await navigator.clipboard.writeText(content) + setResponseCopied(true) + setTimeout(() => setResponseCopied(false), 2000) + } + function duration() { const msg = message() if (!msg) return "" @@ -556,6 +567,15 @@ export function SessionTurn( {/* Response */}
    +
    + + + +

    Response

    Date: Sat, 10 Jan 2026 19:02:47 -0300 Subject: [PATCH 057/470] feat(desktop): Make mouse hover / click work on prompt input autocomplete (#7661) --- packages/app/src/components/prompt-input.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index a5655902a..97f54da5e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -386,6 +386,7 @@ export const PromptInput: Component = (props) => { const { flat: atFlat, active: atActive, + setActive: setAtActive, onInput: atOnInput, onKeyDown: atOnKeyDown, } = useFilteredList({ @@ -452,6 +453,7 @@ export const PromptInput: Component = (props) => { const { flat: slashFlat, active: slashActive, + setActive: setSlashActive, onInput: slashOnInput, onKeyDown: slashOnKeyDown, refetch: slashRefetch, @@ -1299,6 +1301,7 @@ export const PromptInput: Component = (props) => { class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10 overflow-auto no-scrollbar flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md" + onMouseDown={(e) => e.preventDefault()} > @@ -1314,6 +1317,7 @@ export const PromptInput: Component = (props) => { "bg-surface-raised-base-hover": atActive() === atKey(item), }} onClick={() => handleAtSelect(item)} + onMouseEnter={() => setAtActive(atKey(item))} > = (props) => { "bg-surface-raised-base-hover": slashActive() === cmd.id, }} onClick={() => handleSlashSelect(cmd)} + onMouseEnter={() => setSlashActive(cmd.id)} >
    /{cmd.trigger} From 76386f5cfc310dd3abea76222ba80450dc5c6253 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Sat, 10 Jan 2026 19:04:01 -0300 Subject: [PATCH 058/470] feat(desktop): Fork Session (#7673) --- packages/app/src/components/dialog-fork.tsx | 99 +++++++++++++++++++++ packages/app/src/pages/session.tsx | 10 +++ 2 files changed, 109 insertions(+) create mode 100644 packages/app/src/components/dialog-fork.tsx diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx new file mode 100644 index 000000000..472a1994f --- /dev/null +++ b/packages/app/src/components/dialog-fork.tsx @@ -0,0 +1,99 @@ +import { Component, createMemo } from "solid-js" +import { useNavigate, useParams } from "@solidjs/router" +import { useSync } from "@/context/sync" +import { useSDK } from "@/context/sdk" +import { usePrompt } from "@/context/prompt" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import { List } from "@opencode-ai/ui/list" +import { extractPromptFromParts } from "@/utils/prompt" +import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client" +import { base64Encode } from "@opencode-ai/util/encode" + +interface ForkableMessage { + id: string + text: string + time: string +} + +function formatTime(date: Date): string { + return date.toLocaleTimeString(undefined, { timeStyle: "short" }) +} + +export const DialogFork: Component = () => { + const params = useParams() + const navigate = useNavigate() + const sync = useSync() + const sdk = useSDK() + const prompt = usePrompt() + const dialog = useDialog() + + const messages = createMemo((): ForkableMessage[] => { + const sessionID = params.id + if (!sessionID) return [] + + const msgs = sync.data.message[sessionID] ?? [] + const result: ForkableMessage[] = [] + + for (const message of msgs) { + if (message.role !== "user") continue + + const parts = sync.data.part[message.id] ?? [] + const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored) + if (!textPart) continue + + result.push({ + id: message.id, + text: textPart.text.replace(/\n/g, " ").slice(0, 200), + time: formatTime(new Date(message.time.created)), + }) + } + + return result.reverse() + }) + + const handleSelect = (item: ForkableMessage | undefined) => { + if (!item) return + + const sessionID = params.id + if (!sessionID) return + + const parts = sync.data.part[item.id] ?? [] + const restored = extractPromptFromParts(parts, { directory: sdk.directory }) + + dialog.close() + + sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => { + if (!forked.data) return + navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`) + requestAnimationFrame(() => { + prompt.set(restored) + }) + }) + } + + return ( + + x.id} + items={messages} + filterKeys={["text"]} + onSelect={handleSelect} + > + {(item) => ( +
    + + {item.text} + + + {item.time} + +
    + )} +
    +
    + ) +} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index ab6995d92..69065a8fa 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -31,6 +31,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectFile } from "@/components/dialog-select-file" import { DialogSelectModel } from "@/components/dialog-select-model" import { DialogSelectMcp } from "@/components/dialog-select-mcp" +import { DialogFork } from "@/components/dialog-fork" import { useCommand } from "@/context/command" import { useNavigate, useParams } from "@solidjs/router" import { UserMessage } from "@opencode-ai/sdk/v2" @@ -645,6 +646,15 @@ export default function Page() { }) }, }, + { + id: "session.fork", + title: "Fork from message", + description: "Create a new session from a previous message", + category: "Session", + slash: "fork", + disabled: !params.id || visibleUserMessages().length === 0, + onSelect: () => dialog.show(() => ), + }, ]) const handleKeyDown = (event: KeyboardEvent) => { From f94ee5ce9030f0c3a883a1a6c5f81f4e984ef3f3 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 10 Jan 2026 18:49:36 -0500 Subject: [PATCH 059/470] core: extract external directory validation to shared utility to reduce code duplication across tools --- packages/opencode/src/tool/edit.ts | 14 +- .../opencode/src/tool/external-directory.ts | 33 +++++ packages/opencode/src/tool/glob.ts | 2 + packages/opencode/src/tool/grep.ts | 6 +- packages/opencode/src/tool/ls.ts | 2 + packages/opencode/src/tool/lsp.ts | 6 +- packages/opencode/src/tool/patch.ts | 21 +-- packages/opencode/src/tool/read.ts | 17 +-- packages/opencode/src/tool/write.ts | 8 +- .../test/tool/external-directory.test.ts | 126 ++++++++++++++++++ 10 files changed, 186 insertions(+), 49 deletions(-) create mode 100644 packages/opencode/src/tool/external-directory.ts create mode 100644 packages/opencode/test/tool/external-directory.test.ts diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 787282ecd..b68078f14 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -15,6 +15,7 @@ import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Snapshot } from "@/snapshot" +import { assertExternalDirectory } from "./external-directory" const MAX_DIAGNOSTICS_PER_FILE = 20 @@ -40,18 +41,7 @@ export const EditTool = Tool.define("edit", { } const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) - if (!Filesystem.contains(Instance.directory, filePath)) { - const parentDir = path.dirname(filePath) - await ctx.ask({ - permission: "external_directory", - patterns: [parentDir, path.join(parentDir, "*")], - always: [parentDir + "/*"], - metadata: { - filepath: filePath, - parentDir, - }, - }) - } + await assertExternalDirectory(ctx, filePath) let diff = "" let contentOld = "" diff --git a/packages/opencode/src/tool/external-directory.ts b/packages/opencode/src/tool/external-directory.ts new file mode 100644 index 000000000..05f83c00d --- /dev/null +++ b/packages/opencode/src/tool/external-directory.ts @@ -0,0 +1,33 @@ +import path from "path" +import type { Tool } from "./tool" +import { Filesystem } from "../util/filesystem" +import { Instance } from "../project/instance" + +type Kind = "file" | "directory" + +type Options = { + bypass?: boolean + kind?: Kind +} + +export async function assertExternalDirectory(ctx: Tool.Context, target?: string, options?: Options) { + if (!target) return + + if (options?.bypass) return + + if (Filesystem.contains(Instance.directory, target)) return + + const kind = options?.kind ?? "file" + const parentDir = kind === "directory" ? target : path.dirname(target) + const glob = path.join(parentDir, "*") + + await ctx.ask({ + permission: "external_directory", + patterns: [glob], + always: [glob], + metadata: { + filepath: target, + parentDir, + }, + }) +} diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index 0c643796d..dda57f6ee 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -4,6 +4,7 @@ import { Tool } from "./tool" import DESCRIPTION from "./glob.txt" import { Ripgrep } from "../file/ripgrep" import { Instance } from "../project/instance" +import { assertExternalDirectory } from "./external-directory" export const GlobTool = Tool.define("glob", { description: DESCRIPTION, @@ -29,6 +30,7 @@ export const GlobTool = Tool.define("glob", { let search = params.path ?? Instance.directory search = path.isAbsolute(search) ? search : path.resolve(Instance.directory, search) + await assertExternalDirectory(ctx, search, { kind: "directory" }) const limit = 100 const files = [] diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index b7cba1674..ad62621e0 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -4,6 +4,8 @@ import { Ripgrep } from "../file/ripgrep" import DESCRIPTION from "./grep.txt" import { Instance } from "../project/instance" +import path from "path" +import { assertExternalDirectory } from "./external-directory" const MAX_LINE_LENGTH = 2000 @@ -30,7 +32,9 @@ export const GrepTool = Tool.define("grep", { }, }) - const searchPath = params.path || Instance.directory + let searchPath = params.path ?? Instance.directory + searchPath = path.isAbsolute(searchPath) ? searchPath : path.resolve(Instance.directory, searchPath) + await assertExternalDirectory(ctx, searchPath, { kind: "directory" }) const rgPath = await Ripgrep.filepath() const args = ["-nH", "--hidden", "--follow", "--field-match-separator=|", "--regexp", params.pattern] diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts index b8638b3e9..cc3d75007 100644 --- a/packages/opencode/src/tool/ls.ts +++ b/packages/opencode/src/tool/ls.ts @@ -4,6 +4,7 @@ import * as path from "path" import DESCRIPTION from "./ls.txt" import { Instance } from "../project/instance" import { Ripgrep } from "../file/ripgrep" +import { assertExternalDirectory } from "./external-directory" export const IGNORE_PATTERNS = [ "node_modules/", @@ -42,6 +43,7 @@ export const ListTool = Tool.define("list", { }), async execute(params, ctx) { const searchPath = path.resolve(Instance.directory, params.path || ".") + await assertExternalDirectory(ctx, searchPath, { kind: "directory" }) await ctx.ask({ permission: "list", diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index df4692bf6..ca352280b 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -5,6 +5,7 @@ import { LSP } from "../lsp" import DESCRIPTION from "./lsp.txt" import { Instance } from "../project/instance" import { pathToFileURL } from "url" +import { assertExternalDirectory } from "./external-directory" const operations = [ "goToDefinition", @@ -27,14 +28,15 @@ export const LspTool = Tool.define("lsp", { character: z.number().int().min(1).describe("The character offset (1-based, as shown in editors)"), }), execute: async (args, ctx) => { + const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath) + await assertExternalDirectory(ctx, file) + await ctx.ask({ permission: "lsp", patterns: ["*"], always: ["*"], metadata: {}, }) - - const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath) const uri = pathToFileURL(file).href const position = { file, diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts index 62d9f70f2..08a58bfea 100644 --- a/packages/opencode/src/tool/patch.ts +++ b/packages/opencode/src/tool/patch.ts @@ -7,8 +7,8 @@ import { Bus } from "../bus" import { FileWatcher } from "../file/watcher" import { Instance } from "../project/instance" import { Patch } from "../patch" -import { Filesystem } from "../util/filesystem" import { createTwoFilesPatch } from "diff" +import { assertExternalDirectory } from "./external-directory" const PatchParams = z.object({ patchText: z.string().describe("The full patch text that describes all changes to be made"), @@ -49,19 +49,7 @@ export const PatchTool = Tool.define("patch", { for (const hunk of hunks) { const filePath = path.resolve(Instance.directory, hunk.path) - - if (!Filesystem.contains(Instance.directory, filePath)) { - const parentDir = path.dirname(filePath) - await ctx.ask({ - permission: "external_directory", - patterns: [parentDir, path.join(parentDir, "*")], - always: [parentDir + "/*"], - metadata: { - filepath: filePath, - parentDir, - }, - }) - } + await assertExternalDirectory(ctx, filePath) switch (hunk.type) { case "add": @@ -103,12 +91,15 @@ export const PatchTool = Tool.define("patch", { const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent) + const movePath = hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined + await assertExternalDirectory(ctx, movePath) + fileChanges.push({ filePath, oldContent, newContent, type: hunk.move_path ? "move" : "update", - movePath: hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined, + movePath, }) totalDiff += diff + "\n" diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 567425174..ce4ab2861 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -5,9 +5,9 @@ import { Tool } from "./tool" import { LSP } from "../lsp" import { FileTime } from "../file/time" import DESCRIPTION from "./read.txt" -import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Identifier } from "../id/id" +import { assertExternalDirectory } from "./external-directory" const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 @@ -27,18 +27,9 @@ export const ReadTool = Tool.define("read", { } const title = path.relative(Instance.worktree, filepath) - if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) { - const parentDir = path.dirname(filepath) - await ctx.ask({ - permission: "external_directory", - patterns: [parentDir], - always: [parentDir + "/*"], - metadata: { - filepath, - parentDir, - }, - }) - } + await assertExternalDirectory(ctx, filepath, { + bypass: Boolean(ctx.extra?.["bypassCwdCheck"]), + }) await ctx.ask({ permission: "read", diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index a0ca6b14f..222bac3f8 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -10,6 +10,7 @@ import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { trimDiff } from "./edit" +import { assertExternalDirectory } from "./external-directory" const MAX_DIAGNOSTICS_PER_FILE = 20 const MAX_PROJECT_DIAGNOSTICS_FILES = 5 @@ -22,12 +23,7 @@ export const WriteTool = Tool.define("write", { }), async execute(params, ctx) { const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) - /* TODO - if (!Filesystem.contains(Instance.directory, filepath)) { - const parentDir = path.dirname(filepath) - ... - } - */ + await assertExternalDirectory(ctx, filepath) const file = Bun.file(filepath) const exists = await file.exists() diff --git a/packages/opencode/test/tool/external-directory.test.ts b/packages/opencode/test/tool/external-directory.test.ts new file mode 100644 index 000000000..b21f6a971 --- /dev/null +++ b/packages/opencode/test/tool/external-directory.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import type { Tool } from "../../src/tool/tool" +import { Instance } from "../../src/project/instance" +import { assertExternalDirectory } from "../../src/tool/external-directory" +import type { PermissionNext } from "../../src/permission/next" + +const baseCtx: Omit = { + sessionID: "test", + messageID: "", + callID: "", + agent: "build", + abort: AbortSignal.any([]), + metadata: () => {}, +} + +describe("tool.assertExternalDirectory", () => { + test("no-ops for empty target", async () => { + const requests: Array> = [] + const ctx: Tool.Context = { + ...baseCtx, + ask: async (req) => { + requests.push(req) + }, + } + + await Instance.provide({ + directory: "/tmp", + fn: async () => { + await assertExternalDirectory(ctx) + }, + }) + + expect(requests.length).toBe(0) + }) + + test("no-ops for paths inside Instance.directory", async () => { + const requests: Array> = [] + const ctx: Tool.Context = { + ...baseCtx, + ask: async (req) => { + requests.push(req) + }, + } + + await Instance.provide({ + directory: "/tmp/project", + fn: async () => { + await assertExternalDirectory(ctx, path.join("/tmp/project", "file.txt")) + }, + }) + + expect(requests.length).toBe(0) + }) + + test("asks with a single canonical glob", async () => { + const requests: Array> = [] + const ctx: Tool.Context = { + ...baseCtx, + ask: async (req) => { + requests.push(req) + }, + } + + const directory = "/tmp/project" + const target = "/tmp/outside/file.txt" + const expected = path.join(path.dirname(target), "*") + + await Instance.provide({ + directory, + fn: async () => { + await assertExternalDirectory(ctx, target) + }, + }) + + const req = requests.find((r) => r.permission === "external_directory") + expect(req).toBeDefined() + expect(req!.patterns).toEqual([expected]) + expect(req!.always).toEqual([expected]) + }) + + test("uses target directory when kind=directory", async () => { + const requests: Array> = [] + const ctx: Tool.Context = { + ...baseCtx, + ask: async (req) => { + requests.push(req) + }, + } + + const directory = "/tmp/project" + const target = "/tmp/outside" + const expected = path.join(target, "*") + + await Instance.provide({ + directory, + fn: async () => { + await assertExternalDirectory(ctx, target, { kind: "directory" }) + }, + }) + + const req = requests.find((r) => r.permission === "external_directory") + expect(req).toBeDefined() + expect(req!.patterns).toEqual([expected]) + expect(req!.always).toEqual([expected]) + }) + + test("skips prompting when bypass=true", async () => { + const requests: Array> = [] + const ctx: Tool.Context = { + ...baseCtx, + ask: async (req) => { + requests.push(req) + }, + } + + await Instance.provide({ + directory: "/tmp/project", + fn: async () => { + await assertExternalDirectory(ctx, "/tmp/outside/file.txt", { bypass: true }) + }, + }) + + expect(requests.length).toBe(0) + }) +}) From 4752c83155332aa1c16f709ebe71325ab4ed4f9c Mon Sep 17 00:00:00 2001 From: Spoon <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Sun, 11 Jan 2026 01:21:51 +0100 Subject: [PATCH 060/470] feat: pass sessionID to chat.system.transform (#7718) --- packages/opencode/src/session/llm.ts | 2 +- packages/plugin/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index c5792c16d..9cd40f302 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -73,7 +73,7 @@ export namespace LLM { const header = system[0] const original = clone(system) - await Plugin.trigger("experimental.chat.system.transform", {}, { system }) + await Plugin.trigger("experimental.chat.system.transform", { sessionID: input.sessionID }, { system }) if (system.length === 0) { system.push(...original) } diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 46ad8512c..e57eff579 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -195,7 +195,7 @@ export interface Hooks { }, ) => Promise "experimental.chat.system.transform"?: ( - input: {}, + input: { sessionID: string }, output: { system: string[] }, From a457828a67548288276f120de2fd5bf4e60baed9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 10 Jan 2026 19:52:17 -0500 Subject: [PATCH 061/470] fix(opencode): command palette mouse hover highlights wrong item (#7721) --- packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index f2f2927c0..c5d36826c 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -255,7 +255,7 @@ export function DialogSelect(props: DialogSelectProps) { props.onSelect?.(option) }} onMouseOver={() => { - const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value)) + const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) if (index === -1) return moveTo(index) }} From 44fa3d5392d1a4c4cda65395be309d5420ee17ed Mon Sep 17 00:00:00 2001 From: wid4t Date: Sun, 11 Jan 2026 10:46:53 +0700 Subject: [PATCH 062/470] feat(acp): track file modified (#7723) Co-authored-by: Aiden Cline --- packages/opencode/src/acp/agent.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 6d8a64b7d..ebd65bb26 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -30,6 +30,7 @@ import { Todo } from "@/session/todo" import { z } from "zod" import { LoadAPIKeyError } from "ai" import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" +import { applyPatch } from "diff" export namespace ACP { const log = Log.create({ service: "acp-agent" }) @@ -105,6 +106,22 @@ export namespace ACP { }) return } + if (res.outcome.optionId !== "reject" && permission.permission == "edit") { + const metadata = permission.metadata || {} + const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : "" + const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : "" + + const content = await Bun.file(filepath).text() + const newContent = getNewContent(content, diff) + + if (newContent) { + this.connection.writeTextFile({ + sessionId: sessionId, + path: filepath, + content: newContent, + }) + } + } await this.config.sdk.permission.reply({ requestID: permission.id, reply: res.outcome.optionId as "once" | "always" | "reject", @@ -1095,4 +1112,13 @@ export namespace ACP { } } } + + function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined { + const result = applyPatch(fileOriginal, unifiedDiff) + if (result === false) { + log.error("Failed to apply unified diff (context mismatch)") + return undefined + } + return result + } } From ee8b38ab267e76cb05a4c4303188b8605886a70a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 10 Jan 2026 22:48:28 -0600 Subject: [PATCH 063/470] fix: ensure /connect works for openai business plans too (was missing field) --- packages/opencode/src/provider/auth.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index d06253ab4..e6681ff08 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -99,12 +99,16 @@ export namespace ProviderAuth { }) } if ("refresh" in result) { - await Auth.set(input.providerID, { + const info: Auth.Info = { type: "oauth", access: result.access, refresh: result.refresh, expires: result.expires, - }) + } + if (result.accountId) { + info.accountId = result.accountId + } + await Auth.set(input.providerID, info) } return } From 0cc3c3bc78ca4534578c96da89bdd31f5c357f9e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 10 Jan 2026 22:59:39 -0600 Subject: [PATCH 064/470] tweak: ensure codex built in plugin is always available --- packages/opencode/src/plugin/index.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 4912b8f74..f5670f73a 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -34,13 +34,10 @@ export namespace Plugin { $: Bun.$, } - // Load internal plugins first - if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) { - for (const plugin of INTERNAL_PLUGINS) { - log.info("loading internal plugin", { name: plugin.name }) - const init = await plugin(input) - hooks.push(init) - } + for (const plugin of INTERNAL_PLUGINS) { + log.info("loading internal plugin", { name: plugin.name }) + const init = await plugin(input) + hooks.push(init) } const plugins = [...(config.plugin ?? [])] From 9280db3297ae0e87ef44eb5538784bb94e4ab206 Mon Sep 17 00:00:00 2001 From: Mani Sundararajan <10191300+itsrainingmani@users.noreply.github.com> Date: Sun, 11 Jan 2026 01:20:04 -0500 Subject: [PATCH 065/470] fix(tui): move props.ref to onMount (#7702) --- .../cli/cmd/tui/component/prompt/index.tsx | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 4558914cb..1463c813e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -317,6 +317,38 @@ export function Prompt(props: PromptProps) { onMount(() => { promptPartTypeId = input.extmarks.registerType("prompt-part") + props.ref?.({ + get focused() { + return input.focused + }, + get current() { + return store.prompt + }, + focus() { + input.focus() + }, + blur() { + input.blur() + }, + set(prompt) { + input.setText(prompt.input) + setStore("prompt", prompt) + restoreExtmarksFromParts(prompt.parts) + input.gotoBufferEnd() + }, + reset() { + input.clear() + input.extmarks.clear() + setStore("prompt", { + input: "", + parts: [], + }) + setStore("extmarkToPartIndex", new Map()) + }, + submit() { + submit() + }, + }) }) function restoreExtmarksFromParts(parts: PromptInfo["parts"]) { @@ -452,39 +484,6 @@ export function Prompt(props: PromptProps) { }, ]) - props.ref?.({ - get focused() { - return input.focused - }, - get current() { - return store.prompt - }, - focus() { - input.focus() - }, - blur() { - input.blur() - }, - set(prompt) { - input.setText(prompt.input) - setStore("prompt", prompt) - restoreExtmarksFromParts(prompt.parts) - input.gotoBufferEnd() - }, - reset() { - input.clear() - input.extmarks.clear() - setStore("prompt", { - input: "", - parts: [], - }) - setStore("extmarkToPartIndex", new Map()) - }, - submit() { - submit() - }, - }) - async function submit() { if (props.disabled) return if (autocomplete?.visible) return From efbab087df781ff5b5c738c8421d8789f50ee7af Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 11 Jan 2026 07:10:41 +0000 Subject: [PATCH 066/470] release: v1.1.13 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 8e8dab33e..c83946240 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -99,7 +99,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -126,7 +126,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -150,7 +150,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -174,7 +174,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -203,7 +203,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -232,7 +232,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -248,7 +248,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.12", + "version": "1.1.13", "bin": { "opencode": "./bin/opencode", }, @@ -351,7 +351,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -371,7 +371,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.12", + "version": "1.1.13", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -382,7 +382,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -395,7 +395,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -434,7 +434,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "zod": "catalog:", }, @@ -445,7 +445,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 09083bdac..001cdcf3a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.12", + "version": "1.1.13", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 05fb0c8bf..6bd1ba077 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.12", + "version": "1.1.13", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 074932a91..5a30d5577 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.1.12", + "version": "1.1.13", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 33d1f5b3a..5e80e0670 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.12", + "version": "1.1.13", "$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 ea7ac4f56..dcc7c61b1 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.12", + "version": "1.1.13", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 66dcab94e..8f044da75 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.12", + "version": "1.1.13", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 368243dd9..21218d64d 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.12", + "version": "1.1.13", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index ed340fc36..3561c73d5 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.12" +version = "1.1.13" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.12/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 8597f2c80..75ca27ad0 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.12", + "version": "1.1.13", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ba0722045..2eaaa5f85 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.12", + "version": "1.1.13", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 489a582a4..8792455fb 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.1.12", + "version": "1.1.13", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index b9b150ccb..d467a30c9 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.1.12", + "version": "1.1.13", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 23d329166..b4083965d 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.12", + "version": "1.1.13", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index df45e4ba8..429c1f13e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.12", + "version": "1.1.13", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index c5ef19a61..b264a7de3 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.12", + "version": "1.1.13", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 8f06a6cee..bf6562cb6 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.1.12", + "version": "1.1.13", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 7ec45c3c8..5552188e4 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.1.12", + "version": "1.1.13", "publisher": "sst-dev", "repository": { "type": "git", From 3205db9c163d3d8fb411eaa2a5642d879ee05a30 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 11 Jan 2026 12:04:38 +0000 Subject: [PATCH 067/470] ignore: update download stats 2026-01-11 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index fad6b6452..b057e0418 100644 --- a/STATS.md +++ b/STATS.md @@ -197,3 +197,4 @@ | 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) | | 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) | | 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) | +| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) | From 43c2da24d01bcda5d2452f1ec90af0b0980f630a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 11 Jan 2026 11:11:40 -0500 Subject: [PATCH 068/470] fix(tui): slash command autocomplete highlighted row jumping (#7815) --- .../cmd/tui/component/prompt/autocomplete.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 3c757f81b..9aa3b551d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -1,7 +1,7 @@ import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core" import fuzzysort from "fuzzysort" import { firstBy } from "remeda" -import { createMemo, createResource, createEffect, onMount, onCleanup, For, Show, createSignal } from "solid-js" +import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js" import { createStore } from "solid-js/store" import { useSDK } from "@tui/context/sdk" import { useSync } from "@tui/context/sync" @@ -686,7 +686,7 @@ export function Autocomplete(props: { height={height()} scrollbarOptions={{ visible: false }} > - @@ -698,20 +698,20 @@ export function Autocomplete(props: { - - {option.display} + + {option().display} - - - {option.description} + + + {option().description} )} - + ) From bdbbcd8a0c60be5cee7a5206d5727be7e1100edf Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 11 Jan 2026 16:12:14 +0000 Subject: [PATCH 069/470] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 8792455fb..27a82d362 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index d467a30c9..c6c12c7e9 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From c526e2d908f47c59f1667902ba3750c6fc05ed4f Mon Sep 17 00:00:00 2001 From: King'ori Maina Date: Sun, 11 Jan 2026 18:27:17 +0200 Subject: [PATCH 070/470] fix(tui): copy oauth url when no device code (#7812) --- .../opencode/src/cli/cmd/tui/component/dialog-provider.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 8b586fce3..35951c995 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -112,8 +112,7 @@ function AutoMethod(props: AutoMethodProps) { useKeyboard((evt) => { if (evt.name === "c" && !evt.ctrl && !evt.meta) { - const code = - props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4}/)?.[0] ?? props.authorization.instructions + const code = props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4}/)?.[0] ?? props.authorization.url Clipboard.copy(code) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) From a803cf8aee73bac2209e00ea3c3a9aac655e7fb8 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 11 Jan 2026 12:45:20 -0500 Subject: [PATCH 071/470] feat(tui): add mouse hover and click support to autocomplete (#7820) --- .../opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 9aa3b551d..1ecfaaf1f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -700,6 +700,8 @@ export function Autocomplete(props: { paddingRight={1} backgroundColor={index === store.selected ? theme.primary : undefined} flexDirection="row" + onMouseOver={() => moveTo(index)} + onMouseUp={() => select()} > {option().display} From 20c18689c0c32de648489a50829a300858822b5c Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 11 Jan 2026 12:58:14 -0600 Subject: [PATCH 072/470] bump copilot plugin version --- packages/opencode/src/global/index.ts | 2 +- packages/opencode/src/plugin/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 2bd35971c..253b9663d 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -33,7 +33,7 @@ await Promise.all([ fs.mkdir(Global.Path.bin, { recursive: true }), ]) -const CACHE_VERSION = "16" +const CACHE_VERSION = "17" const version = await Bun.file(path.join(Global.Path.cache, "version")) .text() diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index f5670f73a..f57b46a35 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -12,7 +12,7 @@ import { CodexAuthPlugin } from "./codex" export namespace Plugin { const log = Log.create({ service: "plugin" }) - const BUILTIN = ["opencode-copilot-auth@0.0.11", "opencode-anthropic-auth@0.0.8"] + const BUILTIN = ["opencode-copilot-auth@0.0.12", "opencode-anthropic-auth@0.0.8"] // Built-in plugins that are directly imported (not installed from npm) const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin] From b81eca4ebc73279ab2c16fb1ca3291449501729d Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Sun, 11 Jan 2026 11:04:04 -0800 Subject: [PATCH 073/470] docs: fix typos on the providers page (#7829) --- packages/web/src/content/docs/providers.mdx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 882500b96..80c6f89e1 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -6,7 +6,7 @@ description: Using any LLM provider in OpenCode. import config from "../../../config.mjs" export const console = config.console -OpenCode uses the [AI SDK](https://ai-sdk.dev/) and [Models.dev](https://models.dev) to support for **75+ LLM providers** and it supports running local models. +OpenCode uses the [AI SDK](https://ai-sdk.dev/) and [Models.dev](https://models.dev) to support **75+ LLM providers** and it supports running local models. To add a provider you need to: @@ -80,8 +80,7 @@ If you are new, we recommend starting with OpenCode Zen. /models ``` -It works like any other provider in OpenCode. And is completely optional to use -it. +It works like any other provider in OpenCode and is completely optional to use. --- @@ -226,7 +225,7 @@ We recommend signing up for [Claude Pro](https://www.anthropic.com/news/claude-p └ ``` -3. Now all the the Anthropic models should be available when you use the `/models` command. +3. Now all the Anthropic models should be available when you use the `/models` command. ```txt /models From 025ed04da0aed68a17c47f7a814068c97767a361 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Sun, 11 Jan 2026 17:12:52 -0300 Subject: [PATCH 074/470] feat(desktop): Image Preview support for Image Attachments (#7841) --- packages/app/src/components/prompt-input.tsx | 6 +++++- packages/ui/src/components/message-part.css | 4 ---- packages/ui/src/components/message-part.tsx | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 97f54da5e..13f2b00a3 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -38,6 +38,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { ImagePreview } from "@opencode-ai/ui/image-preview" import { ModelSelectorPopover } from "@/components/dialog-select-model" import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" import { useProviders } from "@/hooks/use-providers" @@ -1484,7 +1485,10 @@ export const PromptInput: Component = (props) => { {attachment.filename} + dialog.show(() => ) + } />
    +
    + + {(msg() as UserMessage).agent} + + + {(msg() as UserMessage).model?.modelID} + + {(msg() as UserMessage).variant || "default"} +
    {/* User Message */} From 2e0c2c9db71e7471667ddade681b6cd9f2c05aab Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sun, 11 Jan 2026 20:16:29 +0000 Subject: [PATCH 076/470] chore(lander): fix spacing in desktop app banner (#7822) --- packages/console/app/src/routes/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/index.css b/packages/console/app/src/routes/index.css index f100acf8f..9df1d85c7 100644 --- a/packages/console/app/src/routes/index.css +++ b/packages/console/app/src/routes/index.css @@ -522,7 +522,7 @@ body { [data-slot="content"] { display: flex; align-items: center; - gap: 4px; + gap: 1ch; } [data-slot="text"] { From fa79736b875ba0a8437f42ff01626b35fab1d8f4 Mon Sep 17 00:00:00 2001 From: Dillon Mulroy Date: Sun, 11 Jan 2026 15:17:36 -0500 Subject: [PATCH 077/470] fix: check worktree for external_directory permission in subdirs (#7811) --- packages/opencode/src/file/index.ts | 4 +- packages/opencode/src/project/instance.ts | 13 +++ packages/opencode/src/tool/bash.ts | 4 +- .../opencode/src/tool/external-directory.ts | 3 +- .../opencode/test/file/path-traversal.test.ts | 83 +++++++++++++++++++ 5 files changed, 101 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index d2ff1d0b1..76b7be4b7 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -279,7 +279,7 @@ export namespace File { // TODO: Filesystem.contains is lexical only - symlinks inside the project can escape. // TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization. - if (!Filesystem.contains(Instance.directory, full)) { + if (!Instance.containsPath(full)) { throw new Error(`Access denied: path escapes project directory`) } @@ -339,7 +339,7 @@ export namespace File { // TODO: Filesystem.contains is lexical only - symlinks inside the project can escape. // TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization. - if (!Filesystem.contains(Instance.directory, resolved)) { + if (!Instance.containsPath(resolved)) { throw new Error(`Access denied: path escapes project directory`) } diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index d668037c0..ddaa90f1e 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -4,6 +4,7 @@ import { Project } from "./project" import { State } from "./state" import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" +import { Filesystem } from "@/util/filesystem" interface Context { directory: string @@ -46,6 +47,18 @@ export const Instance = { get project() { return context.use().project }, + /** + * Check if a path is within the project boundary. + * Returns true if path is inside Instance.directory OR Instance.worktree. + * Paths within the worktree but outside the working directory should not trigger external_directory permission. + */ + containsPath(filepath: string) { + if (Filesystem.contains(Instance.directory, filepath)) return true + // Non-git projects set worktree to "/" which would match ANY absolute path. + // Skip worktree check in this case to preserve external_directory permissions. + if (Instance.worktree === "/") return false + return Filesystem.contains(Instance.worktree, filepath) + }, state(init: () => S, dispose?: (state: Awaited) => Promise): () => S { return State.create(() => Instance.directory, init, dispose) }, diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index e06a3f157..f3a1b04d4 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -85,7 +85,7 @@ export const BashTool = Tool.define("bash", async () => { throw new Error("Failed to parse command") } const directories = new Set() - if (!Filesystem.contains(Instance.directory, cwd)) directories.add(cwd) + if (!Instance.containsPath(cwd)) directories.add(cwd) const patterns = new Set() const always = new Set() @@ -124,7 +124,7 @@ export const BashTool = Tool.define("bash", async () => { process.platform === "win32" && resolved.match(/^\/[a-z]\//) ? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\") : resolved - if (!Filesystem.contains(Instance.directory, normalized)) directories.add(normalized) + if (!Instance.containsPath(normalized)) directories.add(normalized) } } } diff --git a/packages/opencode/src/tool/external-directory.ts b/packages/opencode/src/tool/external-directory.ts index 05f83c00d..1d3958fc4 100644 --- a/packages/opencode/src/tool/external-directory.ts +++ b/packages/opencode/src/tool/external-directory.ts @@ -1,6 +1,5 @@ import path from "path" import type { Tool } from "./tool" -import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" type Kind = "file" | "directory" @@ -15,7 +14,7 @@ export async function assertExternalDirectory(ctx: Tool.Context, target?: string if (options?.bypass) return - if (Filesystem.contains(Instance.directory, target)) return + if (Instance.containsPath(target)) return const kind = options?.kind ?? "file" const parentDir = kind === "directory" ? target : path.dirname(target) diff --git a/packages/opencode/test/file/path-traversal.test.ts b/packages/opencode/test/file/path-traversal.test.ts index c20c76a2e..44ae8f154 100644 --- a/packages/opencode/test/file/path-traversal.test.ts +++ b/packages/opencode/test/file/path-traversal.test.ts @@ -1,5 +1,6 @@ import { test, expect, describe } from "bun:test" import path from "path" +import fs from "fs/promises" import { Filesystem } from "../../src/util/filesystem" import { File } from "../../src/file" import { Instance } from "../../src/project/instance" @@ -113,3 +114,85 @@ describe("File.list path traversal protection", () => { }) }) }) + +describe("Instance.containsPath", () => { + test("returns true for path inside directory", async () => { + await using tmp = await tmpdir({ git: true }) + + await Instance.provide({ + directory: tmp.path, + fn: () => { + expect(Instance.containsPath(path.join(tmp.path, "foo.txt"))).toBe(true) + expect(Instance.containsPath(path.join(tmp.path, "src", "file.ts"))).toBe(true) + }, + }) + }) + + test("returns true for path inside worktree but outside directory (monorepo subdirectory scenario)", async () => { + await using tmp = await tmpdir({ git: true }) + const subdir = path.join(tmp.path, "packages", "lib") + await fs.mkdir(subdir, { recursive: true }) + + await Instance.provide({ + directory: subdir, + fn: () => { + // .opencode at worktree root, but we're running from packages/lib + expect(Instance.containsPath(path.join(tmp.path, ".opencode", "state"))).toBe(true) + // sibling package should also be accessible + expect(Instance.containsPath(path.join(tmp.path, "packages", "other", "file.ts"))).toBe(true) + // worktree root itself + expect(Instance.containsPath(tmp.path)).toBe(true) + }, + }) + }) + + test("returns false for path outside both directory and worktree", async () => { + await using tmp = await tmpdir({ git: true }) + + await Instance.provide({ + directory: tmp.path, + fn: () => { + expect(Instance.containsPath("/etc/passwd")).toBe(false) + expect(Instance.containsPath("/tmp/other-project")).toBe(false) + }, + }) + }) + + test("returns false for path with .. escaping worktree", async () => { + await using tmp = await tmpdir({ git: true }) + + await Instance.provide({ + directory: tmp.path, + fn: () => { + expect(Instance.containsPath(path.join(tmp.path, "..", "escape.txt"))).toBe(false) + }, + }) + }) + + test("handles directory === worktree (running from repo root)", async () => { + await using tmp = await tmpdir({ git: true }) + + await Instance.provide({ + directory: tmp.path, + fn: () => { + expect(Instance.directory).toBe(Instance.worktree) + expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true) + expect(Instance.containsPath("/etc/passwd")).toBe(false) + }, + }) + }) + + test("non-git project does not allow arbitrary paths via worktree='/'", async () => { + await using tmp = await tmpdir() // no git: true + + await Instance.provide({ + directory: tmp.path, + fn: () => { + // worktree is "/" for non-git projects, but containsPath should NOT allow all paths + expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true) + expect(Instance.containsPath("/etc/passwd")).toBe(false) + expect(Instance.containsPath("/tmp/other")).toBe(false) + }, + }) + }) +}) From 0c0057a7deb8cde8df90e1899f39d42c6b9837ac Mon Sep 17 00:00:00 2001 From: Octane <88922959+Octane0411@users.noreply.github.com> Date: Mon, 12 Jan 2026 07:50:27 +0800 Subject: [PATCH 078/470] Fix: TUI single-line paste cursor position (#7277) --- packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 1463c813e..ad55c5268 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -915,7 +915,6 @@ export function Prompt(props: PromptProps) { // Force layout update and render for the pasted content setTimeout(() => { input.getLayoutNode().markDirty() - input.gotoBufferEnd() renderer.requestRender() }, 0) }} From 3a30773874ccb92d085eddbab66d053c5d4e6326 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 11 Jan 2026 18:53:34 -0500 Subject: [PATCH 079/470] tui: refactor event streaming to use SDK instead of manual RPC subscription --- STYLE_GUIDE.md | 67 ++++++++++++++++++- packages/opencode/src/cli/cmd/tui/thread.ts | 15 +---- packages/opencode/src/cli/cmd/tui/worker.ts | 72 ++++++++++++++++----- 3 files changed, 125 insertions(+), 29 deletions(-) diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 8dd3be589..a46ce221f 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -4,8 +4,71 @@ - AVOID unnecessary destructuring of variables. instead of doing `const { a, b } = obj` just reference it as obj.a and obj.b. this preserves context - AVOID `try`/`catch` where possible -- AVOID `else` statements - AVOID using `any` type -- AVOID `let` statements - PREFER single word variable names where possible - Use as many bun apis as possible like Bun.file() + +# Avoid let statements + +we don't like let statements, especially combined with if/else statements. +prefer const + +This is bad: + +Good: + +```ts +const foo = condition ? 1 : 2 +``` + +Bad: + +```ts +let foo + +if (condition) foo = 1 +else foo = 2 +``` + +# Avoid else statements + +Prefer early returns or even using `iife` to avoid else statements + +Good: + +```ts +function foo() { + if (condition) return 1 + return 2 +} +``` + +Bad: + +```ts +function foo() { + if (condition) return 1 + else return 2 +} +``` + +# Prefer single word naming + +Try your best to find a single word name for your variables, functions, etc. +Only use multiple words if you cannot. + +Good: + +```ts +const foo = 1 +const bar = 2 +const baz = 3 +``` + +Bad: + +```ts +const fooBar = 1 +const barBaz = 2 +const bazFoo = 3 +``` diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 5e50d38de..057142685 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -34,15 +34,9 @@ function createWorkerFetch(client: RpcClient): typeof fetch { return fn as typeof fetch } -function createEventSource(client: RpcClient, directory: string): EventSource { +function createEventSource(client: RpcClient): EventSource { return { - on: (handler) => - client.on("event", (event) => { - handler(event) - if (event.type === "server.instance.disposed") { - client.call("subscribe", { directory }).catch(() => {}) - } - }), + on: (handler) => client.on("event", handler), } } @@ -131,9 +125,6 @@ export const TuiThreadCommand = cmd({ networkOpts.port !== 0 || networkOpts.hostname !== "127.0.0.1" - // Subscribe to events from worker - await client.call("subscribe", { directory: cwd }) - let url: string let customFetch: typeof fetch | undefined let events: EventSource | undefined @@ -146,7 +137,7 @@ export const TuiThreadCommand = cmd({ // Use direct RPC communication (no HTTP) url = "http://opencode.internal" customFetch = createWorkerFetch(client) - events = createEventSource(client, cwd) + events = createEventSource(client) } const tuiPromise = tui({ diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index ea88e45f1..343a5a310 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -6,8 +6,8 @@ import { InstanceBootstrap } from "@/project/bootstrap" import { Rpc } from "@/util/rpc" import { upgrade } from "@/cli/upgrade" import { Config } from "@/config/config" -import { Bus } from "@/bus" import { GlobalBus } from "@/bus/global" +import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2" import type { BunWebSocketData } from "hono/bun" await Log.init({ @@ -38,6 +38,61 @@ GlobalBus.on("event", (event) => { let server: Bun.Server | undefined +const eventStream = { + abort: undefined as AbortController | undefined, +} + +const startEventStream = (directory: string) => { + if (eventStream.abort) eventStream.abort.abort() + const abort = new AbortController() + eventStream.abort = abort + const signal = abort.signal + + const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { + const request = new Request(input, init) + return Server.App().fetch(request) + }) as typeof globalThis.fetch + + const sdk = createOpencodeClient({ + baseUrl: "http://opencode.internal", + directory, + fetch: fetchFn, + signal, + }) + + ;(async () => { + while (!signal.aborted) { + const events = await Promise.resolve( + sdk.event.subscribe( + {}, + { + signal, + }, + ), + ).catch(() => undefined) + + if (!events) { + await Bun.sleep(250) + continue + } + + for await (const event of events.stream) { + Rpc.emit("event", event as Event) + } + + if (!signal.aborted) { + await Bun.sleep(250) + } + } + })().catch((error) => { + Log.Default.error("event stream error", { + error: error instanceof Error ? error.message : error, + }) + }) +} + +startEventStream(process.cwd()) + export const rpc = { async fetch(input: { url: string; method: string; headers: Record; body?: string }) { const request = new Request(input.url, { @@ -58,20 +113,6 @@ export const rpc = { server = Server.listen(input) return { url: server.url.toString() } }, - async subscribe(input: { directory: string }) { - return Instance.provide({ - directory: input.directory, - init: InstanceBootstrap, - fn: async () => { - Bus.subscribeAll((event) => { - Rpc.emit("event", event) - }) - // Emit connected event - Rpc.emit("event", { type: "server.connected", properties: {} }) - return { subscribed: true } - }, - }) - }, async checkUpgrade(input: { directory: string }) { await Instance.provide({ directory: input.directory, @@ -87,6 +128,7 @@ export const rpc = { }, async shutdown() { Log.Default.info("worker shutting down") + if (eventStream.abort) eventStream.abort.abort() await Instance.disposeAll() if (server) server.stop(true) }, From 68ed664a3f08c64061af0781bbcc345ebd245f5a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 11 Jan 2026 19:40:16 -0500 Subject: [PATCH 080/470] tui: fix prompt ref initialization to prevent undefined reference errors --- .../cli/cmd/tui/component/prompt/index.tsx | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index ad55c5268..644ac2622 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -89,7 +89,7 @@ export function Prompt(props: PromptProps) { const fileStyleId = syntax().getStyleId("extmark.file")! const agentStyleId = syntax().getStyleId("extmark.agent")! const pasteStyleId = syntax().getStyleId("extmark.paste")! - let promptPartTypeId: number + let promptPartTypeId = 0 sdk.event.on(TuiEvent.PromptAppend.type, (evt) => { input.insertText(evt.properties.text) @@ -310,47 +310,44 @@ export function Prompt(props: PromptProps) { ] }) + const ref: PromptRef = { + get focused() { + return input.focused + }, + get current() { + return store.prompt + }, + focus() { + input.focus() + }, + blur() { + input.blur() + }, + set(prompt) { + input.setText(prompt.input) + setStore("prompt", prompt) + restoreExtmarksFromParts(prompt.parts) + input.gotoBufferEnd() + }, + reset() { + input.clear() + input.extmarks.clear() + setStore("prompt", { + input: "", + parts: [], + }) + setStore("extmarkToPartIndex", new Map()) + }, + submit() { + submit() + }, + } + createEffect(() => { if (props.visible !== false) input?.focus() if (props.visible === false) input?.blur() }) - onMount(() => { - promptPartTypeId = input.extmarks.registerType("prompt-part") - props.ref?.({ - get focused() { - return input.focused - }, - get current() { - return store.prompt - }, - focus() { - input.focus() - }, - blur() { - input.blur() - }, - set(prompt) { - input.setText(prompt.input) - setStore("prompt", prompt) - restoreExtmarksFromParts(prompt.parts) - input.gotoBufferEnd() - }, - reset() { - input.clear() - input.extmarks.clear() - setStore("prompt", { - input: "", - parts: [], - }) - setStore("extmarkToPartIndex", new Map()) - }, - submit() { - submit() - }, - }) - }) - function restoreExtmarksFromParts(parts: PromptInfo["parts"]) { input.extmarks.clear() setStore("extmarkToPartIndex", new Map()) @@ -920,6 +917,10 @@ export function Prompt(props: PromptProps) { }} ref={(r: TextareaRenderable) => { input = r + if (promptPartTypeId === 0) { + promptPartTypeId = input.extmarks.registerType("prompt-part") + } + props.ref?.(ref) setTimeout(() => { input.cursorColor = theme.text }, 0) From 82b432349e4063f612f54758ba78999b75cf13fb Mon Sep 17 00:00:00 2001 From: indeep99 <43215243+indeep99@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:01:48 -0500 Subject: [PATCH 081/470] feat(tui): add mouse hover and click support to questions tool (#7905) --- .../cli/cmd/tui/routes/session/question.tsx | 77 +++++++++++-------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx index e43f4219b..c6518ec3f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx @@ -86,6 +86,38 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { setStore("answers", answers) } + function moveTo(index: number) { + setStore("selected", index) + } + + function selectTab(index: number) { + setStore("tab", index) + setStore("selected", 0) + } + + function selectOption() { + if (other()) { + if (!multi()) { + setStore("editing", true) + return + } + const value = input() + if (value && customPicked()) { + toggle(value) + return + } + setStore("editing", true) + return + } + const opt = options()[store.selected] + if (!opt) return + if (multi()) { + toggle(opt.label) + return + } + pick(opt.label) + } + const dialog = useDialog() useKeyboard((evt) => { @@ -149,16 +181,12 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { if (evt.name === "left" || evt.name === "h") { evt.preventDefault() - const next = (store.tab - 1 + tabs()) % tabs() - setStore("tab", next) - setStore("selected", 0) + selectTab((store.tab - 1 + tabs()) % tabs()) } if (evt.name === "right" || evt.name === "l") { evt.preventDefault() - const next = (store.tab + 1) % tabs() - setStore("tab", next) - setStore("selected", 0) + selectTab((store.tab + 1) % tabs()) } if (confirm()) { @@ -176,36 +204,17 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { if (evt.name === "up" || evt.name === "k") { evt.preventDefault() - setStore("selected", (store.selected - 1 + total) % total) + moveTo((store.selected - 1 + total) % total) } if (evt.name === "down" || evt.name === "j") { evt.preventDefault() - setStore("selected", (store.selected + 1) % total) + moveTo((store.selected + 1) % total) } if (evt.name === "return") { evt.preventDefault() - if (other()) { - if (!multi()) { - setStore("editing", true) - return - } - const value = input() - if (value && customPicked()) { - toggle(value) - return - } - setStore("editing", true) - return - } - const opt = opts[store.selected] - if (!opt) return - if (multi()) { - toggle(opt.label) - return - } - pick(opt.label) + selectOption() } if (evt.name === "escape" || keybind.match("app_exit", evt)) { @@ -236,6 +245,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { paddingLeft={1} paddingRight={1} backgroundColor={isActive() ? theme.accent : theme.backgroundElement} + onMouseUp={() => selectTab(index())} > {q.header} @@ -244,7 +254,12 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { ) }}
    - + selectTab(questions().length)} + > Confirm @@ -264,7 +279,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { const active = () => i() === store.selected const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false return ( - + moveTo(i())} onMouseUp={() => selectOption()}> @@ -280,7 +295,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { ) }}
    - + moveTo(options().length)} onMouseUp={() => selectOption()}> From c4ba5961c89313e630be48607c1599111524ab3f Mon Sep 17 00:00:00 2001 From: Prince Carlo Juguilon <47204120+princejoogie@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:19:59 +0800 Subject: [PATCH 082/470] chore: update GitHub stars count to 60K (#7899) --- packages/console/app/src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/console/app/src/config.ts b/packages/console/app/src/config.ts index b92fb1166..4ebb2c71a 100644 --- a/packages/console/app/src/config.ts +++ b/packages/console/app/src/config.ts @@ -9,8 +9,8 @@ export const config = { github: { repoUrl: "https://github.com/anomalyco/opencode", starsFormatted: { - compact: "50K", - full: "50,000", + compact: "60K", + full: "60,000", }, }, From 7cbec9a1a78d8c42b83ca0fd0d0efdbc69f813b7 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Sun, 11 Jan 2026 19:20:19 -0800 Subject: [PATCH 083/470] docs: fix typos in settings doc (#7892) --- packages/docs/essentials/settings.mdx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/docs/essentials/settings.mdx b/packages/docs/essentials/settings.mdx index 7aa44ce1e..2cc202ed1 100644 --- a/packages/docs/essentials/settings.mdx +++ b/packages/docs/essentials/settings.mdx @@ -57,11 +57,10 @@ Example: `mintlify` Hex color codes for your global theme - The primary color. Used for most often for highlighted content, section headers, accents, in light mode + The primary color. Used most often for highlighted content, section headers, accents, in light mode - The primary color for dark mode. Used for most often for highlighted content, section headers, accents, in dark - mode + The primary color for dark mode. Used most often for highlighted content, section headers, accents, in dark mode The primary color for important buttons From 8b9a85b7e7278813cd6be733d7cd67ef3ea24972 Mon Sep 17 00:00:00 2001 From: Andrey Taranov <86911+antigremlin@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:39:42 +0000 Subject: [PATCH 084/470] fix(mcp): support `resource` content type in MCP tool output (#7879) --- packages/opencode/src/session/prompt.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f89161227..34596e629 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -777,8 +777,23 @@ export namespace SessionPrompt { mime: contentItem.mimeType, url: `data:${contentItem.mimeType};base64,${contentItem.data}`, }) + } else if (contentItem.type === "resource") { + const { resource } = contentItem + if (resource.text) { + textParts.push(resource.text) + } + if (resource.blob) { + attachments.push({ + id: Identifier.ascending("part"), + sessionID: input.session.id, + messageID: input.processor.message.id, + type: "file", + mime: resource.mimeType ?? "application/octet-stream", + url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`, + filename: resource.uri, + }) + } } - // Add support for other types if needed } return { From 65724b693b246ae799147d164610de9f9d5ffd1a Mon Sep 17 00:00:00 2001 From: Eyal Cherevatsky Date: Mon, 12 Jan 2026 05:40:23 +0200 Subject: [PATCH 085/470] docs: fix scroll_speed default value (#7867) --- packages/web/src/content/docs/config.mdx | 2 +- packages/web/src/content/docs/tui.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index a5931b6fc..30edbbd21 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -168,7 +168,7 @@ You can configure TUI-specific settings through the `tui` option. Available options: - `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration. **Takes precedence over `scroll_speed`.** -- `scroll_speed` - Custom scroll speed multiplier (default: `1`, minimum: `1`). Ignored if `scroll_acceleration.enabled` is `true`. +- `scroll_speed` - Custom scroll speed multiplier (default: `3`, minimum: `1`). Ignored if `scroll_acceleration.enabled` is `true`. - `diff_style` - Control diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows single column. [Learn more about using the TUI here](/docs/tui). diff --git a/packages/web/src/content/docs/tui.mdx b/packages/web/src/content/docs/tui.mdx index a92da6a02..a7f59f215 100644 --- a/packages/web/src/content/docs/tui.mdx +++ b/packages/web/src/content/docs/tui.mdx @@ -358,7 +358,7 @@ You can customize TUI behavior through your OpenCode config file. ### Options - `scroll_acceleration` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.** -- `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `1`). Defaults to `1` on Unix and `3` on Windows. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.** +- `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `1`). Defaults to `3`. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.** --- From a5b6c57a76a00f367b4d683cce0681c88596b90e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 11 Jan 2026 22:41:10 -0600 Subject: [PATCH 086/470] tweak: make the subagent header have clickable nav --- .../src/cli/cmd/tui/routes/session/header.tsx | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 74cc9af4e..0b690cfac 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -1,10 +1,11 @@ -import { type Accessor, createMemo, Match, Show, Switch } from "solid-js" +import { type Accessor, createMemo, createSignal, Match, Show, Switch } from "solid-js" import { useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" import { pipe, sumBy } from "remeda" import { useTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2" +import { useCommandDialog } from "@tui/component/dialog-command" import { useKeybind } from "../../context/keybind" const Title = (props: { session: Accessor }) => { @@ -59,6 +60,8 @@ export function Header() { const { theme } = useTheme() const keybind = useKeybind() + const command = useCommandDialog() + const [hover, setHover] = createSignal<"parent" | "prev" | "next" | null>(null) return ( @@ -79,15 +82,36 @@ export function Header() { Subagent session - - Parent {keybind.print("session_parent")} - - - Prev {keybind.print("session_child_cycle_reverse")} - - - Next {keybind.print("session_child_cycle")} - + setHover("parent")} + onMouseOut={() => setHover(null)} + onMouseUp={() => command.trigger("session.parent")} + backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel} + > + + Parent {keybind.print("session_parent")} + + + setHover("prev")} + onMouseOut={() => setHover(null)} + onMouseUp={() => command.trigger("session.child.previous")} + backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel} + > + + Prev {keybind.print("session_child_cycle_reverse")} + + + setHover("next")} + onMouseOut={() => setHover(null)} + onMouseUp={() => command.trigger("session.child.next")} + backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel} + > + + Next {keybind.print("session_child_cycle")} + + From 7c6b3f981e56b94b5948619d23e7312a7c7583c0 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 12 Jan 2026 05:08:25 +0000 Subject: [PATCH 087/470] release: v1.1.14 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index c83946240..3a8886bf9 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -99,7 +99,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -126,7 +126,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -150,7 +150,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -174,7 +174,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -203,7 +203,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -232,7 +232,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -248,7 +248,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.13", + "version": "1.1.14", "bin": { "opencode": "./bin/opencode", }, @@ -351,7 +351,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -371,7 +371,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.13", + "version": "1.1.14", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -382,7 +382,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -395,7 +395,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -434,7 +434,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "zod": "catalog:", }, @@ -445,7 +445,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 001cdcf3a..4a88f1940 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.13", + "version": "1.1.14", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 6bd1ba077..efbc2486b 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.13", + "version": "1.1.14", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 5a30d5577..0e3e6137b 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.1.13", + "version": "1.1.14", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 5e80e0670..b18bbafb1 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.13", + "version": "1.1.14", "$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 dcc7c61b1..a09e1cec2 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.13", + "version": "1.1.14", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 8f044da75..b65eaf7ce 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.13", + "version": "1.1.14", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 21218d64d..859c69095 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.13", + "version": "1.1.14", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 3561c73d5..07dfd061f 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.13" +version = "1.1.14" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.13/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 75ca27ad0..535364a99 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.13", + "version": "1.1.14", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 2eaaa5f85..55a0ba06d 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.13", + "version": "1.1.14", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 27a82d362..18aa578fc 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.1.13", + "version": "1.1.14", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index c6c12c7e9..5d4f5c1f4 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.1.13", + "version": "1.1.14", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index b4083965d..82f7aef86 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.13", + "version": "1.1.14", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 429c1f13e..fb7f48953 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.13", + "version": "1.1.14", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index b264a7de3..4a9a8d289 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.13", + "version": "1.1.14", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index bf6562cb6..bc136e5da 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.1.13", + "version": "1.1.14", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 5552188e4..5ab0fd7b7 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.1.13", + "version": "1.1.14", "publisher": "sst-dev", "repository": { "type": "git", From e0eb460fc85562089af21abf6e36af8858e59270 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 12 Jan 2026 13:21:17 +0800 Subject: [PATCH 088/470] app: resolve defaultServerUrl inside AppInterface The desktop app sometimes modified __OPENCODE__.serverUrl after the window is created. Previously this was ignored since defaultServerUrl was created at module scope, now it isn't created until desktop's ensure_server_started query complete, after which point serverUrl is guaranteed to be updated. --- packages/app/src/app.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index bf6da3b4d..91e4ae39b 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -37,16 +37,6 @@ declare global { } } -const defaultServerUrl = iife(() => { - if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" - if (window.__OPENCODE__?.serverUrl) return window.__OPENCODE__.serverUrl - if (window.__OPENCODE__?.port) return `http://127.0.0.1:${window.__OPENCODE__.port}` - if (import.meta.env.DEV) - return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` - - return window.location.origin -}) - export function AppBaseProviders(props: ParentProps) { return ( @@ -76,6 +66,16 @@ function ServerKey(props: ParentProps) { } export function AppInterface() { + const defaultServerUrl = iife(() => { + if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" + if (window.__OPENCODE__?.serverUrl) return window.__OPENCODE__.serverUrl + if (window.__OPENCODE__?.port) return `http://127.0.0.1:${window.__OPENCODE__.port}` + if (import.meta.env.DEV) + return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` + + return window.location.origin + }) + return ( From 087473be6ec4ad21220752de2391e8b882f7058f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 12 Jan 2026 05:22:12 +0000 Subject: [PATCH 089/470] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 18aa578fc..3931f5b7c 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 5d4f5c1f4..6e8867611 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From ebbb4dd88a55a69ea1722eb0622b91d697acf507 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 12 Jan 2026 17:58:13 +0800 Subject: [PATCH 090/470] fix(desktop): improve server detection & connection logic (#7962) --- packages/app/src/app.tsx | 13 +- packages/desktop/src-tauri/Cargo.toml | 2 +- packages/desktop/src-tauri/src/cli.rs | 56 ++++- packages/desktop/src-tauri/src/lib.rs | 288 ++++++++++---------------- packages/desktop/src/index.tsx | 21 +- 5 files changed, 181 insertions(+), 199 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 91e4ae39b..5dfc59a4e 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -33,7 +33,7 @@ const Loading = () =>
    + diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index c8eb0846c..8033d4f14 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -3,7 +3,7 @@ name = "opencode-desktop" version = "0.0.0" description = "The open source AI coding agent" authors = ["Anomaly Innovations"] -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 8b76d1a7f..87ecf4997 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -1,8 +1,30 @@ -use tauri::Manager; +use tauri::{path::BaseDirectory, AppHandle, Manager}; +use tauri_plugin_shell::{process::Command, ShellExt}; const CLI_INSTALL_DIR: &str = ".opencode/bin"; const CLI_BINARY_NAME: &str = "opencode"; +#[derive(serde::Deserialize)] +pub struct ServerConfig { + pub hostname: Option, + pub port: Option, +} + +#[derive(serde::Deserialize)] +pub struct Config { + pub server: Option, +} + +pub async fn get_config(app: &AppHandle) -> Option { + create_command(app, "debug config") + .output() + .await + .inspect_err(|e| eprintln!("Failed to read OC config: {e}")) + .ok() + .and_then(|out| String::from_utf8(out.stdout.to_vec()).ok()) + .and_then(|s| serde_json::from_str::(&s).ok()) +} + fn get_cli_install_path() -> Option { std::env::var("HOME").ok().map(|home| { std::path::PathBuf::from(home) @@ -117,3 +139,35 @@ pub fn sync_cli(app: tauri::AppHandle) -> Result<(), String> { Ok(()) } + +fn get_user_shell() -> String { + std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()) +} + +pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command { + let state_dir = app + .path() + .resolve("", BaseDirectory::AppLocalData) + .expect("Failed to resolve app local data dir"); + + #[cfg(target_os = "windows")] + return app + .shell() + .sidecar("opencode-cli") + .unwrap() + .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") + .env("OPENCODE_CLIENT", "desktop") + .env("XDG_STATE_HOME", &state_dir); + + #[cfg(not(target_os = "windows"))] + return { + let sidecar = get_sidecar_path(app); + let shell = get_user_shell(); + app.shell() + .command(&shell) + .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") + .env("OPENCODE_CLIENT", "desktop") + .env("XDG_STATE_HOME", &state_dir) + .args(["-il", "-c", &format!("\"{}\" {}", sidecar.display(), args)]) + }; +} diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 5ed03fc66..b479ed0b6 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -1,23 +1,18 @@ mod cli; mod window_customizer; -use cli::{get_sidecar_path, install_cli, sync_cli}; +use cli::{install_cli, sync_cli}; use futures::FutureExt; use std::{ collections::VecDeque, - net::{SocketAddr, TcpListener}, + net::TcpListener, sync::{Arc, Mutex}, time::{Duration, Instant}, }; -use tauri::{ - path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, - WebviewWindow, -}; +use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, WebviewWindow}; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; -use tauri_plugin_shell::ShellExt; use tauri_plugin_store::StoreExt; -use tokio::net::TcpSocket; use crate::window_customizer::PinchZoomDisablePlugin; @@ -27,13 +22,13 @@ const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; #[derive(Clone)] struct ServerState { child: Arc>>, - status: futures::future::Shared>>, + status: futures::future::Shared>>, } impl ServerState { pub fn new( child: Option, - status: tokio::sync::oneshot::Receiver>, + status: tokio::sync::oneshot::Receiver>, ) -> Self { Self { child: Arc::new(Mutex::new(child)), @@ -85,7 +80,7 @@ async fn get_logs(app: AppHandle) -> Result { } #[tauri::command] -async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), String> { +async fn ensure_server_ready(state: State<'_, ServerState>) -> Result { state .status .clone() @@ -94,7 +89,7 @@ async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), Stri } #[tauri::command] -async fn get_default_server_url(app: AppHandle) -> Result, String> { +fn get_default_server_url(app: AppHandle) -> Result, String> { let store = app .store(SETTINGS_STORE) .map_err(|e| format!("Failed to open settings store: {}", e))?; @@ -142,49 +137,16 @@ fn get_sidecar_port() -> u32 { }) as u32 } -fn get_user_shell() -> String { - std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()) -} - fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { let log_state = app.state::(); let log_state_clone = log_state.inner().clone(); - let state_dir = app - .path() - .resolve("", BaseDirectory::AppLocalData) - .expect("Failed to resolve app local data dir"); + println!("spawning sidecar on port {port}"); - #[cfg(target_os = "windows")] - let (mut rx, child) = app - .shell() - .sidecar("opencode-cli") - .unwrap() - .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") - .env("OPENCODE_CLIENT", "desktop") - .env("XDG_STATE_HOME", &state_dir) - .args(["serve", &format!("--port={port}")]) + let (mut rx, child) = cli::create_command(app, format!("serve --port {port}").as_str()) .spawn() .expect("Failed to spawn opencode"); - #[cfg(not(target_os = "windows"))] - let (mut rx, child) = { - let sidecar = get_sidecar_path(app); - let shell = get_user_shell(); - app.shell() - .command(&shell) - .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") - .env("OPENCODE_CLIENT", "desktop") - .env("XDG_STATE_HOME", &state_dir) - .args([ - "-il", - "-c", - &format!("\"{}\" serve --port={}", sidecar.display(), port), - ]) - .spawn() - .expect("Failed to spawn opencode") - }; - tauri::async_runtime::spawn(async move { while let Some(event) = rx.recv().await { match event { @@ -222,17 +184,6 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { child } -async fn is_server_running(port: u32) -> bool { - TcpSocket::new_v4() - .unwrap() - .connect(SocketAddr::new( - "127.0.0.1".parse().expect("Failed to parse IP"), - port as u16, - )) - .await - .is_ok() -} - async fn check_server_health(url: &str) -> bool { let health_url = format!("{}/health", url.trim_end_matches('/')); let client = reqwest::Client::builder() @@ -251,12 +202,6 @@ async fn check_server_health(url: &str) -> bool { .unwrap_or(false) } -fn get_configured_server_url(app: &AppHandle) -> Option { - let store = app.store(SETTINGS_STORE).ok()?; - let value = store.get(DEFAULT_SERVER_URL_KEY)?; - value.as_str().map(String::from) -} - #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); @@ -283,7 +228,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ kill_sidecar, install_cli, - ensure_server_started, + ensure_server_ready, get_default_server_url, set_default_server_url ]) @@ -293,15 +238,11 @@ pub fn run() { // Initialize log state app.manage(LogState(Arc::new(Mutex::new(VecDeque::new())))); - // Get port and create window immediately for faster perceived startup - let port = get_sidecar_port(); - let primary_monitor = app.primary_monitor().ok().flatten(); let size = primary_monitor .map(|m| m.size().to_logical(m.scale_factor())) .unwrap_or(LogicalSize::new(1920, 1080)); - // Create window immediately with serverReady = false #[allow(unused_mut)] let mut window_builder = WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into())) @@ -314,7 +255,6 @@ pub fn run() { r#" window.__OPENCODE__ ??= {{}}; window.__OPENCODE__.updaterEnabled = {updater_enabled}; - window.__OPENCODE__.port = {port}; "# )); @@ -325,7 +265,7 @@ pub fn run() { .hidden_title(true); } - let window = window_builder.build().expect("Failed to create window"); + window_builder.build().expect("Failed to create window"); let (tx, rx) = tokio::sync::oneshot::channel(); app.manage(ServerState::new(None, rx)); @@ -333,116 +273,29 @@ pub fn run() { { let app = app.clone(); tauri::async_runtime::spawn(async move { - // Check for configured default server URL - let configured_url = get_configured_server_url(&app); + let mut custom_url = None; - let (child, res, server_url) = if let Some(ref url) = configured_url { - println!("Configured default server URL: {}", url); - - // Try to connect to the configured server - let mut healthy = false; - let mut should_fallback = false; - - loop { - if check_server_health(url).await { - healthy = true; - println!("Connected to configured server: {}", url); - break; - } - - let res = app.dialog() - .message(format!("Could not connect to configured server:\n{}\n\nWould you like to retry or start a local server instead?", url)) - .title("Connection Failed") - .buttons(MessageDialogButtons::OkCancelCustom("Retry".to_string(), "Start Local".to_string())) - .blocking_show_with_result(); - - match res { - MessageDialogResult::Custom(name) if name == "Retry" => { - continue; - }, - _ => { - should_fallback = true; - break; - } - } - } - - if healthy { - (None, Ok(()), Some(url.clone())) - } else if should_fallback { - // Fall back to spawning local sidecar - let child = spawn_sidecar(&app, port); - - let timestamp = Instant::now(); - let res = loop { - if timestamp.elapsed() > Duration::from_secs(7) { - break Err(format!( - "Failed to spawn OpenCode Server. Logs:\n{}", - get_logs(app.clone()).await.unwrap() - )); - } - - tokio::time::sleep(Duration::from_millis(10)).await; - - if is_server_running(port).await { - tokio::time::sleep(Duration::from_millis(10)).await; - break Ok(()); - } - }; - - println!("Server ready after {:?}", timestamp.elapsed()); - (Some(child), res, None) - } else { - (None, Err("User cancelled".to_string()), None) - } - } else { - // No configured URL, spawn local sidecar as before - let should_spawn_sidecar = !is_server_running(port).await; - - let (child, res) = if should_spawn_sidecar { - let child = spawn_sidecar(&app, port); - - let timestamp = Instant::now(); - let res = loop { - if timestamp.elapsed() > Duration::from_secs(7) { - break Err(format!( - "Failed to spawn OpenCode Server. Logs:\n{}", - get_logs(app.clone()).await.unwrap() - )); - } - - tokio::time::sleep(Duration::from_millis(10)).await; - - if is_server_running(port).await { - tokio::time::sleep(Duration::from_millis(10)).await; - break Ok(()); - } - }; - - println!("Server ready after {:?}", timestamp.elapsed()); - - (Some(child), res) - } else { - (None, Ok(())) - }; - - (child, res, None) - }; - - app.state::().set_child(child); - - if res.is_ok() { - let _ = window.eval("window.__OPENCODE__.serverReady = true;"); - - // If using a configured server URL, inject it - if let Some(url) = server_url { - let escaped_url = url.replace('\\', "\\\\").replace('"', "\\\""); - let _ = window.eval(format!( - "window.__OPENCODE__.serverUrl = \"{escaped_url}\";", - )); - } + if let Some(url) = get_default_server_url(app.clone()).ok().flatten() { + println!("Using desktop-specific custom URL: {url}"); + custom_url = Some(url); } + if custom_url.is_none() + && let Some(cli_config) = cli::get_config(&app).await + && let Some(url) = get_server_url_from_config(&cli_config) + { + println!("Using custom server URL from config: {url}"); + custom_url = Some(url); + } + + let res = match setup_server_connection(&app, custom_url).await { + Ok((child, url)) => { + app.state::().set_child(child); + Ok(url) + } + Err(e) => Err(e), + }; + let _ = tx.send(res); }); } @@ -474,3 +327,82 @@ pub fn run() { } }); } + +fn get_server_url_from_config(config: &cli::Config) -> Option { + let server = config.server.as_ref()?; + let port = server.port?; + println!("server.port found in OC config: {port}"); + let hostname = server.hostname.as_ref(); + + Some(format!( + "http://{}:{}", + hostname.map(|v| v.as_str()).unwrap_or("127.0.0.1"), + port + )) +} + +async fn setup_server_connection( + app: &AppHandle, + custom_url: Option, +) -> Result<(Option, String), String> { + if let Some(url) = custom_url { + loop { + if check_server_health(&url).await { + println!("Connected to custom server: {}", url); + return Ok((None, url.clone())); + } + + const RETRY: &str = "Retry"; + + let res = app.dialog() + .message(format!("Could not connect to configured server:\n{}\n\nWould you like to retry or start a local server instead?", url)) + .title("Connection Failed") + .buttons(MessageDialogButtons::OkCancelCustom(RETRY.to_string(), "Start Local".to_string())) + .blocking_show_with_result(); + + match res { + MessageDialogResult::Custom(name) if name == RETRY => { + continue; + } + _ => { + break; + } + } + } + } + + let local_port = get_sidecar_port(); + let local_url = format!("http://127.0.0.1:{local_port}"); + + if !check_server_health(&local_url).await { + match spawn_local_server(app, local_port).await { + Ok(child) => Ok(Some(child)), + Err(err) => Err(err), + } + } else { + Ok(None) + } + .map(|child| (child, local_url)) +} + +async fn spawn_local_server(app: &AppHandle, port: u32) -> Result { + let child = spawn_sidecar(app, port); + let url = format!("http://127.0.0.1:{port}"); + + let timestamp = Instant::now(); + loop { + if timestamp.elapsed() > Duration::from_secs(7) { + break Err(format!( + "Failed to spawn OpenCode Server. Logs:\n{}", + get_logs(app.clone()).await.unwrap() + )); + } + + tokio::time::sleep(Duration::from_millis(10)).await; + + if check_server_health(&url).await { + println!("Server ready after {:?}", timestamp.elapsed()); + break Ok(child); + } + } +} diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index ffb178672..6393b8b45 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -13,7 +13,7 @@ import { AsyncStorage } from "@solid-primitives/storage" import { fetch as tauriFetch } from "@tauri-apps/plugin-http" import { Store } from "@tauri-apps/plugin-store" import { Logo } from "@opencode-ai/ui/logo" -import { Suspense, createResource, ParentProps } from "solid-js" +import { Accessor, JSX, createResource } from "solid-js" import { UPDATER_ENABLED } from "./updater" import { createMenu } from "./menu" @@ -283,7 +283,9 @@ render(() => { )} - + {serverUrl => + + } @@ -291,26 +293,21 @@ render(() => { }, root!) // Gate component that waits for the server to be ready -function ServerGate(props: ParentProps) { - const [status] = createResource(async () => { - if (window.__OPENCODE__?.serverReady) return - return await invoke("ensure_server_started") - }) +function ServerGate(props: { children: (url: Accessor) => JSX.Element }) { + const [serverUrl] = createResource(() => invoke("ensure_server_ready")) return ( // Not using suspense as not all components are compatible with it (undefined refs) -
    Starting server...
    +
    Initializing...
    } > - {/* Trigger error boundary without rendering the returned value */} - {(status(), null)} - {props.children} + {serverUrl => props.children(serverUrl)} ) } From e6045ca925fa946839dc75663a9a3ea930a54e1a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 12 Jan 2026 09:58:50 +0000 Subject: [PATCH 091/470] chore: generate --- packages/app/src/app.tsx | 6 +++--- packages/desktop/src/index.tsx | 8 ++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 5dfc59a4e..3f8080972 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -33,7 +33,7 @@ const Loading = () =>
    diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 6393b8b45..a26cc61ca 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -282,11 +282,7 @@ render(() => {
    )} - - {serverUrl => - - } - + {(serverUrl) => } ) @@ -307,7 +303,7 @@ function ServerGate(props: { children: (url: Accessor) => JSX.Element })
    } > - {serverUrl => props.children(serverUrl)} + {(serverUrl) => props.children(serverUrl)} ) } From c6092e4ad9e97fc594f5f2f453612600b4a0f4d2 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 12 Jan 2026 19:28:55 +0800 Subject: [PATCH 092/470] disable appimage --- packages/console/app/src/routes/download/index.tsx | 5 +++-- packages/desktop/src-tauri/tauri.conf.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/console/app/src/routes/download/index.tsx b/packages/console/app/src/routes/download/index.tsx index 47555f541..4290efd16 100644 --- a/packages/console/app/src/routes/download/index.tsx +++ b/packages/console/app/src/routes/download/index.tsx @@ -244,7 +244,8 @@ export default function Download() { Download
    -
    + {/* Disabled temporarily as it doesn't work */} + {/*
    @@ -259,7 +260,7 @@ export default function Download() { Download -
    +
    */}
    diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json index bcb067a32..b6737d383 100644 --- a/packages/desktop/src-tauri/tauri.conf.json +++ b/packages/desktop/src-tauri/tauri.conf.json @@ -26,7 +26,7 @@ "icons/dev/icon.ico" ], "active": true, - "targets": ["deb", "rpm", "dmg", "nsis", "app", "appimage"], + "targets": ["deb", "rpm", "dmg", "nsis", "app"], "externalBin": ["sidecars/opencode-cli"], "macOS": { "entitlements": "./entitlements.plist" From 4695e685cf68f2dcdf9e952b90c34241c71b2df3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 12 Jan 2026 12:05:37 +0000 Subject: [PATCH 093/470] ignore: update download stats 2026-01-12 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index b057e0418..f0354e19f 100644 --- a/STATS.md +++ b/STATS.md @@ -198,3 +198,4 @@ | 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) | | 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) | | 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) | +| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) | From f1a13f25a410ba79fd1537a4ac0df5864ac14530 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 12 Jan 2026 21:27:52 +0800 Subject: [PATCH 094/470] ci: don't continue-on-error in tauri action --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3a9a9b4ad..8d7a823b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -92,7 +92,7 @@ jobs: publish-tauri: needs: publish - continue-on-error: true + continue-on-error: false strategy: fail-fast: false matrix: From e6bc3b253b837387363b8900ec82a317c130b53c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 12 Jan 2026 09:30:31 -0500 Subject: [PATCH 095/470] tui: remove update complete toast notification --- packages/opencode/src/cli/cmd/tui/app.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index aa62c6c58..1fea3f4b3 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -606,15 +606,6 @@ function App() { }) }) - sdk.event.on(Installation.Event.Updated.type, (evt) => { - toast.show({ - variant: "success", - title: "Update Complete", - message: `OpenCode updated to v${evt.properties.version}`, - duration: 5000, - }) - }) - sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => { toast.show({ variant: "info", From c009cab15b0590aeadb5030661af348bae75b1ea Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:31:36 +0000 Subject: [PATCH 096/470] Redesign tips display on home screen (#6126) Co-authored-by: Dax --- .../cli/cmd/tui/component/did-you-know.tsx | 50 +---- .../src/cli/cmd/tui/component/tips.ts | 202 +++++++++--------- .../opencode/src/cli/cmd/tui/routes/home.tsx | 22 +- 3 files changed, 126 insertions(+), 148 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx b/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx index 36bf174c1..10042d938 100644 --- a/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx @@ -1,8 +1,6 @@ import { createMemo, createSignal, For } from "solid-js" import { useTheme } from "@tui/context/theme" -import { useKeybind } from "@tui/context/keybind" import { TIPS } from "./tips" -import { EmptyBorder } from "./border" type TipPart = { text: string; highlight: boolean } @@ -33,53 +31,21 @@ export function randomizeTip() { setTipIndex(Math.floor(Math.random() * TIPS.length)) } -const BOX_WIDTH = 42 -const TITLE = " 🅘 Did you know? " - export function DidYouKnow() { const { theme } = useTheme() - const keybind = useKeybind() const tipParts = createMemo(() => parseTip(TIPS[tipIndex()])) - const dashes = createMemo(() => { - // ╭─ + title + ─...─ + ╮ = BOX_WIDTH - // 1 + 1 + title.length + dashes + 1 = BOX_WIDTH - return Math.max(0, BOX_WIDTH - 2 - TITLE.length - 1) - }) - return ( - - - ╭─ - {TITLE} - {"─".repeat(dashes())}╮ + + + ● Tip{" "} + + + + {(part) => {part.text}} + - - - - - {(part) => {part.text}} - - - - - - - {keybind.print("tips_toggle")} - hide tips - - ) } diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.ts b/packages/opencode/src/cli/cmd/tui/component/tips.ts index ed8ce1474..24e0913e9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.ts +++ b/packages/opencode/src/cli/cmd/tui/component/tips.ts @@ -1,103 +1,103 @@ export const TIPS = [ - "Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files to your prompt.", - "Start a message with {highlight}!{/highlight} to run shell commands directly (e.g., {highlight}!ls -la{/highlight}).", - "Press {highlight}Tab{/highlight} to cycle between Build (full access) and Plan (read-only) agents.", - "Use {highlight}/undo{/highlight} to revert the last message and any file changes made by OpenCode.", - "Use {highlight}/redo{/highlight} to restore previously undone messages and file changes.", - "Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai.", - "Drag and drop images into the terminal to add them as context for your prompts.", - "Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard directly into the prompt.", - "Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor.", - "Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase structure.", - "Run {highlight}/models{/highlight} or {highlight}Ctrl+X M{/highlight} to see and switch between available AI models.", - "Use {highlight}/theme{/highlight} or {highlight}Ctrl+X T{/highlight} to preview and switch between 50+ built-in themes.", - "Press {highlight}Ctrl+X N{/highlight} or {highlight}/new{/highlight} to start a fresh conversation session.", - "Use {highlight}/sessions{/highlight} or {highlight}Ctrl+X L{/highlight} to list and continue previous conversations.", - "Run {highlight}/compact{/highlight} to summarize long sessions when approaching context limits.", - "Press {highlight}Ctrl+X X{/highlight} or {highlight}/export{/highlight} to save the conversation as Markdown.", - "Press {highlight}Ctrl+X Y{/highlight} to copy the assistant's last message to clipboard.", - "Press {highlight}Ctrl+P{/highlight} to see all available actions and commands.", - "Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers.", - "The default leader key is {highlight}Ctrl+X{/highlight}; combine with other keys for quick actions.", - "Press {highlight}F2{/highlight} to quickly switch between recently used models.", - "Press {highlight}Ctrl+X B{/highlight} to show/hide the sidebar panel.", - "Use {highlight}PageUp{/highlight}/{highlight}PageDown{/highlight} to navigate through conversation history.", - "Press {highlight}Ctrl+G{/highlight} or {highlight}Home{/highlight} to jump to the beginning of the conversation.", - "Press {highlight}Ctrl+Alt+G{/highlight} or {highlight}End{/highlight} to jump to the most recent message.", - "Press {highlight}Shift+Enter{/highlight} or {highlight}Ctrl+J{/highlight} to add newlines in your prompt.", - "Press {highlight}Ctrl+C{/highlight} when typing to clear the input field.", - "Press {highlight}Escape{/highlight} to stop the AI mid-response.", - "Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes.", - "Use {highlight}@{/highlight} in prompts to invoke specialized subagents.", - "Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions.", - "Create {highlight}opencode.json{/highlight} in project root for project-specific settings.", - "Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config.", - "Add {highlight}$schema{/highlight} to your config for autocomplete in your editor.", - "Configure {highlight}model{/highlight} in config to set your default model.", - "Override any keybind in config via the {highlight}keybinds{/highlight} section.", - "Set any keybind to {highlight}none{/highlight} to disable it completely.", - "Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section.", - "OpenCode auto-handles OAuth for remote MCP servers requiring auth.", - "Add {highlight}.md{/highlight} files to {highlight}.opencode/command/{/highlight} to define reusable custom prompts.", - "Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input.", - "Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight}).", - "Add {highlight}.md{/highlight} files to {highlight}.opencode/agent/{/highlight} for specialized AI personas.", - "Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools.", - 'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions.', - 'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands.', - 'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing.', - "OpenCode auto-formats files using prettier, gofmt, ruff, and more.", - 'Set {highlight}"formatter": false{/highlight} in config to disable all auto-formatting.', - "Define custom formatter commands with file extensions in config.", - "OpenCode uses LSP servers for intelligent code analysis.", - "Create {highlight}.ts{/highlight} files in {highlight}.opencode/tool/{/highlight} to define new LLM tools.", - "Tool definitions can invoke scripts written in Python, Go, etc.", - "Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugin/{/highlight} for event hooks.", - "Use plugins to send OS notifications when sessions complete.", - "Create a plugin to prevent OpenCode from reading sensitive files.", - "Use {highlight}opencode run{/highlight} for non-interactive scripting.", - "Use {highlight}opencode run --continue{/highlight} to resume the last session.", - "Use {highlight}opencode run -f file.ts{/highlight} to attach files via CLI.", - "Use {highlight}--format json{/highlight} for machine-readable output in scripts.", - "Run {highlight}opencode serve{/highlight} for headless API access to OpenCode.", - "Use {highlight}opencode run --attach{/highlight} to connect to a running server for faster runs.", - "Run {highlight}opencode upgrade{/highlight} to update to the latest version.", - "Run {highlight}opencode auth list{/highlight} to see all configured providers.", - "Run {highlight}opencode agent create{/highlight} for guided agent creation.", - "Use {highlight}/opencode{/highlight} in GitHub issues/PRs to trigger AI actions.", - "Run {highlight}opencode github install{/highlight} to set up the GitHub workflow.", - "Comment {highlight}/opencode fix this{/highlight} on issues to auto-create PRs.", - "Comment {highlight}/oc{/highlight} on PR code lines for targeted code reviews.", - 'Use {highlight}"theme": "system"{/highlight} to match your terminal\'s colors.', - "Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory.", - "Themes support dark/light variants for both modes.", - "Reference ANSI colors 0-255 in custom themes.", - "Use {highlight}{env:VAR_NAME}{/highlight} syntax to reference environment variables in config.", - "Use {highlight}{file:path}{/highlight} to include file contents in config values.", - "Use {highlight}instructions{/highlight} in config to load additional rules files.", - "Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative).", - "Configure {highlight}maxSteps{/highlight} to limit agentic iterations per request.", - 'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools.', - 'Use {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server.', - "Override global tool settings per agent configuration.", - 'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions.', - 'Set {highlight}"share": "disabled"{/highlight} to prevent any session sharing.', - "Run {highlight}/unshare{/highlight} to remove a session from public access.", - "Permission {highlight}doom_loop{/highlight} prevents infinite tool call loops.", - "Permission {highlight}external_directory{/highlight} protects files outside project.", - "Run {highlight}opencode debug config{/highlight} to troubleshoot configuration.", - "Use {highlight}--print-logs{/highlight} flag to see detailed logs in stderr.", - "Press {highlight}Ctrl+X G{/highlight} or {highlight}/timeline{/highlight} to jump to specific messages.", - "Press {highlight}Ctrl+X H{/highlight} to toggle code block visibility in messages.", - "Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info.", - "Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling.", - "Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight}).", - "Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use.", - "Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models.", - "Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing.", - "Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs.", - "Run {highlight}/help{/highlight} or {highlight}Ctrl+X H{/highlight} to show the help dialog.", - "Use {highlight}/details{/highlight} to toggle tool execution details visibility.", - "Use {highlight}/rename{/highlight} to rename the current session.", - "Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell.", + "Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files", + "Start a message with {highlight}!{/highlight} to run shell commands directly (e.g., {highlight}!ls -la{/highlight})", + "Press {highlight}Tab{/highlight} to cycle between Build and Plan agents", + "Use {highlight}/undo{/highlight} to revert the last message and file changes", + "Use {highlight}/redo{/highlight} to restore previously undone messages and file changes", + "Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai", + "Drag and drop images into the terminal to add them as context", + "Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard into the prompt", + "Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor", + "Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase", + "Run {highlight}/models{/highlight} or {highlight}Ctrl+X M{/highlight} to see and switch between available AI models", + "Use {highlight}/theme{/highlight} or {highlight}Ctrl+X T{/highlight} to switch between 50+ built-in themes", + "Press {highlight}Ctrl+X N{/highlight} or {highlight}/new{/highlight} to start a fresh conversation session", + "Use {highlight}/sessions{/highlight} or {highlight}Ctrl+X L{/highlight} to list and continue previous conversations", + "Run {highlight}/compact{/highlight} to summarize long sessions near context limits", + "Press {highlight}Ctrl+X X{/highlight} or {highlight}/export{/highlight} to save the conversation as Markdown", + "Press {highlight}Ctrl+X Y{/highlight} to copy the assistant's last message to clipboard", + "Press {highlight}Ctrl+P{/highlight} to see all available actions and commands", + "Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers", + "The leader key is {highlight}Ctrl+X{/highlight}; combine with other keys for quick actions", + "Press {highlight}F2{/highlight} to quickly switch between recently used models", + "Press {highlight}Ctrl+X B{/highlight} to show/hide the sidebar panel", + "Use {highlight}PageUp{/highlight}/{highlight}PageDown{/highlight} to navigate through conversation history", + "Press {highlight}Ctrl+G{/highlight} or {highlight}Home{/highlight} to jump to the beginning of the conversation", + "Press {highlight}Ctrl+Alt+G{/highlight} or {highlight}End{/highlight} to jump to the most recent message", + "Press {highlight}Shift+Enter{/highlight} or {highlight}Ctrl+J{/highlight} to add newlines in your prompt", + "Press {highlight}Ctrl+C{/highlight} when typing to clear the input field", + "Press {highlight}Escape{/highlight} to stop the AI mid-response", + "Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes", + "Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents", + "Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions", + "Create {highlight}opencode.json{/highlight} in project root for project-specific settings", + "Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config", + "Add {highlight}$schema{/highlight} to your config for autocomplete in your editor", + "Configure {highlight}model{/highlight} in config to set your default model", + "Override any keybind in config via the {highlight}keybinds{/highlight} section", + "Set any keybind to {highlight}none{/highlight} to disable it completely", + "Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section", + "OpenCode auto-handles OAuth for remote MCP servers requiring auth", + "Add {highlight}.md{/highlight} files to {highlight}.opencode/command/{/highlight} to define reusable custom prompts", + "Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input", + "Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight})", + "Add {highlight}.md{/highlight} files to {highlight}.opencode/agent/{/highlight} for specialized AI personas", + "Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools", + 'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions', + 'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands', + 'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing', + "OpenCode auto-formats files using prettier, gofmt, ruff, and more", + 'Set {highlight}"formatter": false{/highlight} in config to disable all auto-formatting', + "Define custom formatter commands with file extensions in config", + "OpenCode uses LSP servers for intelligent code analysis", + "Create {highlight}.ts{/highlight} files in {highlight}.opencode/tool/{/highlight} to define new LLM tools", + "Tool definitions can invoke scripts written in Python, Go, etc", + "Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugin/{/highlight} for event hooks", + "Use plugins to send OS notifications when sessions complete", + "Create a plugin to prevent OpenCode from reading sensitive files", + "Use {highlight}opencode run{/highlight} for non-interactive scripting", + "Use {highlight}opencode run --continue{/highlight} to resume the last session", + "Use {highlight}opencode run -f file.ts{/highlight} to attach files via CLI", + "Use {highlight}--format json{/highlight} for machine-readable output in scripts", + "Run {highlight}opencode serve{/highlight} for headless API access to OpenCode", + "Use {highlight}opencode run --attach{/highlight} to connect to a running server", + "Run {highlight}opencode upgrade{/highlight} to update to the latest version", + "Run {highlight}opencode auth list{/highlight} to see all configured providers", + "Run {highlight}opencode agent create{/highlight} for guided agent creation", + "Use {highlight}/opencode{/highlight} in GitHub issues/PRs to trigger AI actions", + "Run {highlight}opencode github install{/highlight} to set up the GitHub workflow", + "Comment {highlight}/opencode fix this{/highlight} on issues to auto-create PRs", + "Comment {highlight}/oc{/highlight} on PR code lines for targeted code reviews", + 'Use {highlight}"theme": "system"{/highlight} to match your terminal\'s colors', + "Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory", + "Themes support dark/light variants for both modes", + "Reference ANSI colors 0-255 in custom themes", + "Use {highlight}{env:VAR_NAME}{/highlight} syntax to reference environment variables in config", + "Use {highlight}{file:path}{/highlight} to include file contents in config values", + "Use {highlight}instructions{/highlight} in config to load additional rules files", + "Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative)", + "Configure {highlight}maxSteps{/highlight} to limit agentic iterations per request", + 'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools', + 'Use {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server', + "Override global tool settings per agent configuration", + 'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions', + 'Set {highlight}"share": "disabled"{/highlight} to prevent any session sharing', + "Run {highlight}/unshare{/highlight} to remove a session from public access", + "Permission {highlight}doom_loop{/highlight} prevents infinite tool call loops", + "Permission {highlight}external_directory{/highlight} protects files outside project", + "Run {highlight}opencode debug config{/highlight} to troubleshoot configuration", + "Use {highlight}--print-logs{/highlight} flag to see detailed logs in stderr", + "Press {highlight}Ctrl+X G{/highlight} or {highlight}/timeline{/highlight} to jump to specific messages", + "Press {highlight}Ctrl+X H{/highlight} to toggle code block visibility in messages", + "Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info", + "Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling", + "Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight})", + "Run {highlight}docker run -it --rm ghcr.io/sst/opencode{/highlight} for containerized use", + "Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models", + "Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing", + "Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs", + "Run {highlight}/help{/highlight} or {highlight}Ctrl+X H{/highlight} to show the help dialog", + "Use {highlight}/details{/highlight} to toggle tool execution details visibility", + "Use {highlight}/rename{/highlight} to rename the current session", + "Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell", ] diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 2155ab947..e5ce80efc 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -1,6 +1,7 @@ import { Prompt, type PromptRef } from "@tui/component/prompt" import { createMemo, Match, onMount, Show, Switch } from "solid-js" import { useTheme } from "@tui/context/theme" +import { useKeybind } from "@tui/context/keybind" import { Logo } from "../component/logo" import { DidYouKnow, randomizeTip } from "../component/did-you-know" import { Locale } from "@/util/locale" @@ -36,7 +37,6 @@ export function Home() { const isFirstTimeUser = createMemo(() => sync.data.session.length === 0) const tipsHidden = createMemo(() => kv.get("tips_hidden", false)) const showTips = createMemo(() => { - return false // Don't show tips for first-time users if (isFirstTimeUser()) return false return !tipsHidden() @@ -90,6 +90,8 @@ export function Home() { }) const directory = useDirectory() + const keybind = useKeybind() + return ( <> @@ -103,12 +105,22 @@ export function Home() { hint={Hint} /> + + + + + + + - - - - + + + + {keybind.print("tips_toggle")} + Hide tips + + {directory()} From 547a975707c16253edf03c2d6ec30f8e499c83ad Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 12 Jan 2026 09:42:28 -0500 Subject: [PATCH 097/470] tui: redesign tips display on home screen --- .../cli/cmd/tui/component/did-you-know.tsx | 51 ------------------- .../cmd/tui/component/{tips.ts => tips.tsx} | 51 ++++++++++++++++++- .../opencode/src/cli/cmd/tui/context/kv.tsx | 3 ++ .../opencode/src/cli/cmd/tui/routes/home.tsx | 21 ++------ 4 files changed, 57 insertions(+), 69 deletions(-) delete mode 100644 packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx rename packages/opencode/src/cli/cmd/tui/component/{tips.ts => tips.tsx} (86%) diff --git a/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx b/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx deleted file mode 100644 index 10042d938..000000000 --- a/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { createMemo, createSignal, For } from "solid-js" -import { useTheme } from "@tui/context/theme" -import { TIPS } from "./tips" - -type TipPart = { text: string; highlight: boolean } - -function parseTip(tip: string): TipPart[] { - const parts: TipPart[] = [] - const regex = /\{highlight\}(.*?)\{\/highlight\}/g - let lastIndex = 0 - let match - - while ((match = regex.exec(tip)) !== null) { - if (match.index > lastIndex) { - parts.push({ text: tip.slice(lastIndex, match.index), highlight: false }) - } - parts.push({ text: match[1], highlight: true }) - lastIndex = regex.lastIndex - } - - if (lastIndex < tip.length) { - parts.push({ text: tip.slice(lastIndex), highlight: false }) - } - - return parts -} - -const [tipIndex, setTipIndex] = createSignal(Math.floor(Math.random() * TIPS.length)) - -export function randomizeTip() { - setTipIndex(Math.floor(Math.random() * TIPS.length)) -} - -export function DidYouKnow() { - const { theme } = useTheme() - - const tipParts = createMemo(() => parseTip(TIPS[tipIndex()])) - - return ( - - - ● Tip{" "} - - - - {(part) => {part.text}} - - - - ) -} diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.ts b/packages/opencode/src/cli/cmd/tui/component/tips.tsx similarity index 86% rename from packages/opencode/src/cli/cmd/tui/component/tips.ts rename to packages/opencode/src/cli/cmd/tui/component/tips.tsx index 24e0913e9..18d3c2df1 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.ts +++ b/packages/opencode/src/cli/cmd/tui/component/tips.tsx @@ -1,4 +1,51 @@ -export const TIPS = [ +import { createMemo, createSignal, For } from "solid-js" +import { useTheme } from "@tui/context/theme" + +type TipPart = { text: string; highlight: boolean } + +function parse(tip: string): TipPart[] { + const parts: TipPart[] = [] + const regex = /\{highlight\}(.*?)\{\/highlight\}/g + const found = Array.from(tip.matchAll(regex)) + const state = found.reduce( + (acc, match) => { + const start = match.index ?? 0 + if (start > acc.index) { + acc.parts.push({ text: tip.slice(acc.index, start), highlight: false }) + } + acc.parts.push({ text: match[1], highlight: true }) + acc.index = start + match[0].length + return acc + }, + { parts, index: 0 }, + ) + + if (state.index < tip.length) { + parts.push({ text: tip.slice(state.index), highlight: false }) + } + + return parts +} + +export function Tips() { + const theme = useTheme().theme + const parts = parse(TIPS[Math.floor(Math.random() * TIPS.length)]) + + return ( + + + ● Tip{" "} + + + + {(part) => {part.text}} + + + + ) +} + +const TIPS = [ "Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files", "Start a message with {highlight}!{/highlight} to run shell commands directly (e.g., {highlight}!ls -la{/highlight})", "Press {highlight}Tab{/highlight} to cycle between Build and Plan agents", @@ -78,7 +125,7 @@ export const TIPS = [ "Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative)", "Configure {highlight}maxSteps{/highlight} to limit agentic iterations per request", 'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools', - 'Use {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server', + 'Set {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server', "Override global tool settings per agent configuration", 'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions', 'Set {highlight}"share": "disabled"{/highlight} to prevent any session sharing', diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index e0844ba5a..93f469afa 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -25,6 +25,9 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ get ready() { return ready() }, + get store() { + return kvStore + }, signal(name: string, defaultValue: T) { if (kvStore[name] === undefined) setKvStore(name, defaultValue) return [ diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index e5ce80efc..6b4ab3354 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -3,7 +3,7 @@ import { createMemo, Match, onMount, Show, Switch } from "solid-js" import { useTheme } from "@tui/context/theme" import { useKeybind } from "@tui/context/keybind" import { Logo } from "../component/logo" -import { DidYouKnow, randomizeTip } from "../component/did-you-know" +import { Tips } from "../component/tips" import { Locale } from "@/util/locale" import { useSync } from "../context/sync" import { Toast } from "../ui/toast" @@ -77,7 +77,6 @@ export function Home() { let prompt: PromptRef const args = useArgs() onMount(() => { - randomizeTip() if (once) return if (route.initialPrompt) { prompt.set(route.initialPrompt) @@ -105,23 +104,13 @@ export function Home() { hint={Hint} /> - - - - - - + + + +
    - - - - {keybind.print("tips_toggle")} - Hide tips - - - {directory()} From 20399bbdfe7d3f0da96231333b5df91d9b5da599 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 12 Jan 2026 09:53:08 -0500 Subject: [PATCH 098/470] tui: rename kvStore to store for consistency --- packages/opencode/src/cli/cmd/tui/context/kv.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 93f469afa..651c2dbc0 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -8,13 +8,13 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ name: "KV", init: () => { const [ready, setReady] = createSignal(false) - const [kvStore, setKvStore] = createStore>() + const [store, setStore] = createStore>() const file = Bun.file(path.join(Global.Path.state, "kv.json")) file .json() .then((x) => { - setKvStore(x) + setStore(x) }) .catch(() => {}) .finally(() => { @@ -26,10 +26,10 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ return ready() }, get store() { - return kvStore + return store }, signal(name: string, defaultValue: T) { - if (kvStore[name] === undefined) setKvStore(name, defaultValue) + if (store[name] === undefined) setStore(name, defaultValue) return [ function () { return result.get(name) @@ -40,11 +40,11 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ ] as const }, get(key: string, defaultValue?: any) { - return kvStore[key] ?? defaultValue + return store[key] ?? defaultValue }, set(key: string, value: any) { - setKvStore(key, value) - Bun.write(file, JSON.stringify(kvStore, null, 2)) + setStore(key, value) + Bun.write(file, JSON.stringify(store, null, 2)) }, } return result From 71a7ad1a4ebbc860a40c076df408c9be72408103 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 12 Jan 2026 12:45:24 -0500 Subject: [PATCH 099/470] core: fix model selection in title generation to use user's model instead of assistant's --- packages/opencode/src/session/summary.ts | 53 ++---------------------- 1 file changed, 4 insertions(+), 49 deletions(-) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 83519307a..2bd1b0da6 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -74,11 +74,6 @@ export namespace SessionSummary { } await Session.updateMessage(userMsg) - const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant - const small = - (await Provider.getSmallModel(assistantMsg.providerID)) ?? - (await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID)) - const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart if (textPart && !userMsg.summary?.title) { const agent = await Agent.get("title") @@ -86,7 +81,10 @@ export namespace SessionSummary { agent, user: userMsg, tools: {}, - model: agent.model ? await Provider.getModel(agent.model.providerID, agent.model.modelID) : small, + model: agent.model + ? await Provider.getModel(agent.model.providerID, agent.model.modelID) + : ((await Provider.getSmallModel(userMsg.model.providerID)) ?? + (await Provider.getModel(userMsg.model.providerID, userMsg.model.modelID))), small: true, messages: [ { @@ -109,49 +107,6 @@ export namespace SessionSummary { userMsg.summary.title = result await Session.updateMessage(userMsg) } - - if ( - messages.some( - (m) => - m.info.role === "assistant" && m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"), - ) - ) { - if (diffs.length > 0) { - for (const msg of messages) { - for (const part of msg.parts) { - if (part.type === "tool" && part.state.status === "completed") { - part.state.output = "[TOOL OUTPUT PRUNED]" - } - } - } - const summaryAgent = await Agent.get("summary") - const stream = await LLM.stream({ - agent: summaryAgent, - user: userMsg, - tools: {}, - model: summaryAgent.model - ? await Provider.getModel(summaryAgent.model.providerID, summaryAgent.model.modelID) - : small, - small: true, - messages: [ - ...MessageV2.toModelMessage(messages), - { - role: "user" as const, - content: `Summarize the above conversation according to your system prompts.`, - }, - ], - abort: new AbortController().signal, - sessionID: userMsg.sessionID, - system: [], - retries: 3, - }) - const result = await stream.text - if (result) { - userMsg.summary.body = result - } - } - await Session.updateMessage(userMsg) - } } export const diff = fn( From 61d0b3e4dc2841080e35f7898065ac8c06f02a92 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 12:25:01 -0600 Subject: [PATCH 100/470] chore: bump ai sdk packages --- bun.lock | 140 ++++++++++++++++++--------------- package.json | 2 +- packages/opencode/package.json | 36 ++++----- 3 files changed, 94 insertions(+), 84 deletions(-) diff --git a/bun.lock b/bun.lock index 3a8886bf9..ec0fad173 100644 --- a/bun.lock +++ b/bun.lock @@ -256,25 +256,25 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.5.1", - "@ai-sdk/amazon-bedrock": "3.0.57", - "@ai-sdk/anthropic": "2.0.56", - "@ai-sdk/azure": "2.0.82", - "@ai-sdk/cerebras": "1.0.33", - "@ai-sdk/cohere": "2.0.21", - "@ai-sdk/deepinfra": "1.0.30", - "@ai-sdk/gateway": "2.0.23", - "@ai-sdk/google": "2.0.49", - "@ai-sdk/google-vertex": "3.0.81", - "@ai-sdk/groq": "2.0.33", - "@ai-sdk/mistral": "2.0.26", - "@ai-sdk/openai": "2.0.71", - "@ai-sdk/openai-compatible": "1.0.29", - "@ai-sdk/perplexity": "2.0.22", - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.19", - "@ai-sdk/togetherai": "1.0.30", + "@ai-sdk/amazon-bedrock": "3.0.73", + "@ai-sdk/anthropic": "2.0.57", + "@ai-sdk/azure": "2.0.91", + "@ai-sdk/cerebras": "1.0.34", + "@ai-sdk/cohere": "2.0.22", + "@ai-sdk/deepinfra": "1.0.31", + "@ai-sdk/gateway": "2.0.25", + "@ai-sdk/google": "2.0.52", + "@ai-sdk/google-vertex": "3.0.97", + "@ai-sdk/groq": "2.0.34", + "@ai-sdk/mistral": "2.0.27", + "@ai-sdk/openai": "2.0.89", + "@ai-sdk/openai-compatible": "1.0.30", + "@ai-sdk/perplexity": "2.0.23", + "@ai-sdk/provider": "2.0.1", + "@ai-sdk/provider-utils": "3.0.20", + "@ai-sdk/togetherai": "1.0.31", "@ai-sdk/vercel": "1.0.31", - "@ai-sdk/xai": "2.0.42", + "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", @@ -505,7 +505,7 @@ "@types/luxon": "3.7.1", "@types/node": "22.13.9", "@typescript/native-preview": "7.0.0-dev.20251207.1", - "ai": "5.0.97", + "ai": "5.0.119", "diff": "8.0.2", "fuzzysort": "3.1.0", "hono": "4.10.7", @@ -542,43 +542,43 @@ "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.5.1", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.57", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.45", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mOUSLe+RgZzx0rtL1p9QXmSd/08z1EkBR+vQ1ydpd1t5P0Nx2kB8afiukEgM8nuDvmO9eYQlp7VTy1n5ffPs2g=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.73", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="], - "@ai-sdk/azure": ["@ai-sdk/azure@2.0.82", "", { "dependencies": { "@ai-sdk/openai": "2.0.80", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Bpab51ETBB4adZC1xGMYsryL/CB8j1sA+t5aDqhRv3t3WRLTxhaBDcFKtQTIuxiEQTFosz9Q2xQqdfBvQm5jHw=="], + "@ai-sdk/azure": ["@ai-sdk/azure@2.0.91", "", { "dependencies": { "@ai-sdk/openai": "2.0.89", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9tznVSs6LGQNKKxb8pKd7CkBV9yk+a/ENpFicHCj2CmBUKefxzwJ9JbUqrlK3VF6dGZw3LXq0dWxt7/Yekaj1w=="], - "@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2gSSS/7kunIwMdC4td5oWsUAzoLw84ccGpz6wQbxVnrb1iWnrEnKa5tRBduaP6IXpzLWsu8wME3+dQhZy+gT7w=="], + "@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.34", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XOK0dJsAGoPYi/lfR4KFBi8xhvJ46oCpAxUD6FmJAuJ4eh0qlj5zDt+myvzM8gvN7S6K7zHD+mdWlOPKGQT8Vg=="], - "@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZjaZFvJlc5XOPi3QwTLEFZbHIgTJc6YGvxz+8zIMGVZi/hdynR8/f/C1A9x6mhzmBtAqi/dZ2h11oouAQH5z4g=="], + "@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="], - "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XK8oRZFApzo6xnS5C+FhWUUkB2itA5Nfon3pU9dJVM0goViq8GwdleZTBRqhu4DE4KJURo5DGWpJr2hfV54cEg=="], + "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-87qFcYNvDF/89hB//MQjYTb3tlsAfmgeZrZ34RESeBTZpSgs0EzYOMqPMwFTHUNp4wteoifikDJbaS/9Da8cfw=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qmX7afPRszUqG5hryHF3UN8ITPIRSGmDW6VYCmByzjoUkgm3MekzSx2hMV1wr0P+llDeuXb378SjqUfpvWJulg=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.25", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rq+FX55ne7lMiqai7NcvvDZj4HLsr+hg77WayqmySqc6zhw3tIOLxd4Ty6OpwNj0C0bVMi3iCl2zvJIEirh9XA=="], - "@ai-sdk/google": ["@ai-sdk/google@2.0.49", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-efwKk4mOV0SpumUaQskeYABk37FJPmEYwoDJQEjyLRmGSjtHRe9P5Cwof5ffLvaFav2IaJpBGEz98pyTs7oNWA=="], + "@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.81", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/google": "2.0.44", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yrl5Ug0Mqwo9ya45oxczgy2RWgpEA/XQQCSFYP+3NZMQ4yA3Iim1vkOjVCsGaZZ8rjVk395abi1ZMZV0/6rqVA=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.97", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/google": "2.0.52", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-s4tI7Z15i6FlbtCvS4SBRal8wRfkOXJzKxlS6cU4mJW/QfUfoVy4b22836NVNJwDvkG/HkDSfzwm/X8mn46MhA=="], - "@ai-sdk/groq": ["@ai-sdk/groq@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FWGl7xNr88NBveao3y9EcVWYUt9ABPrwLFY7pIutSNgaTf32vgvyhREobaMrLU4Scr5G/2tlNqOPZ5wkYMaZig=="], + "@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="], - "@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.26", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jxDB++4WI1wEx5ONNBI+VbkmYJOYIuS8UQY13/83UGRaiW7oB/WHiH4ETe6KzbKpQPB3XruwTJQjUMsMfKyTXA=="], + "@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gaptHgaXjMw3+eA0Q4FABcsj5nQNP6EpFaGUR+Pj5WJy7Kn6mApl975/x57224MfeJIShNpt8wFKK3tvh5ewKg=="], "@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="], - "@ai-sdk/perplexity": ["@ai-sdk/perplexity@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zwzcnk08R2J3mZcQPn4Ifl4wYGrvANR7jsBB0hCTUSbb+Rx3ybpikSWiGuXQXxdiRc1I5MWXgj70m+bZaLPvHw=="], + "@ai-sdk/perplexity": ["@ai-sdk/perplexity@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-aiaRvnc6mhQZKhTTSXPCjPH8Iqr5D/PfCN1hgVP/3RGTBbJtsd9HemIBSABeSdAKbsMH/PwJxgnqH75HEamcBA=="], - "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + "@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - "@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9bxQbIXnWSN4bNismrza3NvIo+ui/Y3pj3UN6e9vCszCWFCN45RgISi4oDe10RqmzaJ/X8cfO/Tem+K8MT3wGQ=="], + "@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RlYubjStoZQxna4Ng91Vvo8YskvL7lW9zj68IwZfCnaDBSAp1u6Nhc5BR4ZtKnY6PA3XEtu4bATIQl7yiiQ+Lw=="], "@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ggvwAMt/KsbqcdR6ILQrjwrRONLV/8aG6rOLbjcOGvV0Ai+WdZRRKQj5nOeQ06PvwVQtKdkp7S4IinpXIhCiHg=="], - "@ai-sdk/xai": ["@ai-sdk/xai@2.0.42", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wlwO4yRoZ/d+ca29vN8SDzxus7POdnL7GBTyRdSrt6icUF0hooLesauC8qRUC4aLxtqvMEc1YHtJOU7ZnLWbTQ=="], + "@ai-sdk/xai": ["@ai-sdk/xai@2.0.51", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AI3le03qiegkZvn9hpnpDwez49lOvQLj4QUBT8H41SMbrdTYOxn3ktTwrsSu90cNDdzKGMvoH0u2GHju1EdnCg=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -1904,7 +1904,7 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="], + "ai": ["ai@5.0.119", "", { "dependencies": { "@ai-sdk/gateway": "2.0.25", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HUOwhc17fl2SZTJGZyA/99aNu706qKfXaUBCy9vgZiXBwrxg2eTzn2BCz7kmYDsfx6Fg2ACBy2icm41bsDXCTw=="], "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -2206,6 +2206,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], @@ -2420,6 +2422,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -2450,6 +2454,8 @@ "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], @@ -2472,9 +2478,9 @@ "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], - "gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="], + "gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], - "gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], "gel": ["gel@2.2.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="], @@ -2520,9 +2526,9 @@ "globby": ["globby@11.0.4", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.1.1", "ignore": "^5.1.4", "merge2": "^1.3.0", "slash": "^3.0.0" } }, "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg=="], - "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="], + "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], - "google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -2530,7 +2536,7 @@ "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], - "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="], + "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], "h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="], @@ -3418,6 +3424,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], "rou3": ["rou3@0.7.10", "", {}, "sha512-aoFj6f7MJZ5muJ+Of79nrhs9N3oLGqi2VEMe94Zbkjb6Wupha46EuoYgpWSOZlXww3bbd8ojgXTAA2mzimX5Ww=="], @@ -3910,35 +3918,33 @@ "@agentclientprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ipv62vavDCmrV/oE/lXehL9FzwQuZOnnlhPEftWizx464Wb6lvnBTJx8uhmEYruFSzOWTI95Z33ncZ4tA8E6RQ=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="], + "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], - "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.80", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tNHuraF11db+8xJEDBoU9E3vMcpnHFKRhnLQ3DQX2LnEzfPB9DksZ8rE+yVuDN1WRW9cm2OWAhgHFgVKs7ICuw=="], + "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], - "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="], + "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], - "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="], + "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="], + "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], - "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="], + "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], + "@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], - "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="], + "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], "@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], - "@ai-sdk/vercel/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], - - "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="], + "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], "@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], @@ -4206,10 +4212,6 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "ai/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="], - - "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="], - "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -4274,13 +4276,13 @@ "express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "gaxios/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], @@ -4322,11 +4324,11 @@ "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="], + "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], - "opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="], + "opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], - "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="], + "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], @@ -4364,6 +4366,8 @@ "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], @@ -4926,8 +4930,6 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="], - "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], @@ -4944,6 +4946,12 @@ "readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -5126,6 +5134,8 @@ "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], diff --git a/package.json b/package.json index 66d252365..031296b26 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", - "ai": "5.0.97", + "ai": "5.0.119", "hono": "4.10.7", "hono-openapi": "1.1.2", "fuzzysort": "3.1.0", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 55a0ba06d..ecce3feef 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -50,25 +50,25 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.5.1", - "@ai-sdk/amazon-bedrock": "3.0.57", - "@ai-sdk/anthropic": "2.0.56", - "@ai-sdk/azure": "2.0.82", - "@ai-sdk/cerebras": "1.0.33", - "@ai-sdk/cohere": "2.0.21", - "@ai-sdk/deepinfra": "1.0.30", - "@ai-sdk/gateway": "2.0.23", - "@ai-sdk/google": "2.0.49", - "@ai-sdk/google-vertex": "3.0.81", - "@ai-sdk/groq": "2.0.33", - "@ai-sdk/mistral": "2.0.26", - "@ai-sdk/openai": "2.0.71", - "@ai-sdk/openai-compatible": "1.0.29", - "@ai-sdk/perplexity": "2.0.22", - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.19", - "@ai-sdk/togetherai": "1.0.30", + "@ai-sdk/amazon-bedrock": "3.0.73", + "@ai-sdk/anthropic": "2.0.57", + "@ai-sdk/azure": "2.0.91", + "@ai-sdk/cerebras": "1.0.34", + "@ai-sdk/cohere": "2.0.22", + "@ai-sdk/deepinfra": "1.0.31", + "@ai-sdk/gateway": "2.0.25", + "@ai-sdk/google": "2.0.52", + "@ai-sdk/google-vertex": "3.0.97", + "@ai-sdk/groq": "2.0.34", + "@ai-sdk/mistral": "2.0.27", + "@ai-sdk/openai": "2.0.89", + "@ai-sdk/openai-compatible": "1.0.30", + "@ai-sdk/perplexity": "2.0.23", + "@ai-sdk/provider": "2.0.1", + "@ai-sdk/provider-utils": "3.0.20", + "@ai-sdk/togetherai": "1.0.31", "@ai-sdk/vercel": "1.0.31", - "@ai-sdk/xai": "2.0.42", + "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", From 762c58b756561e2730c0d2eb3d8f118ecbe270e3 Mon Sep 17 00:00:00 2001 From: Github Action Date: Mon, 12 Jan 2026 18:26:56 +0000 Subject: [PATCH 101/470] Update Nix flake.lock and hashes --- flake.lock | 6 +++--- nix/hashes.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index b0f4d3f6b..3e4611cf5 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1767966113, - "narHash": "sha256-mSTsvXa4WveSRJexsmCbm9dY17B1fKp7NLpJxllpQw4=", + "lastModified": 1768178648, + "narHash": "sha256-kz/F6mhESPvU1diB7tOM3nLcBfQe7GU7GQCymRlTi/s=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5f02c91314c8ba4afe83b256b023756412218535", + "rev": "3fbab70c6e69c87ea2b6e48aa6629da2aa6a23b0", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index 41583ec35..8de9c2319 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-aIP+P0hSnQ50+lhnKvfRzQ8DlOhLjrrwarckxI3LBmE=" + "nodeModules": "sha256-FbV9MDkPXCSPO0TL3uYvkMmfVTDH9Lyr2r1ZolYdWW0=" } From 2e9c22d9114c289f30397d4888bdf523f8d816b1 Mon Sep 17 00:00:00 2001 From: Shubh Porwal Date: Tue, 13 Jan 2026 00:00:30 +0530 Subject: [PATCH 102/470] docs: fix typo (#8041) --- packages/web/src/content/docs/models.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/models.mdx b/packages/web/src/content/docs/models.mdx index e070f1e2e..b135e7554 100644 --- a/packages/web/src/content/docs/models.mdx +++ b/packages/web/src/content/docs/models.mdx @@ -3,7 +3,7 @@ title: Models description: Configuring an LLM provider and model. --- -OpenCode uses the [AI SDK](https://ai-sdk.dev/) and [Models.dev](https://models.dev) to support for **75+ LLM providers** and it supports running local models. +OpenCode uses the [AI SDK](https://ai-sdk.dev/) and [Models.dev](https://models.dev) to support **75+ LLM providers** and it supports running local models. --- From d527ceeb2be7c00b29f051163762e46ea6d70537 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 12:33:39 -0600 Subject: [PATCH 103/470] test: fix --- packages/opencode/test/session/message-v2.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 3e27ff292..071da270c 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -298,6 +298,7 @@ describe("session.message-v2.toModelMessage", () => { toolCallId: "call-1", toolName: "bash", output: { type: "text", value: "ok" }, + providerOptions: { openai: { tool: "meta" } }, }, ], }, @@ -433,6 +434,7 @@ describe("session.message-v2.toModelMessage", () => { toolCallId: "call-1", toolName: "bash", output: { type: "error-text", value: "nope" }, + providerOptions: { openai: { tool: "meta" } }, }, ], }, From ca1b597b010bdf624fd839de479533cf5e4010df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 12 Jan 2026 19:49:06 +0100 Subject: [PATCH 104/470] fix(nix): filter optional dependencies by target platform (#8033) --- .github/workflows/update-nix-hashes.yml | 99 +++++++++++++++++++++++-- flake.nix | 26 ++++++- nix/hashes.json | 5 +- nix/node-modules.nix | 6 +- nix/scripts/update-hashes.sh | 9 ++- 5 files changed, 131 insertions(+), 14 deletions(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index d2c60b08f..46ea12d18 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -17,7 +17,7 @@ on: - "packages/*/package.json" jobs: - update: + update-linux: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: blacksmith-4vcpu-ubuntu-2404 env: @@ -47,14 +47,14 @@ jobs: nix flake update echo "✅ flake.lock updated successfully" - - name: Update node_modules hash + - name: Update node_modules hash for x86_64-linux run: | set -euo pipefail - echo "🔄 Updating node_modules hash..." + echo "🔄 Updating node_modules hash for x86_64-linux..." nix/scripts/update-hashes.sh - echo "✅ node_modules hash updated successfully" + echo "✅ node_modules hash for x86_64-linux updated successfully" - - name: Commit hash changes + - name: Commit Linux hash changes env: TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} run: | @@ -65,7 +65,7 @@ jobs: summarize() { local status="$1" { - echo "### Nix Hash Update" + echo "### Nix Hash Update (x86_64-linux)" echo "" echo "- ref: ${GITHUB_REF_NAME}" echo "- status: ${status}" @@ -89,7 +89,92 @@ jobs: echo "🔗 Staging files..." git add "${FILES[@]}" echo "💾 Committing changes..." - git commit -m "Update Nix flake.lock and hashes" + git commit -m "Update Nix flake.lock and x86_64-linux hash" + echo "✅ Changes committed" + + BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" + echo "🌳 Pulling latest from branch: $BRANCH" + git pull --rebase origin "$BRANCH" + echo "🚀 Pushing changes to branch: $BRANCH" + git push origin HEAD:"$BRANCH" + echo "✅ Changes pushed successfully" + + summarize "committed $(git rev-parse --short HEAD)" + + update-macos: + needs: update-linux + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: macos-latest + env: + SYSTEM: aarch64-darwin + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + ref: ${{ github.head_ref || github.ref_name }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + + - name: Setup Nix + uses: DeterminateSystems/nix-installer-action@v20 + + - name: Configure git + run: | + git config --global user.email "action@github.com" + git config --global user.name "Github Action" + + - name: Pull latest changes + env: + TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} + run: | + BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" + git pull origin "$BRANCH" + + - name: Update node_modules hash for aarch64-darwin + run: | + set -euo pipefail + echo "🔄 Updating node_modules hash for aarch64-darwin..." + nix/scripts/update-hashes.sh + echo "✅ node_modules hash for aarch64-darwin updated successfully" + + - name: Commit macOS hash changes + env: + TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} + run: | + set -euo pipefail + + echo "🔍 Checking for changes in tracked Nix files..." + + summarize() { + local status="$1" + { + echo "### Nix Hash Update (aarch64-darwin)" + echo "" + echo "- ref: ${GITHUB_REF_NAME}" + echo "- status: ${status}" + } >> "$GITHUB_STEP_SUMMARY" + if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then + echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY" + fi + echo "" >> "$GITHUB_STEP_SUMMARY" + } + + FILES=(nix/hashes.json) + STATUS="$(git status --short -- "${FILES[@]}" || true)" + if [ -z "$STATUS" ]; then + echo "✅ No changes detected. Hash is already up to date." + summarize "no changes" + exit 0 + fi + + echo "📝 Changes detected:" + echo "$STATUS" + echo "🔗 Staging files..." + git add "${FILES[@]}" + echo "💾 Committing changes..." + git commit -m "Update aarch64-darwin hash" echo "✅ Changes committed" BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" diff --git a/flake.nix b/flake.nix index e53053217..4219a7e8e 100644 --- a/flake.nix +++ b/flake.nix @@ -27,11 +27,28 @@ "aarch64-darwin" = "bun-darwin-arm64"; "x86_64-darwin" = "bun-darwin-x64"; }; - defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + + # Parse "bun-{os}-{cpu}" to {os, cpu} + parseBunTarget = + target: + let + parts = lib.splitString "-" target; + in + { + os = builtins.elemAt parts 1; + cpu = builtins.elemAt parts 2; + }; + hashesFile = "${./nix}/hashes.json"; hashesData = if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { }; - nodeModulesHash = hashesData.nodeModules or defaultNodeModules; + # Lookup hash: supports per-system ({system: hash}) or legacy single hash + nodeModulesHashFor = + system: + if builtins.isAttrs hashesData.nodeModules then + hashesData.nodeModules.${system} + else + hashesData.nodeModules; modelsDev = forEachSystem ( system: let @@ -63,8 +80,11 @@ system: let pkgs = pkgsFor system; + bunPlatform = parseBunTarget bunTarget.${system}; mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { - hash = nodeModulesHash; + hash = nodeModulesHashFor system; + bunCpu = bunPlatform.cpu; + bunOs = bunPlatform.os; }; mkOpencode = pkgs.callPackage ./nix/opencode.nix { }; mkDesktop = pkgs.callPackage ./nix/desktop.nix { }; diff --git a/nix/hashes.json b/nix/hashes.json index 8de9c2319..b85e146d7 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,6 @@ { - "nodeModules": "sha256-FbV9MDkPXCSPO0TL3uYvkMmfVTDH9Lyr2r1ZolYdWW0=" + "nodeModules": { + "x86_64-linux": "sha256-8nur5CuUCSV/SzD16hNXVoIlKsiPBXDzCnoITK0IhC4=", + "aarch64-darwin": "sha256-vD1g9dviI2nMBTTPwI87sK01hSZ+cdnmb1V72AdJYq4=" + } } diff --git a/nix/node-modules.nix b/nix/node-modules.nix index be7edd9c7..2a8f0a47c 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -5,6 +5,8 @@ bun, cacert, curl, + bunCpu, + bunOs, }: args: stdenvNoCC.mkDerivation { @@ -29,8 +31,8 @@ stdenvNoCC.mkDerivation { export HOME=$(mktemp -d) export BUN_INSTALL_CACHE_DIR=$(mktemp -d) bun install \ - --cpu="*" \ - --os="*" \ + --cpu="${bunCpu}" \ + --os="${bunOs}" \ --frozen-lockfile \ --ignore-scripts \ --no-progress \ diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index 7bf183c5b..22c556363 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -33,9 +33,16 @@ trap cleanup EXIT write_node_modules_hash() { local value="$1" + local system="${2:-$SYSTEM}" local temp temp=$(mktemp) - jq --arg value "$value" '.nodeModules = $value' "$HASH_FILE" >"$temp" + + if jq -e '.nodeModules | type == "object"' "$HASH_FILE" >/dev/null 2>&1; then + jq --arg system "$system" --arg value "$value" '.nodeModules[$system] = $value' "$HASH_FILE" >"$temp" + else + jq --arg system "$system" --arg value "$value" '.nodeModules = {($system): $value}' "$HASH_FILE" >"$temp" + fi + mv "$temp" "$HASH_FILE" } From 22c68a6992e9764d71c2a3e058c3fd97136febba Mon Sep 17 00:00:00 2001 From: Ravi Kumar <82090231+Raviguntakala@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:22:36 +0530 Subject: [PATCH 105/470] fix(tui): reopen autocomplete after backspace deletes space (#6031) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- .../cmd/tui/component/prompt/autocomplete.tsx | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 1ecfaaf1f..4f8f979d5 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -601,8 +601,31 @@ export function Autocomplete(props: { (store.visible === "/" && value.match(/^\S+\s+\S+\s*$/)) ) { hide() - return } + return + } + + // Check if autocomplete should reopen (e.g., after backspace deleted a space) + const offset = props.input().cursorOffset + if (offset === 0) return + + // Check for "/" at position 0 - reopen slash commands + if (value.startsWith("/") && !value.slice(0, offset).match(/\s/)) { + show("/") + setStore("index", 0) + return + } + + // Check for "@" trigger - find the nearest "@" before cursor with no whitespace between + const text = value.slice(0, offset) + const idx = text.lastIndexOf("@") + if (idx === -1) return + + const between = text.slice(idx) + const before = idx === 0 ? undefined : value[idx - 1] + if ((before === undefined || /\s/.test(before)) && !between.match(/\s/)) { + show("@") + setStore("index", idx) } }, onKeyDown(e: KeyEvent) { From 62702fbd11879bde0c6153346f4ad9f02defa51f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 13:02:29 -0600 Subject: [PATCH 106/470] fix: permissions wildcarding so that for ex: 'ls *' includes ls * AND 'ls' to prevent having to double state commands or use 'ls*' --- packages/opencode/src/util/wildcard.ts | 22 +++++++++++--------- packages/opencode/test/util/wildcard.test.ts | 20 ++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/util/wildcard.ts b/packages/opencode/src/util/wildcard.ts index 9b595a0a9..4a6eba96f 100644 --- a/packages/opencode/src/util/wildcard.ts +++ b/packages/opencode/src/util/wildcard.ts @@ -2,16 +2,18 @@ import { sortBy, pipe } from "remeda" export namespace Wildcard { export function match(str: string, pattern: string) { - const regex = new RegExp( - "^" + - pattern - .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars - .replace(/\*/g, ".*") // * becomes .* - .replace(/\?/g, ".") + // ? becomes . - "$", - "s", // s flag enables multiline matching - ) - return regex.test(str) + let escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars + .replace(/\*/g, ".*") // * becomes .* + .replace(/\?/g, ".") // ? becomes . + + // If pattern ends with " *" (space + wildcard), make the trailing part optional + // This allows "ls *" to match both "ls" and "ls -la" + if (escaped.endsWith(" .*")) { + escaped = escaped.slice(0, -3) + "( .*)?" + } + + return new RegExp("^" + escaped + "$", "s").test(str) } export function all(input: string, patterns: Record) { diff --git a/packages/opencode/test/util/wildcard.test.ts b/packages/opencode/test/util/wildcard.test.ts index f7f1e1545..9cd0e9b94 100644 --- a/packages/opencode/test/util/wildcard.test.ts +++ b/packages/opencode/test/util/wildcard.test.ts @@ -7,6 +7,26 @@ test("match handles glob tokens", () => { expect(Wildcard.match("foo+bar", "foo+bar")).toBe(true) }) +test("match with trailing space+wildcard matches command with or without args", () => { + // "ls *" should match "ls" (no args) and "ls -la" (with args) + expect(Wildcard.match("ls", "ls *")).toBe(true) + expect(Wildcard.match("ls -la", "ls *")).toBe(true) + expect(Wildcard.match("ls foo bar", "ls *")).toBe(true) + + // "ls*" (no space) should NOT match "ls" alone — wait, it should because .* matches empty + // but it WILL match "lstmeval" which is the dangerous case users should avoid + expect(Wildcard.match("ls", "ls*")).toBe(true) + expect(Wildcard.match("lstmeval", "ls*")).toBe(true) + + // "ls *" (with space) should NOT match "lstmeval" + expect(Wildcard.match("lstmeval", "ls *")).toBe(false) + + // multi-word commands + expect(Wildcard.match("git status", "git *")).toBe(true) + expect(Wildcard.match("git", "git *")).toBe(true) + expect(Wildcard.match("git commit -m foo", "git *")).toBe(true) +}) + test("all picks the most specific pattern", () => { const rules = { "*": "deny", From b7b09fdfc2cb83bdd8f4abb99e2c621728c4f11c Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 12 Jan 2026 14:20:31 -0500 Subject: [PATCH 107/470] admin unshare --- packages/enterprise/package.json | 3 ++- packages/enterprise/script/scrap.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 packages/enterprise/script/scrap.ts diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 859c69095..d08aa6578 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -9,7 +9,8 @@ "dev": "vite dev", "build": "vite build", "build:cloudflare": "OPENCODE_DEPLOYMENT_TARGET=cloudflare vite build", - "start": "vite start" + "start": "vite start", + "shell-prod": "sst shell --target Teams --stage production" }, "dependencies": { "@opencode-ai/util": "workspace:*", diff --git a/packages/enterprise/script/scrap.ts b/packages/enterprise/script/scrap.ts new file mode 100644 index 000000000..5e985224d --- /dev/null +++ b/packages/enterprise/script/scrap.ts @@ -0,0 +1,15 @@ +import { Storage } from "../src/core/storage" + +// read share id from args +const args = process.argv.slice(2) +if (args.length !== 1) { + console.error("Usage: bun script/scrap.ts ") + process.exit(1) +} +const shareID = args[0] + +await Storage.remove(["share", shareID]) +const list = await Storage.list({ prefix: ["share_data", shareID] }) +for (const item of list) { + await Storage.remove(item) +} From 1c24dd02a61d93382e5fc5582fb5d029e2cf46c1 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 13:28:13 -0600 Subject: [PATCH 108/470] ci: adjust triage prompt --- .opencode/agent/triage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 539be1549..5d1147a88 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -45,9 +45,9 @@ Desktop app issues: #### zen -**Only** add if the issue mentions "zen" or "opencode zen". Zen is our gateway for coding models. **Do not** add for other gateways or inference providers. +**Only** add if the issue mentions "zen" or "opencode zen" or "opencode black". -If the issue doesn't have "zen" in it then don't add zen label +If the issue doesn't have "zen" or "opencode black" in it then don't add zen label #### docs From cd56845dce07d418bed853cb16ebeea82107b79f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 13:35:07 -0600 Subject: [PATCH 109/470] tui: add hint for variants toggle --- packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 644ac2622..d5e0a0aa2 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1062,7 +1062,10 @@ export function Prompt(props: PromptProps) { - {keybind.print("agent_cycle")} switch agent + {keybind.print("variant_cycle")} variants + + + {keybind.print("agent_cycle")} agents {keybind.print("command_list")} commands From e47f3831375f96acaf69b081deb6b32ab4d403d0 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 13:47:25 -0600 Subject: [PATCH 110/470] core: improve error handling for built-in plugin installation failures --- packages/opencode/src/plugin/index.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index f57b46a35..b0c9eee2c 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -8,6 +8,8 @@ import { BunProc } from "../bun" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" import { CodexAuthPlugin } from "./codex" +import { Session } from "../session" +import { NamedError } from "@opencode-ai/util/error" export namespace Plugin { const log = Log.create({ service: "plugin" }) @@ -54,8 +56,21 @@ export namespace Plugin { const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest" const builtin = BUILTIN.some((x) => x.startsWith(pkg + "@")) plugin = await BunProc.install(pkg, version).catch((err) => { - if (builtin) return "" - throw err + if (!builtin) throw err + + const message = err instanceof Error ? err.message : String(err) + log.error("failed to install builtin plugin", { + pkg, + version, + error: message, + }) + Bus.publish(Session.Event.Error, { + error: new NamedError.Unknown({ + message: `Failed to install built-in plugin ${pkg}@${version}: ${message}`, + }).toObject(), + }) + + return "" }) if (!plugin) continue } From f2504d8eb29e17dca7b1a34d16e2d017d784009e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 12 Jan 2026 15:05:37 -0500 Subject: [PATCH 111/470] security.md --- SECURITY.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..537834e4d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Reporting Security Issues + +We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. + +To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/anomalyco/opencode/security/advisories/new) tab. + +The team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. + +## Escalation + +If you do not receive an acknowledgement of your report within 6 business days, you may send an email to security@anoma.ly From b4f33485a7845183272083134618e8f7a15574e0 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Mon, 12 Jan 2026 17:22:03 -0300 Subject: [PATCH 112/470] fix(desktop): Improve User Message Badges for Big Titles and Small Screens (#8037) --- packages/ui/src/components/session-turn.css | 50 ++++++++++----------- packages/ui/src/components/session-turn.tsx | 5 +-- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index abed69897..1e3cc0b29 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -28,35 +28,37 @@ min-width: 0; gap: 28px; overflow-anchor: none; - } - [data-slot="session-turn-user-badges"] { - display: flex; - flex-shrink: 0; - align-items: center; - gap: 6px; - opacity: 0; - transition: opacity 0.15s ease; - pointer-events: none; + [data-slot="session-turn-user-badges"] { + position: absolute; + right: 0; + display: flex; + gap: 6px; + padding-left: 16px; + background: linear-gradient(to right, transparent, var(--background-stronger) 12px); + opacity: 0; + transition: opacity 0.15s ease; + pointer-events: none; + } - &[data-visible="true"] { + &:hover [data-slot="session-turn-user-badges"] { opacity: 1; pointer-events: auto; } - } - [data-slot="session-turn-badge"] { - display: inline-flex; - align-items: center; - padding: 2px 6px; - border-radius: 4px; - font-family: var(--font-family-mono); - font-size: var(--font-size-x-small); - font-weight: var(--font-weight-medium); - line-height: var(--line-height-normal); - white-space: nowrap; - color: var(--text-base); - background: var(--surface-raised-base); + [data-slot="session-turn-badge"] { + display: inline-flex; + align-items: center; + padding: 2px 6px; + border-radius: 4px; + font-family: var(--font-family-mono); + font-size: var(--font-size-x-small); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-normal); + white-space: nowrap; + color: var(--text-base); + background: var(--surface-raised-base); + } } [data-slot="session-turn-sticky-title"] { @@ -81,8 +83,6 @@ [data-slot="session-turn-message-header"] { display: flex; align-items: center; - justify-content: space-between; - gap: 12px; align-self: stretch; height: 32px; } diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index cca811631..9947578b9 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -374,7 +374,6 @@ export function SessionTurn( diffLimit: diffInit, status: rawStatus(), duration: duration(), - userMessageHovered: false, }) createEffect( @@ -475,8 +474,6 @@ export function SessionTurn( data-slot="session-turn-message-container" class={props.classes?.container} style={{ "--sticky-header-height": `${store.stickyHeaderHeight}px` }} - onMouseEnter={() => setStore("userMessageHovered", true)} - onMouseLeave={() => setStore("userMessageHovered", false)} > @@ -496,7 +493,7 @@ export function SessionTurn(
    -
    +
    {(msg() as UserMessage).agent} From 1954c1255e71ab86283e6a99ac82598268fc308d Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 12 Jan 2026 15:23:12 -0500 Subject: [PATCH 113/470] core: add password authentication and improve server security - Add OPENCODE_PASSWORD flag for basic auth protection - Show security warnings when password is not set - Remove deprecated spawn command - Improve error handling with HTTPException responses --- packages/opencode/src/cli/cmd/run.ts | 10 ++--- packages/opencode/src/cli/cmd/serve.ts | 4 ++ packages/opencode/src/cli/cmd/tui/spawn.ts | 48 ---------------------- packages/opencode/src/cli/cmd/web.ts | 4 ++ packages/opencode/src/flag/flag.ts | 1 + packages/opencode/src/index.ts | 2 - packages/opencode/src/server/server.ts | 9 ++++ 7 files changed, 23 insertions(+), 55 deletions(-) delete mode 100644 packages/opencode/src/cli/cmd/tui/spawn.ts diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index a86b435ec..54248f96f 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -339,13 +339,15 @@ export const RunCommand = cmd({ } await bootstrap(process.cwd(), async () => { - const server = Server.listen({ port: args.port ?? 0, hostname: "127.0.0.1" }) - const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}` }) + const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { + const request = new Request(input, init) + return Server.App().fetch(request) + }) as typeof globalThis.fetch + const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn }) if (args.command) { const exists = await Command.get(args.command) if (!exists) { - server.stop() UI.error(`Command "${args.command}" not found`) process.exit(1) } @@ -370,7 +372,6 @@ export const RunCommand = cmd({ })() if (!sessionID) { - server.stop() UI.error("Session not found") process.exit(1) } @@ -389,7 +390,6 @@ export const RunCommand = cmd({ } await execute(sdk, sessionID) - server.stop() }) }, }) diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 657f9196c..441240609 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,12 +1,16 @@ import { Server } from "../../server/server" import { cmd } from "./cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" +import { Flag } from "../../flag/flag" export const ServeCommand = cmd({ command: "serve", builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { + if (!Flag.OPENCODE_PASSWORD) { + console.log("Warning: OPENCODE_PASSWORD is not set; server is unsecured.") + } const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) console.log(`opencode server listening on http://${server.hostname}:${server.port}`) diff --git a/packages/opencode/src/cli/cmd/tui/spawn.ts b/packages/opencode/src/cli/cmd/tui/spawn.ts deleted file mode 100644 index ef359e6f4..000000000 --- a/packages/opencode/src/cli/cmd/tui/spawn.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { cmd } from "@/cli/cmd/cmd" -import { Instance } from "@/project/instance" -import path from "path" -import { Server } from "@/server/server" -import { upgrade } from "@/cli/upgrade" -import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" - -export const TuiSpawnCommand = cmd({ - command: "spawn [project]", - builder: (yargs) => - withNetworkOptions(yargs).positional("project", { - type: "string", - describe: "path to start opencode in", - }), - handler: async (args) => { - upgrade() - const opts = await resolveNetworkOptions(args) - const server = Server.listen(opts) - const bin = process.execPath - const cmd = [] - let cwd = process.cwd() - if (bin.endsWith("bun")) { - cmd.push( - process.execPath, - "run", - "--conditions", - "browser", - new URL("../../../index.ts", import.meta.url).pathname, - ) - cwd = new URL("../../../../", import.meta.url).pathname - } else cmd.push(process.execPath) - cmd.push("attach", server.url.toString(), "--dir", args.project ? path.resolve(args.project) : process.cwd()) - const proc = Bun.spawn({ - cmd, - cwd, - stdout: "inherit", - stderr: "inherit", - stdin: "inherit", - env: { - ...process.env, - BUN_OPTIONS: "", - }, - }) - await proc.exited - await Instance.disposeAll() - await server.stop(true) - }, -}) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index e2ecc1187..abb347798 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -2,6 +2,7 @@ import { Server } from "../../server/server" import { UI } from "../ui" import { cmd } from "./cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" +import { Flag } from "../../flag/flag" import open from "open" import { networkInterfaces } from "os" @@ -32,6 +33,9 @@ export const WebCommand = cmd({ builder: (yargs) => withNetworkOptions(yargs), describe: "start opencode server and open web interface", handler: async (args) => { + if (!Flag.OPENCODE_PASSWORD) { + UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_PASSWORD is not set; server is unsecured.") + } const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) UI.empty() diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 50e3cd79e..77260a84c 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -20,6 +20,7 @@ export namespace Flag { OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS") export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli" + export const OPENCODE_PASSWORD = process.env["OPENCODE_PASSWORD"] // Experimental export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL") diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 6099443e7..3de7735bd 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -21,7 +21,6 @@ import { ExportCommand } from "./cli/cmd/export" import { ImportCommand } from "./cli/cmd/import" import { AttachCommand } from "./cli/cmd/tui/attach" import { TuiThreadCommand } from "./cli/cmd/tui/thread" -import { TuiSpawnCommand } from "./cli/cmd/tui/spawn" import { AcpCommand } from "./cli/cmd/acp" import { EOL } from "os" import { WebCommand } from "./cli/cmd/web" @@ -81,7 +80,6 @@ const cli = yargs(hideBin(process.argv)) .command(AcpCommand) .command(McpCommand) .command(TuiThreadCommand) - .command(TuiSpawnCommand) .command(AttachCommand) .command(RunCommand) .command(GenerateCommand) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 32d7a1795..05024acde 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -7,6 +7,7 @@ import { Hono } from "hono" import { cors } from "hono/cors" import { stream, streamSSE } from "hono/streaming" import { proxy } from "hono/proxy" +import { basicAuth } from "hono/basic-auth" import { Session } from "../session" import z from "zod" import { Provider } from "../provider/provider" @@ -25,6 +26,7 @@ import { Project } from "../project/project" import { Vcs } from "../project/vcs" import { Agent } from "../agent/agent" import { Auth } from "../auth" +import { Flag } from "../flag/flag" import { Command } from "../command" import { ProviderAuth } from "../provider/auth" import { Global } from "../global" @@ -45,6 +47,7 @@ import { Snapshot } from "@/snapshot" import { SessionSummary } from "@/session/summary" import { SessionStatus } from "@/session/status" import { upgradeWebSocket, websocket } from "hono/bun" +import { HTTPException } from "hono/http-exception" import { errors } from "./error" import { Pty } from "@/pty" import { PermissionNext } from "@/permission/next" @@ -80,6 +83,7 @@ export namespace Server { log.error("failed", { error: err, }) + if (err instanceof HTTPException) return err.getResponse() if (err instanceof NamedError) { let status: ContentfulStatusCode if (err instanceof Storage.NotFoundError) status = 404 @@ -93,6 +97,11 @@ export namespace Server { status: 500, }) }) + .use((c, next) => { + const password = Flag.OPENCODE_PASSWORD + if (!password) return next() + return basicAuth({ username: "opencode", password })(c, next) + }) .use(async (c, next) => { const skipLogging = c.req.path === "/log" if (!skipLogging) { From 7ca767de55c50f47bbace107619d79586c1c53d7 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 12 Jan 2026 15:43:48 -0500 Subject: [PATCH 114/470] core: fix HTTP exception handling order to prevent NamedError from masking HTTP exceptions docs: add OPENCODE_PASSWORD environment variable documentation for basic auth --- packages/opencode/src/server/server.ts | 2 +- packages/web/src/content/docs/cli.mdx | 45 ++++++++++++------------ packages/web/src/content/docs/server.mdx | 10 ++++++ 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 05024acde..e79c68558 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -83,7 +83,6 @@ export namespace Server { log.error("failed", { error: err, }) - if (err instanceof HTTPException) return err.getResponse() if (err instanceof NamedError) { let status: ContentfulStatusCode if (err instanceof Storage.NotFoundError) status = 404 @@ -92,6 +91,7 @@ export namespace Server { else status = 500 return c.json(err.toObject(), { status }) } + if (err instanceof HTTPException) return err.getResponse() const message = err instanceof Error && err.stack ? err.stack : err.toString() return c.json(new NamedError.Unknown({ message }).toObject(), { status: 500, diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 357c04ad2..11a1d60d1 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -358,7 +358,7 @@ Start a headless OpenCode server for API access. Check out the [server docs](/do opencode serve ``` -This starts an HTTP server that provides API access to opencode functionality without the TUI interface. +This starts an HTTP server that provides API access to opencode functionality without the TUI interface. Set `OPENCODE_PASSWORD` to enable HTTP basic auth (username `opencode`). #### Flags @@ -454,7 +454,7 @@ Start a headless OpenCode server with a web interface. opencode web ``` -This starts an HTTP server and opens a web browser to access OpenCode through a web interface. +This starts an HTTP server and opens a web browser to access OpenCode through a web interface. Set `OPENCODE_PASSWORD` to enable HTTP basic auth (username `opencode`). #### Flags @@ -551,26 +551,27 @@ The opencode CLI takes the following global flags. OpenCode can be configured using environment variables. -| Variable | Type | Description | -| ------------------------------------- | ------- | ------------------------------------------------ | -| `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions | -| `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows | -| `OPENCODE_CONFIG` | string | Path to config file | -| `OPENCODE_CONFIG_DIR` | string | Path to config directory | -| `OPENCODE_CONFIG_CONTENT` | string | Inline json config content | -| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks | -| `OPENCODE_DISABLE_PRUNE` | boolean | Disable pruning of old data | -| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Disable automatic terminal title updates | -| `OPENCODE_PERMISSION` | string | Inlined json permissions config | -| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Disable default plugins | -| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Disable automatic LSP server downloads | -| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Enable experimental models | -| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Disable automatic context compaction | -| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Disable reading from `.claude` (prompt + skills) | -| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Disable reading `~/.claude/CLAUDE.md` | -| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Disable loading `.claude/skills` | -| `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) | -| `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools | +| Variable | Type | Description | +| ------------------------------------- | ------- | ----------------------------------------------------- | +| `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions | +| `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows | +| `OPENCODE_CONFIG` | string | Path to config file | +| `OPENCODE_CONFIG_DIR` | string | Path to config directory | +| `OPENCODE_CONFIG_CONTENT` | string | Inline json config content | +| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks | +| `OPENCODE_DISABLE_PRUNE` | boolean | Disable pruning of old data | +| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Disable automatic terminal title updates | +| `OPENCODE_PERMISSION` | string | Inlined json permissions config | +| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Disable default plugins | +| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Disable automatic LSP server downloads | +| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Enable experimental models | +| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Disable automatic context compaction | +| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Disable reading from `.claude` (prompt + skills) | +| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Disable reading `~/.claude/CLAUDE.md` | +| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Disable loading `.claude/skills` | +| `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) | +| `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools | +| `OPENCODE_PASSWORD` | string | Enable basic auth for `serve`/`web` (user `opencode`) | --- diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx index a61d7bae1..a123acb15 100644 --- a/packages/web/src/content/docs/server.mdx +++ b/packages/web/src/content/docs/server.mdx @@ -33,6 +33,16 @@ opencode serve --cors http://localhost:5173 --cors https://app.example.com --- +### Authentication + +Set `OPENCODE_PASSWORD` to protect the server with HTTP basic auth. The username is always `opencode`, and the password is the value of `OPENCODE_PASSWORD`. This applies to both `opencode serve` and `opencode web`. + +```bash +OPENCODE_PASSWORD=your-password opencode serve +``` + +--- + ### How it works When you run `opencode` it starts a TUI and a server. Where the TUI is the From c47438068490ab2deb65380c25aa056afd23fdd8 Mon Sep 17 00:00:00 2001 From: Kenny Date: Mon, 12 Jan 2026 15:45:17 -0500 Subject: [PATCH 115/470] docs: fix permission rule ordering in examples (#7010) --- packages/web/src/content/docs/skills.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/content/docs/skills.mdx b/packages/web/src/content/docs/skills.mdx index 09519e2be..54c2c9d06 100644 --- a/packages/web/src/content/docs/skills.mdx +++ b/packages/web/src/content/docs/skills.mdx @@ -128,10 +128,10 @@ Control which skills agents can access using pattern-based permissions in `openc { "permission": { "skill": { + "*": "allow", "pr-review": "allow", "internal-*": "deny", - "experimental-*": "ask", - "*": "allow" + "experimental-*": "ask" } } } From 983f8ffecaa5aae9ecb4f22593c3ed91fc00f5bf Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Mon, 12 Jan 2026 15:57:49 -0500 Subject: [PATCH 116/470] fix(tui): prevent layout jump when toggling tips visibility (#8068) --- packages/opencode/src/cli/cmd/tui/routes/home.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 6b4ab3354..59923c69d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -94,6 +94,7 @@ export function Home() { return ( <> + - - + + - - + + From f0912ee838e72b4bedd567d0110fa840820ddf18 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 12 Jan 2026 15:59:17 -0500 Subject: [PATCH 117/470] core: rename OPENCODE_PASSWORD to OPENCODE_SERVER_PASSWORD for clearer authentication configuration --- packages/opencode/src/cli/cmd/serve.ts | 4 +- packages/opencode/src/cli/cmd/web.ts | 4 +- packages/opencode/src/flag/flag.ts | 3 +- packages/opencode/src/server/server.ts | 5 ++- packages/web/src/content/docs/cli.mdx | 47 ++++++++++++------------ packages/web/src/content/docs/server.mdx | 4 +- 6 files changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 441240609..bee2c8f71 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -8,8 +8,8 @@ export const ServeCommand = cmd({ builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { - if (!Flag.OPENCODE_PASSWORD) { - console.log("Warning: OPENCODE_PASSWORD is not set; server is unsecured.") + if (!Flag.OPENCODE_SERVER_PASSWORD) { + console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index abb347798..2c207ecc2 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -33,8 +33,8 @@ export const WebCommand = cmd({ builder: (yargs) => withNetworkOptions(yargs), describe: "start opencode server and open web interface", handler: async (args) => { - if (!Flag.OPENCODE_PASSWORD) { - UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_PASSWORD is not set; server is unsecured.") + if (!Flag.OPENCODE_SERVER_PASSWORD) { + UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 77260a84c..ad6052dec 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -20,7 +20,8 @@ export namespace Flag { OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS") export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli" - export const OPENCODE_PASSWORD = process.env["OPENCODE_PASSWORD"] + export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"] + export const OPENCODE_SERVER_USERNAME = process.env["OPENCODE_SERVER_USERNAME"] // Experimental export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL") diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index e79c68558..52457515b 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -98,9 +98,10 @@ export namespace Server { }) }) .use((c, next) => { - const password = Flag.OPENCODE_PASSWORD + const password = Flag.OPENCODE_SERVER_PASSWORD if (!password) return next() - return basicAuth({ username: "opencode", password })(c, next) + const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" + return basicAuth({ username, password })(c, next) }) .use(async (c, next) => { const skipLogging = c.req.path === "/log" diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 11a1d60d1..d1d11ed70 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -358,7 +358,7 @@ Start a headless OpenCode server for API access. Check out the [server docs](/do opencode serve ``` -This starts an HTTP server that provides API access to opencode functionality without the TUI interface. Set `OPENCODE_PASSWORD` to enable HTTP basic auth (username `opencode`). +This starts an HTTP server that provides API access to opencode functionality without the TUI interface. Set `OPENCODE_SERVER_PASSWORD` to enable HTTP basic auth (username defaults to `opencode`). #### Flags @@ -454,7 +454,7 @@ Start a headless OpenCode server with a web interface. opencode web ``` -This starts an HTTP server and opens a web browser to access OpenCode through a web interface. Set `OPENCODE_PASSWORD` to enable HTTP basic auth (username `opencode`). +This starts an HTTP server and opens a web browser to access OpenCode through a web interface. Set `OPENCODE_SERVER_PASSWORD` to enable HTTP basic auth (username defaults to `opencode`). #### Flags @@ -551,27 +551,28 @@ The opencode CLI takes the following global flags. OpenCode can be configured using environment variables. -| Variable | Type | Description | -| ------------------------------------- | ------- | ----------------------------------------------------- | -| `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions | -| `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows | -| `OPENCODE_CONFIG` | string | Path to config file | -| `OPENCODE_CONFIG_DIR` | string | Path to config directory | -| `OPENCODE_CONFIG_CONTENT` | string | Inline json config content | -| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks | -| `OPENCODE_DISABLE_PRUNE` | boolean | Disable pruning of old data | -| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Disable automatic terminal title updates | -| `OPENCODE_PERMISSION` | string | Inlined json permissions config | -| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Disable default plugins | -| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Disable automatic LSP server downloads | -| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Enable experimental models | -| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Disable automatic context compaction | -| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Disable reading from `.claude` (prompt + skills) | -| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Disable reading `~/.claude/CLAUDE.md` | -| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Disable loading `.claude/skills` | -| `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) | -| `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools | -| `OPENCODE_PASSWORD` | string | Enable basic auth for `serve`/`web` (user `opencode`) | +| Variable | Type | Description | +| ------------------------------------- | ------- | ------------------------------------------------- | +| `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions | +| `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows | +| `OPENCODE_CONFIG` | string | Path to config file | +| `OPENCODE_CONFIG_DIR` | string | Path to config directory | +| `OPENCODE_CONFIG_CONTENT` | string | Inline json config content | +| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks | +| `OPENCODE_DISABLE_PRUNE` | boolean | Disable pruning of old data | +| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Disable automatic terminal title updates | +| `OPENCODE_PERMISSION` | string | Inlined json permissions config | +| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Disable default plugins | +| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Disable automatic LSP server downloads | +| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Enable experimental models | +| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Disable automatic context compaction | +| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Disable reading from `.claude` (prompt + skills) | +| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Disable reading `~/.claude/CLAUDE.md` | +| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Disable loading `.claude/skills` | +| `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) | +| `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools | +| `OPENCODE_SERVER_PASSWORD` | string | Enable basic auth for `serve`/`web` | +| `OPENCODE_SERVER_USERNAME` | string | Override basic auth username (default `opencode`) | --- diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx index a123acb15..7229e09b2 100644 --- a/packages/web/src/content/docs/server.mdx +++ b/packages/web/src/content/docs/server.mdx @@ -35,10 +35,10 @@ opencode serve --cors http://localhost:5173 --cors https://app.example.com ### Authentication -Set `OPENCODE_PASSWORD` to protect the server with HTTP basic auth. The username is always `opencode`, and the password is the value of `OPENCODE_PASSWORD`. This applies to both `opencode serve` and `opencode web`. +Set `OPENCODE_SERVER_PASSWORD` to protect the server with HTTP basic auth. The username defaults to `opencode`, or set `OPENCODE_SERVER_USERNAME` to override it. This applies to both `opencode serve` and `opencode web`. ```bash -OPENCODE_PASSWORD=your-password opencode serve +OPENCODE_SERVER_PASSWORD=your-password opencode serve ``` --- From db7243c3647f96c80e18f07290926046bef21d8d Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 12 Jan 2026 21:03:52 +0000 Subject: [PATCH 118/470] release: v1.1.15 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index ec0fad173..c8ced856b 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -99,7 +99,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -126,7 +126,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -150,7 +150,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -174,7 +174,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -203,7 +203,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -232,7 +232,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -248,7 +248,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.14", + "version": "1.1.15", "bin": { "opencode": "./bin/opencode", }, @@ -351,7 +351,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -371,7 +371,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.14", + "version": "1.1.15", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -382,7 +382,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -395,7 +395,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -434,7 +434,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "zod": "catalog:", }, @@ -445,7 +445,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 4a88f1940..771d9322d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.14", + "version": "1.1.15", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index efbc2486b..c44cfb5e5 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.14", + "version": "1.1.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 0e3e6137b..502537ec4 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.1.14", + "version": "1.1.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index b18bbafb1..5bf3f0975 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.14", + "version": "1.1.15", "$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 a09e1cec2..b4d371afd 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.14", + "version": "1.1.15", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index b65eaf7ce..d8c3fc92a 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.14", + "version": "1.1.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index d08aa6578..d8fc98c61 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.14", + "version": "1.1.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 07dfd061f..51bb13a3e 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.14" +version = "1.1.15" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.14/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 535364a99..8c85fe0dd 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.14", + "version": "1.1.15", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ecce3feef..459e6f657 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.14", + "version": "1.1.15", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 3931f5b7c..7a7c8ece1 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.1.14", + "version": "1.1.15", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 6e8867611..e8777e39a 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.1.14", + "version": "1.1.15", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 82f7aef86..c73dc0b8a 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.14", + "version": "1.1.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index fb7f48953..bc37a826e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.14", + "version": "1.1.15", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 4a9a8d289..3be2c9e71 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.14", + "version": "1.1.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index bc136e5da..5eeadf227 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.1.14", + "version": "1.1.15", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 5ab0fd7b7..2a953baf8 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.1.14", + "version": "1.1.15", "publisher": "sst-dev", "repository": { "type": "git", From 735f3d17bc836e4b0905d1094794699c45a99804 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:21:01 -0800 Subject: [PATCH 119/470] fix: ensure plurals are properly handled (#8070) --- packages/opencode/src/config/config.ts | 43 +++--- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/test/config/config.test.ts | 141 +++++++++++++++++++ packages/opencode/test/tool/registry.test.ts | 76 ++++++++++ 4 files changed, 237 insertions(+), 25 deletions(-) create mode 100644 packages/opencode/test/tool/registry.test.ts diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ead3a0149..127406d1d 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -209,6 +209,19 @@ export namespace Config { await BunProc.run(["install"], { cwd: dir }).catch(() => {}) } + function rel(item: string, patterns: string[]) { + for (const pattern of patterns) { + const index = item.indexOf(pattern) + if (index === -1) continue + return item.slice(index + pattern.length) + } + } + + function trim(file: string) { + const ext = path.extname(file) + return ext.length ? file.slice(0, -ext.length) : file + } + const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md") async function loadCommand(dir: string) { const result: Record = {} @@ -221,16 +234,9 @@ export namespace Config { const md = await ConfigMarkdown.parse(item) if (!md.data) continue - const name = (() => { - const patterns = ["/.opencode/command/", "/command/"] - const pattern = patterns.find((p) => item.includes(p)) - - if (pattern) { - const index = item.indexOf(pattern) - return item.slice(index + pattern.length, -3) - } - return path.basename(item, ".md") - })() + const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"] + const file = rel(item, patterns) ?? path.basename(item) + const name = trim(file) const config = { name, @@ -260,20 +266,9 @@ export namespace Config { const md = await ConfigMarkdown.parse(item) if (!md.data) continue - // Extract relative path from agent folder for nested agents - let agentName = path.basename(item, ".md") - const agentFolderPath = item.includes("/.opencode/agent/") - ? item.split("/.opencode/agent/")[1] - : item.includes("/agent/") - ? item.split("/agent/")[1] - : agentName + ".md" - - // If agent is in a subfolder, include folder path in name - if (agentFolderPath.includes("/")) { - const relativePath = agentFolderPath.replace(".md", "") - const pathParts = relativePath.split("/") - agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1] - } + const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"] + const file = rel(item, patterns) ?? path.basename(item) + const agentName = trim(file) const config = { name: agentName, diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index eb76681de..82bf7f563 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -31,7 +31,7 @@ export namespace ToolRegistry { export const state = Instance.state(async () => { const custom = [] as Tool.Info[] - const glob = new Bun.Glob("tool/*.{js,ts}") + const glob = new Bun.Glob("{tool,tools}/*.{js,ts}") for (const dir of await Config.directories()) { for await (const match of glob.scan({ diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 087eb0c62..86cadca5d 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -334,6 +334,147 @@ Test agent prompt`, }) }) +test("loads agents from .opencode/agents (plural)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const opencodeDir = path.join(dir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + const agentsDir = path.join(opencodeDir, "agents") + await fs.mkdir(path.join(agentsDir, "nested"), { recursive: true }) + + await Bun.write( + path.join(agentsDir, "helper.md"), + `--- +model: test/model +mode: subagent +--- +Helper agent prompt`, + ) + + await Bun.write( + path.join(agentsDir, "nested", "child.md"), + `--- +model: test/model +mode: subagent +--- +Nested agent prompt`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + + expect(config.agent?.["helper"]).toMatchObject({ + name: "helper", + model: "test/model", + mode: "subagent", + prompt: "Helper agent prompt", + }) + + expect(config.agent?.["nested/child"]).toMatchObject({ + name: "nested/child", + model: "test/model", + mode: "subagent", + prompt: "Nested agent prompt", + }) + }, + }) +}) + +test("loads commands from .opencode/command (singular)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const opencodeDir = path.join(dir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + const commandDir = path.join(opencodeDir, "command") + await fs.mkdir(path.join(commandDir, "nested"), { recursive: true }) + + await Bun.write( + path.join(commandDir, "hello.md"), + `--- +description: Test command +--- +Hello from singular command`, + ) + + await Bun.write( + path.join(commandDir, "nested", "child.md"), + `--- +description: Nested command +--- +Nested command template`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + + expect(config.command?.["hello"]).toEqual({ + description: "Test command", + template: "Hello from singular command", + }) + + expect(config.command?.["nested/child"]).toEqual({ + description: "Nested command", + template: "Nested command template", + }) + }, + }) +}) + +test("loads commands from .opencode/commands (plural)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const opencodeDir = path.join(dir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + const commandsDir = path.join(opencodeDir, "commands") + await fs.mkdir(path.join(commandsDir, "nested"), { recursive: true }) + + await Bun.write( + path.join(commandsDir, "hello.md"), + `--- +description: Test command +--- +Hello from plural commands`, + ) + + await Bun.write( + path.join(commandsDir, "nested", "child.md"), + `--- +description: Nested command +--- +Nested command template`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + + expect(config.command?.["hello"]).toEqual({ + description: "Test command", + template: "Hello from plural commands", + }) + + expect(config.command?.["nested/child"]).toEqual({ + description: "Nested command", + template: "Nested command template", + }) + }, + }) +}) + test("updates config and writes to file", async () => { await using tmp = await tmpdir() await Instance.provide({ diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts new file mode 100644 index 000000000..aea8b7088 --- /dev/null +++ b/packages/opencode/test/tool/registry.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import fs from "fs/promises" +import { tmpdir } from "../fixture/fixture" +import { Instance } from "../../src/project/instance" +import { ToolRegistry } from "../../src/tool/registry" + +describe("tool.registry", () => { + test("loads tools from .opencode/tool (singular)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const opencodeDir = path.join(dir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + const toolDir = path.join(opencodeDir, "tool") + await fs.mkdir(toolDir, { recursive: true }) + + await Bun.write( + path.join(toolDir, "hello.ts"), + [ + "export default {", + " description: 'hello tool',", + " args: {},", + " execute: async () => {", + " return 'hello world'", + " },", + "}", + "", + ].join("\n"), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const ids = await ToolRegistry.ids() + expect(ids).toContain("hello") + }, + }) + }) + + test("loads tools from .opencode/tools (plural)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const opencodeDir = path.join(dir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + const toolsDir = path.join(opencodeDir, "tools") + await fs.mkdir(toolsDir, { recursive: true }) + + await Bun.write( + path.join(toolsDir, "hello.ts"), + [ + "export default {", + " description: 'hello tool',", + " args: {},", + " execute: async () => {", + " return 'hello world'", + " },", + "}", + "", + ].join("\n"), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const ids = await ToolRegistry.ids() + expect(ids).toContain("hello") + }, + }) + }) +}) From 9c69c1de9fb8b05f212a923d7c1c4017590aef70 Mon Sep 17 00:00:00 2001 From: Andrew Pashynnyk <30318777+kynnyhsap@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:21:38 +0200 Subject: [PATCH 120/470] fix(formatter): use biome `check` instead of `format` to include import sorting (#8057) Co-authored-by: kynnyhsap --- packages/opencode/src/format/formatter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 9e3db92df..667a954c0 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -96,7 +96,7 @@ export const oxfmt: Info = { export const biome: Info = { name: "biome", - command: [BunProc.which(), "x", "@biomejs/biome", "format", "--write", "$FILE"], + command: [BunProc.which(), "x", "@biomejs/biome", "check", "--write", "$FILE"], environment: { BUN_BE_BUN: "1", }, From 08d4d6d4af2f94d2e527fda9deebeb6bbd1e25b9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 12 Jan 2026 21:22:21 +0000 Subject: [PATCH 121/470] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 7a7c8ece1..8fc6be7ac 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e8777e39a..e29be370c 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From b3a1360ad9d29811e672ba714dd006ee732d5a7b Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:26:58 +0100 Subject: [PATCH 122/470] feat(dialog-select-server): add icon button for server removal (#8053) --- .../src/components/dialog-select-server.tsx | 44 +++++++++++++------ packages/ui/src/hooks/use-filtered-list.tsx | 15 +++++-- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 7e2bcc181..3e4ee8883 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -5,6 +5,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { TextField } from "@opencode-ai/ui/text-field" import { Button } from "@opencode-ai/ui/button" +import { IconButton } from "@opencode-ai/ui/icon-button" import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server" import { usePlatform } from "@/context/platform" import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" @@ -116,6 +117,10 @@ export function DialogSelectServer() { select(value, true) } + async function handleRemove(url: string) { + server.remove(url) + } + return (
    @@ -130,20 +135,33 @@ export function DialogSelectServer() { }} > {(i) => ( -
    +
    - {serverDisplayName(i)} - {store.status[i]?.version} + class="flex items-center gap-2 min-w-0 flex-1" + classList={{ "opacity-50": store.status[i]?.healthy === false }} + > +
    + {serverDisplayName(i)} + {store.status[i]?.version} +
    + + { + e.stopPropagation() + handleRemove(i) + }} + /> +
    )} diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx index 1b3be4b4c..11bc35548 100644 --- a/packages/ui/src/hooks/use-filtered-list.tsx +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -22,10 +22,17 @@ export function useFilteredList(props: FilteredListProps) { const empty: Group[] = [] const [grouped, { refetch }] = createResource( - () => ({ - filter: store.filter, - items: typeof props.items === "function" ? undefined : props.items, - }), + () => { + // When items is a function (not async filter function), call it to track changes + const itemsValue = typeof props.items === "function" + ? (props.items as () => T[])() // Call synchronous function to track it + : props.items + + return { + filter: store.filter, + items: itemsValue, + } + }, async ({ filter, items }) => { const needle = filter?.toLowerCase() const all = (items ?? (await (props.items as (filter: string) => T[] | Promise)(needle))) || [] From 64f0205f97dee21a00a654013454055fedd1a824 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 12 Jan 2026 21:27:36 +0000 Subject: [PATCH 123/470] chore: generate --- packages/app/src/components/dialog-select-server.tsx | 2 +- packages/ui/src/hooks/use-filtered-list.tsx | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 3e4ee8883..90f372128 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -118,7 +118,7 @@ export function DialogSelectServer() { } async function handleRemove(url: string) { - server.remove(url) + server.remove(url) } return ( diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx index 11bc35548..b6bd7d5c6 100644 --- a/packages/ui/src/hooks/use-filtered-list.tsx +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -24,10 +24,11 @@ export function useFilteredList(props: FilteredListProps) { const [grouped, { refetch }] = createResource( () => { // When items is a function (not async filter function), call it to track changes - const itemsValue = typeof props.items === "function" - ? (props.items as () => T[])() // Call synchronous function to track it - : props.items - + const itemsValue = + typeof props.items === "function" + ? (props.items as () => T[])() // Call synchronous function to track it + : props.items + return { filter: store.filter, items: itemsValue, From 0be37cc2c60424940388e307b843beea089c9e4f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 15:32:43 -0600 Subject: [PATCH 124/470] tweak: make the .env reads ask user for permission instead of blocking --- packages/opencode/src/agent/agent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 176955838..fffc20426 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -55,8 +55,8 @@ export namespace Agent { // mirrors github.com/github/gitignore Node.gitignore pattern for .env files read: { "*": "allow", - "*.env": "deny", - "*.env.*": "deny", + "*.env": "ask", + "*.env.*": "ask", "*.env.example": "allow", }, }) From 5c4345da4f896c40089a7254eaaedf718739202c Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 15:49:31 -0600 Subject: [PATCH 125/470] test: fix read test --- packages/opencode/test/tool/read.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 1a08b15b4..04ffc80ea 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -124,7 +124,7 @@ describe("tool.read external_directory permission", () => { }) }) -describe("tool.read env file blocking", () => { +describe("tool.read env file permissions", () => { const cases: [string, boolean][] = [ [".env", true], [".env.local", true], @@ -136,7 +136,7 @@ describe("tool.read env file blocking", () => { ] describe.each(["build", "plan"])("agent=%s", (agentName) => { - test.each(cases)("%s blocked=%s", async (filename, blocked) => { + test.each(cases)("%s asks=%s", async (filename, shouldAsk) => { await using tmp = await tmpdir({ init: (dir) => Bun.write(path.join(dir, filename), "content"), }) @@ -144,11 +144,15 @@ describe("tool.read env file blocking", () => { directory: tmp.path, fn: async () => { const agent = await Agent.get(agentName) + let askedForEnv = false const ctxWithPermissions = { ...ctx, ask: async (req: Omit) => { for (const pattern of req.patterns) { const rule = PermissionNext.evaluate(req.permission, pattern, agent.permission) + if (rule.action === "ask" && req.permission === "read") { + askedForEnv = true + } if (rule.action === "deny") { throw new PermissionNext.DeniedError(agent.permission) } @@ -156,12 +160,8 @@ describe("tool.read env file blocking", () => { }, } const read = await ReadTool.init() - const promise = read.execute({ filePath: path.join(tmp.path, filename) }, ctxWithPermissions) - if (blocked) { - await expect(promise).rejects.toThrow(PermissionNext.DeniedError) - } else { - expect((await promise).output).toContain("content") - } + await read.execute({ filePath: path.join(tmp.path, filename) }, ctxWithPermissions) + expect(askedForEnv).toBe(shouldAsk) }, }) }) From d7a1c268d9b75cb5aea9ad9686d7ac52a3053aa4 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:50:11 -0600 Subject: [PATCH 126/470] fix(app): sanitize markdown -> html --- bun.lock | 6 +++++ package.json | 1 + packages/ui/package.json | 1 + packages/ui/src/components/markdown.tsx | 34 +++++++++++++++++++++++-- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index c8ced856b..787264e4f 100644 --- a/bun.lock +++ b/bun.lock @@ -407,6 +407,7 @@ "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", "@typescript/native-preview": "catalog:", + "dompurify": "catalog:", "fuzzysort": "catalog:", "katex": "0.16.27", "luxon": "catalog:", @@ -507,6 +508,7 @@ "@typescript/native-preview": "7.0.0-dev.20251207.1", "ai": "5.0.119", "diff": "8.0.2", + "dompurify": "3.3.1", "fuzzysort": "3.1.0", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -1828,6 +1830,8 @@ "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="], "@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="], @@ -2280,6 +2284,8 @@ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + "dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="], + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], diff --git a/package.json b/package.json index 031296b26..d134a187a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", + "dompurify": "3.3.1", "ai": "5.0.119", "hono": "4.10.7", "hono-openapi": "1.1.2", diff --git a/packages/ui/package.json b/packages/ui/package.json index bc37a826e..5b440f515 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -51,6 +51,7 @@ "fuzzysort": "catalog:", "katex": "0.16.27", "luxon": "catalog:", + "dompurify": "catalog:", "marked": "catalog:", "marked-katex-extension": "5.1.6", "marked-shiki": "catalog:", diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index 2b0b01874..3aefe04da 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -1,6 +1,8 @@ import { useMarked } from "../context/marked" +import DOMPurify from "dompurify" import { checksum } from "@opencode-ai/util/encode" import { ComponentProps, createResource, splitProps } from "solid-js" +import { isServer } from "solid-js/web" type Entry = { hash: string @@ -10,6 +12,31 @@ type Entry = { const max = 200 const cache = new Map() +if (typeof window !== "undefined" && DOMPurify.isSupported) { + DOMPurify.addHook("afterSanitizeAttributes", (node: Element) => { + if (!(node instanceof HTMLAnchorElement)) return + if (node.target !== "_blank") return + + const rel = node.getAttribute("rel") ?? "" + const set = new Set(rel.split(/\s+/).filter(Boolean)) + set.add("noopener") + set.add("noreferrer") + node.setAttribute("rel", Array.from(set).join(" ")) + }) +} + +const config = { + USE_PROFILES: { html: true, mathMl: true }, + SANITIZE_NAMED_PROPS: true, + FORBID_TAGS: ["style"], + FORBID_CONTENTS: ["style", "script"], +} + +function sanitize(html: string) { + if (!DOMPurify.isSupported) return "" + return DOMPurify.sanitize(html, config) +} + function touch(key: string, value: Entry) { cache.delete(key) cache.set(key, value) @@ -34,6 +61,8 @@ export function Markdown( const [html] = createResource( () => local.text, async (markdown) => { + if (isServer) return "" + const hash = checksum(markdown) const key = local.cacheKey ?? hash @@ -46,8 +75,9 @@ export function Markdown( } const next = await marked.parse(markdown) - if (key && hash) touch(key, { hash, html: next }) - return next + const safe = sanitize(next) + if (key && hash) touch(key, { hash, html: safe }) + return safe }, { initialValue: "" }, ) From e146083b73e0d8f2a33570e24672176d0811d663 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 12 Jan 2026 17:10:50 -0500 Subject: [PATCH 127/470] wip: black --- .../console/app/src/routes/black/index.css | 188 ++++++++++++++++++ .../console/app/src/routes/black/index.tsx | 118 +++++++++-- 2 files changed, 291 insertions(+), 15 deletions(-) diff --git a/packages/console/app/src/routes/black/index.css b/packages/console/app/src/routes/black/index.css index eb0ec87d3..418598792 100644 --- a/packages/console/app/src/routes/black/index.css +++ b/packages/console/app/src/routes/black/index.css @@ -131,6 +131,188 @@ text-decoration: none; } + [data-slot="pricing"] { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 540px; + padding: 0 20px; + + @media (min-width: 768px) { + padding: 0; + } + } + + [data-slot="pricing-card"] { + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + text-decoration: none; + transition: border-color 0.15s ease; + background: transparent; + cursor: pointer; + text-align: left; + + &:hover { + border-color: rgba(255, 255, 255, 0.35); + } + + [data-slot="icon"] { + color: rgba(255, 255, 255, 0.59); + } + + [data-slot="price"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + } + + [data-slot="amount"] { + color: rgba(255, 255, 255, 0.92); + font-size: 24px; + font-weight: 500; + } + + [data-slot="period"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + + [data-slot="multiplier"] { + color: rgba(255, 255, 255, 0.39); + font-size: 14px; + + &::before { + content: "·"; + margin-right: 8px; + } + } + } + + [data-slot="selected-plan"] { + display: flex; + flex-direction: column; + gap: 32px; + width: fit-content; + max-width: calc(100% - 40px); + margin: 0 auto; + } + + [data-slot="selected-card"] { + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + width: fit-content; + + [data-slot="icon"] { + color: rgba(255, 255, 255, 0.59); + } + + [data-slot="price"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + } + + [data-slot="amount"] { + color: rgba(255, 255, 255, 0.92); + font-size: 24px; + font-weight: 500; + } + + [data-slot="period"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + + [data-slot="multiplier"] { + color: rgba(255, 255, 255, 0.39); + font-size: 14px; + + &::before { + content: "·"; + margin-right: 8px; + } + } + + [data-slot="terms"] { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; + text-align: left; + + li { + color: rgba(255, 255, 255, 0.59); + font-size: 13px; + line-height: 1.5; + padding-left: 16px; + position: relative; + white-space: nowrap; + + &::before { + content: "▪"; + position: absolute; + left: 0; + color: rgba(255, 255, 255, 0.39); + } + } + } + + [data-slot="actions"] { + display: flex; + gap: 16px; + margin-top: 8px; + + button, + a { + flex: 1; + display: inline-flex; + height: 48px; + padding: 0 16px; + justify-content: center; + align-items: center; + border-radius: 4px; + font-family: var(--font-mono); + font-size: 16px; + font-weight: 400; + text-decoration: none; + cursor: pointer; + } + + [data-slot="cancel"] { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.17); + color: rgba(255, 255, 255, 0.92); + + &:hover { + border-color: rgba(255, 255, 255, 0.35); + } + } + + [data-slot="continue"] { + background: rgba(255, 255, 255, 0.17); + border: 1px solid rgba(255, 255, 255, 0.17); + color: rgba(255, 255, 255, 0.59); + + &:hover { + background: rgba(255, 255, 255, 0.25); + } + } + } + } + [data-slot="fine-print"] { color: rgba(255, 255, 255, 0.39); text-align: center; @@ -138,6 +320,12 @@ font-style: normal; font-weight: 400; line-height: 160%; /* 20.8px */ + font-style: italic; + + a { + color: rgba(255, 255, 255, 0.39); + text-decoration: underline; + } } } } diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index c83ccd251..f5a375adf 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -1,11 +1,54 @@ -import { A, createAsync } from "@solidjs/router" +import { A, createAsync, useSearchParams } from "@solidjs/router" import "./index.css" import { Title } from "@solidjs/meta" import { github } from "~/lib/github" -import { createMemo, Match, Switch } from "solid-js" +import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js" import { config } from "~/config" +const plans = [ + { id: "20", amount: 20, multiplier: null }, + { id: "100", amount: 100, multiplier: "6x more usage than Black 20" }, + { id: "200", amount: 200, multiplier: "21x more usage than Black 20" }, +] as const + +function PlanIcon(props: { plan: string }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + export default function Black() { + const [params] = useSearchParams() + const [selected, setSelected] = createSignal(params.plan as string | null) + const selectedPlan = createMemo(() => plans.find((p) => p.id === selected())) + const githubData = createAsync(() => github()) const starCount = createMemo(() => githubData()?.stars @@ -16,9 +59,6 @@ export default function Black() { : config.github.starsFormatted.compact, ) - // TODO: Frank, toggle this based on availability - const available = false - return (
    opencode @@ -148,17 +188,65 @@ export default function Black() {

    Including Claude, GPT, Gemini, and more

    - - - Subscribe $200/mo - -

    Fair usage limits apply

    + +
    + + {(plan) => ( + + )} + +
    +

    + Prices shown don't include applicable tax · Terms of Service +

    - -

    We’ll be back soon with more availability.

    - - Follow @opencode - + + {(plan) => ( +
    +
    +
    + +
    +

    + ${plan().amount}{" "} + per person billed monthly + + {plan().multiplier} + +

    +
      +
    • Your subscription will not start immediately
    • +
    • You will be added to the waitlist and activated soon
    • +
    • Your card will be only charged when your subscription is activated
    • +
    • Usage limits apply, heavily automated use may reach limits sooner
    • +
    • Subscriptions for individuals, contact Enterprise for teams
    • +
    • Limits may be adjusted and plans may be discontinued in the future
    • +
    • Cancel your subscription at anytime
    • +
    +
    + + + Continue + +
    +
    +

    + Prices shown don't include applicable tax · Terms of Service +

    +
    + )}
    From fd37d5b54e029a1d082373baccdb56a922af4b8f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 16:46:32 -0600 Subject: [PATCH 128/470] tweak: truncation dir perm --- packages/opencode/src/agent/agent.ts | 12 ++++++++---- packages/opencode/src/tool/truncation.ts | 1 + packages/opencode/test/agent/agent.test.ts | 3 +++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index fffc20426..ea9d3e3ba 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -50,6 +50,7 @@ export namespace Agent { external_directory: { "*": "ask", [Truncate.DIR]: "allow", + [Truncate.GLOB]: "allow", }, question: "deny", // mirrors github.com/github/gitignore Node.gitignore pattern for .env files @@ -124,6 +125,7 @@ export namespace Agent { read: "allow", external_directory: { [Truncate.DIR]: "allow", + [Truncate.GLOB]: "allow", }, }), user, @@ -213,14 +215,16 @@ export namespace Agent { // Ensure Truncate.DIR is allowed unless explicitly configured for (const name in result) { const agent = result[name] - const explicit = agent.permission.some( - (r) => r.permission === "external_directory" && r.pattern === Truncate.DIR && r.action === "deny", - ) + const explicit = agent.permission.some((r) => { + if (r.permission !== "external_directory") return false + if (r.action !== "deny") return false + return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB + }) if (explicit) continue result[name].permission = PermissionNext.merge( result[name].permission, - PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow" } }), + PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }), ) } diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 133c57d3d..a876705d7 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -10,6 +10,7 @@ export namespace Truncate { export const MAX_LINES = 2000 export const MAX_BYTES = 50 * 1024 export const DIR = path.join(Global.Path.data, "tool-output") + export const GLOB = path.join(DIR, "*") const RETENTION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string } diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 68833c794..45f674c18 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -461,6 +461,7 @@ test("Truncate.DIR is allowed even when user denies external_directory globally" fn: async () => { const build = await Agent.get("build") expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("allow") + expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow") expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny") }, }) @@ -484,6 +485,7 @@ test("Truncate.DIR is allowed even when user denies external_directory per-agent fn: async () => { const build = await Agent.get("build") expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("allow") + expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow") expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny") }, }) @@ -506,6 +508,7 @@ test("explicit Truncate.DIR deny is respected", async () => { fn: async () => { const build = await Agent.get("build") expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") + expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny") }, }) }) From 8f22a6b69d09e9e5d8a0bd39ab2137a1f6d87388 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:55:12 -0600 Subject: [PATCH 129/470] Add nova-2 to region prefix models (#8085) Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- packages/opencode/src/provider/provider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 9b01eae9e..4ccaacd54 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -237,6 +237,7 @@ export namespace Provider { "nova-lite", "nova-pro", "nova-premier", + "nova-2", "claude", "deepseek", ].some((m) => modelID.includes(m)) From d954e1e3b641d584113471fe624e86067817c152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jason=20K=C3=B6lker?= Date: Mon, 12 Jan 2026 23:22:42 +0000 Subject: [PATCH 130/470] fix(session): store api url in metadata (#8090) --- packages/opencode/src/session/message-v2.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 2dff17a5e..c1d4015f6 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -664,6 +664,7 @@ export namespace MessageV2 { return `${msg}: ${e.responseBody}` }).trim() + const metadata = e.url ? { url: e.url } : undefined return new MessageV2.APIError( { message, @@ -671,6 +672,7 @@ export namespace MessageV2 { isRetryable: e.isRetryable, responseHeaders: e.responseHeaders, responseBody: e.responseBody, + metadata, }, { cause: e }, ).toObject() From 1a5a63843ed49d94dc2fb2db491eb08c31a169d6 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:30:09 +0100 Subject: [PATCH 131/470] feat(terminal): implement better dynamic terminal numbering (#8081) --- packages/app/src/context/terminal.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 43672fcb3..e85da7896 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -8,6 +8,7 @@ import { Persist, persisted } from "@/utils/persist" export type LocalPTY = { id: string title: string + titleNumber: number rows?: number cols?: number buffer?: string @@ -42,8 +43,21 @@ function createTerminalSession(sdk: ReturnType, dir: string, id: all: createMemo(() => Object.values(store.all)), active: createMemo(() => store.active), new() { + const existingTitleNumbers = new Set( + store.all + .map((pty) => { + const match = pty.titleNumber + return match + }) + ) + + let nextNumber = 1 + while (existingTitleNumbers.has(nextNumber)) { + nextNumber++ + } + sdk.client.pty - .create({ title: `Terminal ${store.all.length + 1}` }) + .create({ title: `Terminal ${nextNumber}` }) .then((pty) => { const id = pty.data?.id if (!id) return @@ -52,6 +66,7 @@ function createTerminalSession(sdk: ReturnType, dir: string, id: { id, title: pty.data?.title ?? "Terminal", + titleNumber: nextNumber, }, ]) setStore("active", id) From 835e48cd2816d789d0ce5312419f7eb35138c9ee Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 12 Jan 2026 23:30:47 +0000 Subject: [PATCH 132/470] chore: generate --- packages/app/src/context/terminal.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index e85da7896..a7753069c 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -44,11 +44,10 @@ function createTerminalSession(sdk: ReturnType, dir: string, id: active: createMemo(() => store.active), new() { const existingTitleNumbers = new Set( - store.all - .map((pty) => { - const match = pty.titleNumber - return match - }) + store.all.map((pty) => { + const match = pty.titleNumber + return match + }), ) let nextNumber = 1 @@ -57,7 +56,7 @@ function createTerminalSession(sdk: ReturnType, dir: string, id: } sdk.client.pty - .create({ title: `Terminal ${nextNumber}` }) + .create({ title: `Terminal ${nextNumber}` }) .then((pty) => { const id = pty.data?.id if (!id) return From 498a4ab408bfebf00fb85db128a26268bcda951f Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 13 Jan 2026 01:02:59 +0100 Subject: [PATCH 133/470] fix: actually modify opencode config with `mcp add` (#7339) --- packages/opencode/src/cli/cmd/mcp.ts | 293 +++++++++++++++++---------- 1 file changed, 182 insertions(+), 111 deletions(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index aaef75267..cfb54081f 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -1,7 +1,6 @@ import { cmd } from "./cmd" import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" import * as prompts from "@clack/prompts" import { UI } from "../ui" @@ -13,6 +12,7 @@ import { Instance } from "../../project/instance" import { Installation } from "../../installation" import path from "path" import { Global } from "../../global" +import { modify, applyEdits } from "jsonc-parser" function getAuthStatusIcon(status: MCP.AuthStatus): string { switch (status) { @@ -366,133 +366,204 @@ export const McpLogoutCommand = cmd({ }, }) +async function resolveConfigPath(baseDir: string, global = false) { + // Check for existing config files (prefer .jsonc over .json, check .opencode/ subdirectory too) + const candidates = [path.join(baseDir, "opencode.json"), path.join(baseDir, "opencode.jsonc")] + + if (!global) { + candidates.push(path.join(baseDir, ".opencode", "opencode.json"), path.join(baseDir, ".opencode", "opencode.jsonc")) + } + + for (const candidate of candidates) { + if (await Bun.file(candidate).exists()) { + return candidate + } + } + + // Default to opencode.json if none exist + return candidates[0] +} + +async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) { + const file = Bun.file(configPath) + + let text = "{}" + if (await file.exists()) { + text = await file.text() + } + + // Use jsonc-parser to modify while preserving comments + const edits = modify(text, ["mcp", name], mcpConfig, { + formattingOptions: { tabSize: 2, insertSpaces: true }, + }) + const result = applyEdits(text, edits) + + await Bun.write(configPath, result) + + return configPath +} + export const McpAddCommand = cmd({ command: "add", describe: "add an MCP server", async handler() { - UI.empty() - prompts.intro("Add MCP server") + await Instance.provide({ + directory: process.cwd(), + async fn() { + UI.empty() + prompts.intro("Add MCP server") - const name = await prompts.text({ - message: "Enter MCP server name", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), - }) - if (prompts.isCancel(name)) throw new UI.CancelledError() + const project = Instance.project - const type = await prompts.select({ - message: "Select MCP server type", - options: [ - { - label: "Local", - value: "local", - hint: "Run a local command", - }, - { - label: "Remote", - value: "remote", - hint: "Connect to a remote URL", - }, - ], - }) - if (prompts.isCancel(type)) throw new UI.CancelledError() + // Resolve config paths eagerly for hints + const [projectConfigPath, globalConfigPath] = await Promise.all([ + resolveConfigPath(Instance.worktree), + resolveConfigPath(Global.Path.config, true), + ]) - if (type === "local") { - const command = await prompts.text({ - message: "Enter command to run", - placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), - }) - if (prompts.isCancel(command)) throw new UI.CancelledError() + // Determine scope + let configPath = globalConfigPath + if (project.vcs === "git") { + const scopeResult = await prompts.select({ + message: "Location", + options: [ + { + label: "Current project", + value: projectConfigPath, + hint: projectConfigPath, + }, + { + label: "Global", + value: globalConfigPath, + hint: globalConfigPath, + }, + ], + }) + if (prompts.isCancel(scopeResult)) throw new UI.CancelledError() + configPath = scopeResult + } - prompts.log.info(`Local MCP server "${name}" configured with command: ${command}`) - prompts.outro("MCP server added successfully") - return - } - - if (type === "remote") { - const url = await prompts.text({ - message: "Enter MCP server URL", - placeholder: "e.g., https://example.com/mcp", - validate: (x) => { - if (!x) return "Required" - if (x.length === 0) return "Required" - const isValid = URL.canParse(x) - return isValid ? undefined : "Invalid URL" - }, - }) - if (prompts.isCancel(url)) throw new UI.CancelledError() - - const useOAuth = await prompts.confirm({ - message: "Does this server require OAuth authentication?", - initialValue: false, - }) - if (prompts.isCancel(useOAuth)) throw new UI.CancelledError() - - if (useOAuth) { - const hasClientId = await prompts.confirm({ - message: "Do you have a pre-registered client ID?", - initialValue: false, + const name = await prompts.text({ + message: "Enter MCP server name", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), }) - if (prompts.isCancel(hasClientId)) throw new UI.CancelledError() + if (prompts.isCancel(name)) throw new UI.CancelledError() - if (hasClientId) { - const clientId = await prompts.text({ - message: "Enter client ID", + const type = await prompts.select({ + message: "Select MCP server type", + options: [ + { + label: "Local", + value: "local", + hint: "Run a local command", + }, + { + label: "Remote", + value: "remote", + hint: "Connect to a remote URL", + }, + ], + }) + if (prompts.isCancel(type)) throw new UI.CancelledError() + + if (type === "local") { + const command = await prompts.text({ + message: "Enter command to run", + placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem", validate: (x) => (x && x.length > 0 ? undefined : "Required"), }) - if (prompts.isCancel(clientId)) throw new UI.CancelledError() + if (prompts.isCancel(command)) throw new UI.CancelledError() - const hasSecret = await prompts.confirm({ - message: "Do you have a client secret?", - initialValue: false, - }) - if (prompts.isCancel(hasSecret)) throw new UI.CancelledError() - - let clientSecret: string | undefined - if (hasSecret) { - const secret = await prompts.password({ - message: "Enter client secret", - }) - if (prompts.isCancel(secret)) throw new UI.CancelledError() - clientSecret = secret + const mcpConfig: Config.Mcp = { + type: "local", + command: command.split(" "), } - prompts.log.info(`Remote MCP server "${name}" configured with OAuth (client ID: ${clientId})`) - prompts.log.info("Add this to your opencode.json:") - prompts.log.info(` - "mcp": { - "${name}": { - "type": "remote", - "url": "${url}", - "oauth": { - "clientId": "${clientId}"${clientSecret ? `,\n "clientSecret": "${clientSecret}"` : ""} - } - } - }`) - } else { - prompts.log.info(`Remote MCP server "${name}" configured with OAuth (dynamic registration)`) - prompts.log.info("Add this to your opencode.json:") - prompts.log.info(` - "mcp": { - "${name}": { - "type": "remote", - "url": "${url}", - "oauth": {} - } - }`) + await addMcpToConfig(name, mcpConfig, configPath) + prompts.log.success(`MCP server "${name}" added to ${configPath}`) + prompts.outro("MCP server added successfully") + return } - } else { - const client = new Client({ - name: "opencode", - version: "1.0.0", - }) - const transport = new StreamableHTTPClientTransport(new URL(url)) - await client.connect(transport) - prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`) - } - } - prompts.outro("MCP server added successfully") + if (type === "remote") { + const url = await prompts.text({ + message: "Enter MCP server URL", + placeholder: "e.g., https://example.com/mcp", + validate: (x) => { + if (!x) return "Required" + if (x.length === 0) return "Required" + const isValid = URL.canParse(x) + return isValid ? undefined : "Invalid URL" + }, + }) + if (prompts.isCancel(url)) throw new UI.CancelledError() + + const useOAuth = await prompts.confirm({ + message: "Does this server require OAuth authentication?", + initialValue: false, + }) + if (prompts.isCancel(useOAuth)) throw new UI.CancelledError() + + let mcpConfig: Config.Mcp + + if (useOAuth) { + const hasClientId = await prompts.confirm({ + message: "Do you have a pre-registered client ID?", + initialValue: false, + }) + if (prompts.isCancel(hasClientId)) throw new UI.CancelledError() + + if (hasClientId) { + const clientId = await prompts.text({ + message: "Enter client ID", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + }) + if (prompts.isCancel(clientId)) throw new UI.CancelledError() + + const hasSecret = await prompts.confirm({ + message: "Do you have a client secret?", + initialValue: false, + }) + if (prompts.isCancel(hasSecret)) throw new UI.CancelledError() + + let clientSecret: string | undefined + if (hasSecret) { + const secret = await prompts.password({ + message: "Enter client secret", + }) + if (prompts.isCancel(secret)) throw new UI.CancelledError() + clientSecret = secret + } + + mcpConfig = { + type: "remote", + url, + oauth: { + clientId, + ...(clientSecret && { clientSecret }), + }, + } + } else { + mcpConfig = { + type: "remote", + url, + oauth: {}, + } + } + } else { + mcpConfig = { + type: "remote", + url, + } + } + + await addMcpToConfig(name, mcpConfig, configPath) + prompts.log.success(`MCP server "${name}" added to ${configPath}`) + } + + prompts.outro("MCP server added successfully") + }, + }) }, }) From d839f70834e6e8a32dcc6e5ad4c6679813904413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 13 Jan 2026 01:18:48 +0100 Subject: [PATCH 134/470] fix(nix): initialize hashes.json with per-system format (#8095) --- nix/hashes.json | 4 ++-- nix/scripts/update-hashes.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index b85e146d7..4c953c5a8 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { - "x86_64-linux": "sha256-8nur5CuUCSV/SzD16hNXVoIlKsiPBXDzCnoITK0IhC4=", - "aarch64-darwin": "sha256-vD1g9dviI2nMBTTPwI87sK01hSZ+cdnmb1V72AdJYq4=" + "x86_64-linux": "sha256-UCPTTk4b7d2bets7KgCeYBHWAUwUAPUyKm+xDYkSexE=", + "aarch64-darwin": "sha256-Y3o6lovahSWoG9un/l1qxu7hCmIlZXm2LxOLKNiPQfQ=" } } diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index 22c556363..1e294fe4f 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -10,7 +10,7 @@ HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE} if [ ! -f "$HASH_FILE" ]; then cat >"$HASH_FILE" </dev/null +jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH" From bf37a88f7f86dc01341a711e9a49e914f9e066a4 Mon Sep 17 00:00:00 2001 From: Darsh Patel Date: Mon, 12 Jan 2026 16:20:50 -0800 Subject: [PATCH 135/470] fix: await auth.set to prevent race condition when setting API key (#8075) --- packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 35951c995..cb33c6301 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -225,7 +225,7 @@ function ApiMethod(props: ApiMethodProps) { } onConfirm={async (value) => { if (!value) return - sdk.client.auth.set({ + await sdk.client.auth.set({ providerID: props.providerID, auth: { type: "api", From 05019dae7607451aab33c593875438f0cc88b199 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 12 Jan 2026 18:42:19 -0600 Subject: [PATCH 136/470] fix(bedrock): use reasoningConfig for Anthropic models on Bedrock (#8092) --- packages/opencode/src/provider/transform.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 38b2c9aa1..fe2484785 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -364,6 +364,25 @@ export namespace ProviderTransform { case "@ai-sdk/amazon-bedrock": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock + // For Anthropic models on Bedrock, use reasoningConfig with budgetTokens + if (model.api.id.includes("anthropic")) { + return { + high: { + reasoningConfig: { + type: "enabled", + budgetTokens: 16000, + }, + }, + max: { + reasoningConfig: { + type: "enabled", + budgetTokens: 31999, + }, + }, + } + } + + // For Amazon Nova models, use reasoningConfig with maxReasoningEffort return Object.fromEntries( WIDELY_SUPPORTED_EFFORTS.map((effort) => [ effort, From 20088a87b0ef37f769eb8096faac89dd4a190af3 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 13 Jan 2026 07:38:27 +0530 Subject: [PATCH 137/470] fix: max completion tokens error for cloudflare (#7970) --- packages/opencode/src/provider/provider.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 4ccaacd54..3b76b1e02 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -419,11 +419,26 @@ export namespace Provider { "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }, - // Custom fetch to strip Authorization header - AI Gateway uses cf-aig-authorization instead - // Sending Authorization header with invalid value causes auth errors + // Custom fetch to handle parameter transformation and auth fetch: async (input: RequestInfo | URL, init?: RequestInit) => { const headers = new Headers(init?.headers) + // Strip Authorization header - AI Gateway uses cf-aig-authorization instead headers.delete("Authorization") + + // Transform max_tokens to max_completion_tokens for newer models + if (init?.body && init.method === "POST") { + try { + const body = JSON.parse(init.body as string) + if (body.max_tokens !== undefined && !body.max_completion_tokens) { + body.max_completion_tokens = body.max_tokens + delete body.max_tokens + init = { ...init, body: JSON.stringify(body) } + } + } catch (e) { + // If body parsing fails, continue with original request + } + } + return fetch(input, { ...init, headers }) }, }, From 5d37e58d3477ecf9783cf28bf952b16b4e1ee044 Mon Sep 17 00:00:00 2001 From: "M. Adel Alhashemi" <64827602+malhashemi@users.noreply.github.com> Date: Tue, 13 Jan 2026 05:37:42 +0300 Subject: [PATCH 138/470] fix(task): respect agent task permission for nested sub-agents (#8111) --- packages/opencode/src/tool/task.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 53b501ba9..170d44480 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -56,6 +56,9 @@ export const TaskTool = Tool.define("task", async (ctx) => { const agent = await Agent.get(params.subagent_type) if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`) + + const hasTaskPermission = agent.permission.some((rule) => rule.permission === "task") + const session = await iife(async () => { if (params.session_id) { const found = await Session.get(params.session_id).catch(() => {}) @@ -76,11 +79,15 @@ export const TaskTool = Tool.define("task", async (ctx) => { pattern: "*", action: "deny", }, - { - permission: "task", - pattern: "*", - action: "deny", - }, + ...(hasTaskPermission + ? [] + : [ + { + permission: "task" as const, + pattern: "*" as const, + action: "deny" as const, + }, + ]), ...(config.experimental?.primary_tools?.map((t) => ({ pattern: "*", action: "allow" as const, @@ -146,7 +153,7 @@ export const TaskTool = Tool.define("task", async (ctx) => { tools: { todowrite: false, todoread: false, - task: false, + ...(hasTaskPermission ? {} : { task: false }), ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), }, parts: promptParts, From eb2044989e4edb59890330562ab35cc9efc0fd33 Mon Sep 17 00:00:00 2001 From: Patrick Schiel Date: Tue, 13 Jan 2026 03:38:01 +0100 Subject: [PATCH 139/470] fix: add missing args to windows tauri cli spawn (#8084) --- packages/desktop/src-tauri/src/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 87ecf4997..2fd26dd01 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -155,6 +155,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command { .shell() .sidecar("opencode-cli") .unwrap() + .args(args.split_whitespace()) .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") .env("OPENCODE_CLIENT", "desktop") .env("XDG_STATE_HOME", &state_dir); From efaa9166fbb3fca1034035f4e9e36f6da68c567b Mon Sep 17 00:00:00 2001 From: lemon <48896771+lengmodkx@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:40:03 +0800 Subject: [PATCH 140/470] fix: prevent [object Object] error display in console output (#8116) Co-authored-by: Claude --- packages/opencode/src/cli/cmd/debug/file.ts | 2 +- packages/opencode/src/cli/cmd/github.ts | 8 ++++---- packages/opencode/src/index.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/debug/file.ts b/packages/opencode/src/cli/cmd/debug/file.ts index d3136952b..6faaf399a 100644 --- a/packages/opencode/src/cli/cmd/debug/file.ts +++ b/packages/opencode/src/cli/cmd/debug/file.ts @@ -78,7 +78,7 @@ const FileTreeCommand = cmd({ }), async handler(args) { const files = await Ripgrep.tree({ cwd: args.dir, limit: 200 }) - console.log(files) + console.log(JSON.stringify(files, null, 2)) }, }) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index e6203d665..d8b1bea30 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -624,7 +624,7 @@ export const GithubRunCommand = cmd({ } } catch (e: any) { exitCode = 1 - console.error(e) + console.error(e instanceof Error ? e.message : String(e)) let msg = e if (e instanceof $.ShellError) { msg = e.stderr.toString() @@ -915,7 +915,7 @@ export const GithubRunCommand = cmd({ // result should always be assistant just satisfying type checker if (result.info.role === "assistant" && result.info.error) { - console.error(result.info) + console.error("Agent error:", result.info.error) throw new Error( `${result.info.error.name}: ${"message" in result.info.error ? result.info.error.message : ""}`, ) @@ -944,7 +944,7 @@ export const GithubRunCommand = cmd({ }) if (summary.info.role === "assistant" && summary.info.error) { - console.error(summary.info) + console.error("Summary agent error:", summary.info.error) throw new Error( `${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`, ) @@ -962,7 +962,7 @@ export const GithubRunCommand = cmd({ try { return await core.getIDToken("opencode-github-action") } catch (error) { - console.error("Failed to get OIDC token:", error) + console.error("Failed to get OIDC token:", error instanceof Error ? error.message : error) throw new Error( "Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.", ) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 3de7735bd..6dc5e99e9 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -147,7 +147,7 @@ try { if (formatted) UI.error(formatted) if (formatted === undefined) { UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL) - console.error(e) + console.error(e instanceof Error ? e.message : String(e)) } process.exitCode = 1 } finally { From 66f9bdab32b21110c0dd9ce0aaa8d928aea8eba2 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 20:39:57 -0600 Subject: [PATCH 141/470] core: tweak edit and write tool outputs to prevent agent from thinking edit didn't apply --- packages/opencode/src/tool/edit.ts | 4 ++-- packages/opencode/src/tool/write.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index b68078f14..7ace4e4a2 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -119,7 +119,7 @@ export const EditTool = Tool.define("edit", { }, }) - let output = "" + let output = "Edit applied successfully." await LSP.touchFile(filePath, true) const diagnostics = await LSP.diagnostics() const normalizedFilePath = Filesystem.normalizePath(filePath) @@ -129,7 +129,7 @@ export const EditTool = Tool.define("edit", { const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE) const suffix = errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : "" - output += `\nThis file has errors, please fix\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n` + output += `\n\nLSP errors detected in this file:\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n` } return { diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 222bac3f8..d621a6e26 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -47,7 +47,7 @@ export const WriteTool = Tool.define("write", { }) FileTime.read(ctx.sessionID, filepath) - let output = "" + let output = "Wrote file successfully." await LSP.touchFile(filepath, true) const diagnostics = await LSP.diagnostics() const normalizedFilepath = Filesystem.normalizePath(filepath) @@ -59,12 +59,12 @@ export const WriteTool = Tool.define("write", { const suffix = errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : "" if (file === normalizedFilepath) { - output += `\nThis file has errors, please fix\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n` + output += `\n\nLSP errors detected in this file:\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n` continue } if (projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue projectDiagnosticsCount++ - output += `\n\n${file}\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n` + output += `\n\nLSP errors detected in other files:\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n` } return { From eaa76dad0cd7be52a969ceaa2a6baa338cc9929d Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 12 Jan 2026 22:33:57 -0500 Subject: [PATCH 142/470] get rid of extra file --- infra/console.ts | 1 + .../console/app/src/context/auth.session.ts | 24 ----------------- packages/console/app/src/context/auth.ts | 27 ++++++++++++++++++- .../console/app/src/routes/auth/callback.ts | 2 +- .../console/app/src/routes/auth/logout.ts | 2 +- .../console/app/src/routes/auth/status.ts | 2 +- packages/console/app/src/routes/user-menu.tsx | 2 +- packages/console/core/sst-env.d.ts | 4 +++ packages/console/function/sst-env.d.ts | 4 +++ packages/console/resource/sst-env.d.ts | 4 +++ packages/enterprise/sst-env.d.ts | 4 +++ packages/function/sst-env.d.ts | 4 +++ sst-env.d.ts | 4 +++ 13 files changed, 55 insertions(+), 29 deletions(-) diff --git a/infra/console.ts b/infra/console.ts index 1e584ca57..1368ef202 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -163,6 +163,7 @@ new sst.cloudflare.x.SolidStart("Console", { AWS_SES_ACCESS_KEY_ID, AWS_SES_SECRET_ACCESS_KEY, ZEN_BLACK, + new sst.Secret("ZEN_SESSION_SECRET"), ...ZEN_MODELS, ...($dev ? [ diff --git a/packages/console/app/src/context/auth.session.ts b/packages/console/app/src/context/auth.session.ts index 726b6c834..e69de29bb 100644 --- a/packages/console/app/src/context/auth.session.ts +++ b/packages/console/app/src/context/auth.session.ts @@ -1,24 +0,0 @@ -import { useSession } from "@solidjs/start/http" - -export interface AuthSession { - account?: Record< - string, - { - id: string - email: string - } - > - current?: string -} - -export function useAuthSession() { - return useSession({ - password: "0".repeat(32), - name: "auth", - maxAge: 60 * 60 * 24 * 365, - cookie: { - secure: false, - httpOnly: true, - }, - }) -} diff --git a/packages/console/app/src/context/auth.ts b/packages/console/app/src/context/auth.ts index dbbd3c3b2..aed07a630 100644 --- a/packages/console/app/src/context/auth.ts +++ b/packages/console/app/src/context/auth.ts @@ -5,13 +5,38 @@ import { redirect } from "@solidjs/router" import { Actor } from "@opencode-ai/console-core/actor.js" import { createClient } from "@openauthjs/openauth/client" -import { useAuthSession } from "./auth.session" export const AuthClient = createClient({ clientID: "app", issuer: import.meta.env.VITE_AUTH_URL, }) +import { useSession } from "@solidjs/start/http" +import { Resource } from "@opencode-ai/console-resource" + +export interface AuthSession { + account?: Record< + string, + { + id: string + email: string + } + > + current?: string +} + +export function useAuthSession() { + return useSession({ + password: Resource.ZEN_SESSION_SECRET.value, + name: "auth", + maxAge: 60 * 60 * 24 * 365, + cookie: { + secure: false, + httpOnly: true, + }, + }) +} + export const getActor = async (workspace?: string): Promise => { "use server" const evt = getRequestEvent() diff --git a/packages/console/app/src/routes/auth/callback.ts b/packages/console/app/src/routes/auth/callback.ts index 2f8781e98..9b7296791 100644 --- a/packages/console/app/src/routes/auth/callback.ts +++ b/packages/console/app/src/routes/auth/callback.ts @@ -1,7 +1,7 @@ import { redirect } from "@solidjs/router" import type { APIEvent } from "@solidjs/start/server" import { AuthClient } from "~/context/auth" -import { useAuthSession } from "~/context/auth.session" +import { useAuthSession } from "~/context/auth" export async function GET(input: APIEvent) { const url = new URL(input.request.url) diff --git a/packages/console/app/src/routes/auth/logout.ts b/packages/console/app/src/routes/auth/logout.ts index 7fbe5199a..9aaac37e2 100644 --- a/packages/console/app/src/routes/auth/logout.ts +++ b/packages/console/app/src/routes/auth/logout.ts @@ -1,6 +1,6 @@ import { redirect } from "@solidjs/router" import { APIEvent } from "@solidjs/start" -import { useAuthSession } from "~/context/auth.session" +import { useAuthSession } from "~/context/auth" export async function GET(event: APIEvent) { const auth = await useAuthSession() diff --git a/packages/console/app/src/routes/auth/status.ts b/packages/console/app/src/routes/auth/status.ts index eaab9dbef..215cae698 100644 --- a/packages/console/app/src/routes/auth/status.ts +++ b/packages/console/app/src/routes/auth/status.ts @@ -1,5 +1,5 @@ import { APIEvent } from "@solidjs/start" -import { useAuthSession } from "~/context/auth.session" +import { useAuthSession } from "~/context/auth" export async function GET(input: APIEvent) { const session = await useAuthSession() diff --git a/packages/console/app/src/routes/user-menu.tsx b/packages/console/app/src/routes/user-menu.tsx index e0931efd9..a910c2efd 100644 --- a/packages/console/app/src/routes/user-menu.tsx +++ b/packages/console/app/src/routes/user-menu.tsx @@ -1,6 +1,6 @@ import { action } from "@solidjs/router" import { getRequestEvent } from "solid-js/web" -import { useAuthSession } from "~/context/auth.session" +import { useAuthSession } from "~/context/auth" import { Dropdown } from "~/component/dropdown" import "./user-menu.css" diff --git a/packages/console/core/sst-env.d.ts b/packages/console/core/sst-env.d.ts index 4450c6cb6..96fada3e3 100644 --- a/packages/console/core/sst-env.d.ts +++ b/packages/console/core/sst-env.d.ts @@ -130,6 +130,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_SESSION_SECRET": { + "type": "sst.sst.Secret" + "value": string + } } } // cloudflare diff --git a/packages/console/function/sst-env.d.ts b/packages/console/function/sst-env.d.ts index 4450c6cb6..96fada3e3 100644 --- a/packages/console/function/sst-env.d.ts +++ b/packages/console/function/sst-env.d.ts @@ -130,6 +130,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_SESSION_SECRET": { + "type": "sst.sst.Secret" + "value": string + } } } // cloudflare diff --git a/packages/console/resource/sst-env.d.ts b/packages/console/resource/sst-env.d.ts index 4450c6cb6..96fada3e3 100644 --- a/packages/console/resource/sst-env.d.ts +++ b/packages/console/resource/sst-env.d.ts @@ -130,6 +130,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_SESSION_SECRET": { + "type": "sst.sst.Secret" + "value": string + } } } // cloudflare diff --git a/packages/enterprise/sst-env.d.ts b/packages/enterprise/sst-env.d.ts index 4450c6cb6..96fada3e3 100644 --- a/packages/enterprise/sst-env.d.ts +++ b/packages/enterprise/sst-env.d.ts @@ -130,6 +130,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_SESSION_SECRET": { + "type": "sst.sst.Secret" + "value": string + } } } // cloudflare diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index 4450c6cb6..96fada3e3 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -130,6 +130,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_SESSION_SECRET": { + "type": "sst.sst.Secret" + "value": string + } } } // cloudflare diff --git a/sst-env.d.ts b/sst-env.d.ts index 6e8b8e67e..035a5fc21 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -156,6 +156,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_SESSION_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "ZenData": { "name": string "type": "sst.cloudflare.Bucket" From 789e111a0f3e09b01fa52eddad15007e0c8a025d Mon Sep 17 00:00:00 2001 From: Leonidas <77194479+LeonMueller-OneAndOnly@users.noreply.github.com> Date: Tue, 13 Jan 2026 04:43:44 +0100 Subject: [PATCH 143/470] fix(TUI): dont submit prompt when switching sessions (#8016) --- packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index c5d36826c..98adcdeb1 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -158,7 +158,8 @@ export function DialogSelect(props: DialogSelectProps) { if (evt.name === "return") { const option = selected() if (option) { - // evt.preventDefault() + evt.preventDefault() + evt.stopPropagation() if (option.onSelect) option.onSelect(dialog) props.onSelect?.(option) } From b4ad5c138ee1a5c808cf742ae828b6903b0f7649 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 12 Jan 2026 23:04:50 -0600 Subject: [PATCH 144/470] tweak: for zai ensure clear_thinking is false --- packages/opencode/src/provider/transform.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index fe2484785..28e8d97ab 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -476,6 +476,13 @@ export namespace ProviderTransform { result["chat_template_args"] = { enable_thinking: true } } + if (["zai", "zhipuai"].includes(model.providerID) && model.api.npm === "@ai-sdk/openai-compatible") { + result["thinking"] = { + type: "enabled", + clear_thinking: false, + } + } + if (model.providerID === "openai" || providerOptions?.setCacheKey) { result["promptCacheKey"] = sessionID } From f4f8f2d15134ccd7b5538687cc1e50f6dc652989 Mon Sep 17 00:00:00 2001 From: Eric Guo Date: Tue, 13 Jan 2026 13:10:56 +0800 Subject: [PATCH 145/470] feat(cli): Support debug tool calling directly in CLI. (#6564) --- packages/opencode/src/cli/cmd/debug/agent.ts | 149 ++++++++++++++++++- 1 file changed, 143 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index ec5ef0c43..ef6b0c4fc 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -1,6 +1,14 @@ import { EOL } from "os" import { basename } from "path" import { Agent } from "../../../agent/agent" +import { Provider } from "../../../provider/provider" +import { Session } from "../../../session" +import type { MessageV2 } from "../../../session/message-v2" +import { Identifier } from "../../../id/id" +import { ToolRegistry } from "../../../tool/registry" +import { Instance } from "../../../project/instance" +import { PermissionNext } from "../../../permission/next" +import { iife } from "../../../util/iife" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" @@ -8,11 +16,20 @@ export const AgentCommand = cmd({ command: "agent ", describe: "show agent configuration details", builder: (yargs) => - yargs.positional("name", { - type: "string", - demandOption: true, - description: "Agent name", - }), + yargs + .positional("name", { + type: "string", + demandOption: true, + description: "Agent name", + }) + .option("tool", { + type: "string", + description: "Tool id to execute", + }) + .option("params", { + type: "string", + description: "Tool params as JSON or a JS object literal", + }), async handler(args) { await bootstrap(process.cwd(), async () => { const agentName = args.name as string @@ -23,7 +40,127 @@ export const AgentCommand = cmd({ ) process.exit(1) } - process.stdout.write(JSON.stringify(agent, null, 2) + EOL) + const availableTools = await getAvailableTools(agent) + const resolvedTools = await resolveTools(agent, availableTools) + const toolID = args.tool as string | undefined + if (toolID) { + const tool = availableTools.find((item) => item.id === toolID) + if (!tool) { + process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL) + process.exit(1) + } + if (resolvedTools[toolID] === false) { + process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL) + process.exit(1) + } + const params = parseToolParams(args.params as string | undefined) + const ctx = await createToolContext(agent) + const result = await tool.execute(params, ctx) + process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL) + return + } + + const output = { + ...agent, + tools: resolvedTools, + } + process.stdout.write(JSON.stringify(output, null, 2) + EOL) }) }, }) + +async function getAvailableTools(agent: Agent.Info) { + const providerID = agent.model?.providerID ?? (await Provider.defaultModel()).providerID + return ToolRegistry.tools(providerID, agent) +} + +async function resolveTools(agent: Agent.Info, availableTools: Awaited>) { + const disabled = PermissionNext.disabled( + availableTools.map((tool) => tool.id), + agent.permission, + ) + const resolved: Record = {} + for (const tool of availableTools) { + resolved[tool.id] = !disabled.has(tool.id) + } + return resolved +} + +function parseToolParams(input?: string) { + if (!input) return {} + const trimmed = input.trim() + if (trimmed.length === 0) return {} + + const parsed = iife(() => { + try { + return JSON.parse(trimmed) + } catch (jsonError) { + try { + return new Function(`return (${trimmed})`)() + } catch (evalError) { + throw new Error( + `Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`, + ) + } + } + }) + + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Tool params must be an object.") + } + return parsed as Record +} + +async function createToolContext(agent: Agent.Info) { + const session = await Session.create({ title: `Debug tool run (${agent.name})` }) + const messageID = Identifier.ascending("message") + const model = agent.model ?? (await Provider.defaultModel()) + const now = Date.now() + const message: MessageV2.Assistant = { + id: messageID, + sessionID: session.id, + role: "assistant", + time: { + created: now, + }, + parentID: messageID, + modelID: model.modelID, + providerID: model.providerID, + mode: "debug", + agent: agent.name, + path: { + cwd: Instance.directory, + root: Instance.worktree, + }, + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { + read: 0, + write: 0, + }, + }, + } + await Session.updateMessage(message) + + const ruleset = PermissionNext.merge(agent.permission, session.permission ?? []) + + return { + sessionID: session.id, + messageID, + callID: Identifier.ascending("part"), + agent: agent.name, + abort: new AbortController().signal, + metadata: () => {}, + async ask(req: Omit) { + for (const pattern of req.patterns) { + const rule = PermissionNext.evaluate(req.permission, pattern, ruleset) + if (rule.action === "deny") { + throw new PermissionNext.DeniedError(ruleset) + } + } + }, + } +} From c0b214232d8820f53c6a9f7fc1d38334012417db Mon Sep 17 00:00:00 2001 From: ShoeBoom <15147944+ShoeBoom@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:11:26 -0500 Subject: [PATCH 146/470] fix(config): handle write errors when updating schema in opencode config (#8125) --- packages/opencode/src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 127406d1d..bf4a6035b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1159,7 +1159,7 @@ export namespace Config { if (parsed.success) { if (!parsed.data.$schema) { parsed.data.$schema = "https://opencode.ai/config.json" - await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2)) + await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2)).catch(() => {}) } const data = parsed.data if (data.plugin) { From f05f175842d57003692ea766b07857d6f4b05f2b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 13 Jan 2026 13:58:00 +0800 Subject: [PATCH 147/470] feat(desktop): spawn local server with password (#8139) --- packages/app/src/context/global-sdk.tsx | 3 +- packages/app/src/context/global-sync.tsx | 4 ++ packages/desktop/scripts/predev.ts | 4 +- packages/desktop/scripts/prepare.ts | 6 +- packages/desktop/scripts/utils.ts | 7 ++- packages/desktop/src-tauri/Cargo.lock | 7 ++- packages/desktop/src-tauri/Cargo.toml | 1 + packages/desktop/src-tauri/src/lib.rs | 76 +++++++++++++++++------- packages/desktop/src/index.tsx | 54 +++++++++++++---- 9 files changed, 118 insertions(+), 44 deletions(-) diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index dc8f937ff..7d93682bf 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -9,11 +9,13 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo name: "GlobalSDK", init: () => { const server = useServer() + const platform = usePlatform() const abort = new AbortController() const eventSdk = createOpencodeClient({ baseUrl: server.url, signal: abort.signal, + fetch: platform.fetch, }) const emitter = createGlobalEmitter<{ [key: string]: Event @@ -93,7 +95,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo stop() }) - const platform = usePlatform() const sdk = createOpencodeClient({ baseUrl: server.url, fetch: platform.fetch, diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index a0656c5fc..ddac1f228 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -26,6 +26,7 @@ import { ErrorPage, type InitError } from "../pages/error" import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js" import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/util/path" +import { usePlatform } from "./platform" type State = { status: "loading" | "partial" | "complete" @@ -64,6 +65,7 @@ type State = { function createGlobalSync() { const globalSDK = useGlobalSDK() + const platform = usePlatform() const [globalStore, setGlobalStore] = createStore<{ ready: boolean error?: InitError @@ -139,6 +141,7 @@ function createGlobalSync() { const [store, setStore] = child(directory) const sdk = createOpencodeClient({ baseUrl: globalSDK.url, + fetch: platform.fetch, directory, throwOnError: true, }) @@ -396,6 +399,7 @@ function createGlobalSync() { case "lsp.updated": { const sdk = createOpencodeClient({ baseUrl: globalSDK.url, + fetch: platform.fetch, directory, throwOnError: true, }) diff --git a/packages/desktop/scripts/predev.ts b/packages/desktop/scripts/predev.ts index 3d0cd5e92..3e14250b1 100644 --- a/packages/desktop/scripts/predev.ts +++ b/packages/desktop/scripts/predev.ts @@ -1,12 +1,12 @@ import { $ } from "bun" -import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils" +import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils" const RUST_TARGET = Bun.env.TAURI_ENV_TARGET_TRIPLE const sidecarConfig = getCurrentSidecar(RUST_TARGET) -const binaryPath = `../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}` +const binaryPath = windowsify(`../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode`) await $`cd ../opencode && bun run build --single` diff --git a/packages/desktop/scripts/prepare.ts b/packages/desktop/scripts/prepare.ts index 495a0baea..24ff9e7e0 100755 --- a/packages/desktop/scripts/prepare.ts +++ b/packages/desktop/scripts/prepare.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { $ } from "bun" -import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils" +import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils" const sidecarConfig = getCurrentSidecar() @@ -10,6 +10,4 @@ const dir = "src-tauri/target/opencode-binaries" await $`mkdir -p ${dir}` await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir) -await copyBinaryToSidecarFolder( - `${dir}/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`, -) +await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`)) diff --git a/packages/desktop/scripts/utils.ts b/packages/desktop/scripts/utils.ts index 885d0afce..c3019f0b9 100644 --- a/packages/desktop/scripts/utils.ts +++ b/packages/desktop/scripts/utils.ts @@ -41,8 +41,13 @@ export function getCurrentSidecar(target = RUST_TARGET) { export async function copyBinaryToSidecarFolder(source: string, target = RUST_TARGET) { await $`mkdir -p src-tauri/sidecars` - const dest = `src-tauri/sidecars/opencode-cli-${target}${process.platform === "win32" ? ".exe" : ""}` + const dest = windowsify(`src-tauri/sidecars/opencode-cli-${target}`) await $`cp ${source} ${dest}` console.log(`Copied ${source} to ${dest}`) } + +export function windowsify(path: string) { + if (path.endsWith(".exe")) return path + return `${path}${process.platform === "win32" ? ".exe" : ""}` +} diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 92953ea19..43f24a6ad 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2814,6 +2814,7 @@ dependencies = [ "tauri-plugin-updater", "tauri-plugin-window-state", "tokio", + "uuid", "webkit2gtk", ] @@ -5364,13 +5365,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 8033d4f14..3145ae4b2 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -39,6 +39,7 @@ tauri-plugin-os = "2" futures = "0.3.31" semver = "1.0.27" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +uuid = { version = "1.19.0", features = ["v4"] } [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index b479ed0b6..e2682ec71 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ mod window_customizer; use cli::{install_cli, sync_cli}; use futures::FutureExt; +use futures::future; use std::{ collections::VecDeque, net::TcpListener, @@ -13,22 +14,29 @@ use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, Webvie use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_store::StoreExt; +use tokio::sync::oneshot; use crate::window_customizer::PinchZoomDisablePlugin; const SETTINGS_STORE: &str = "opencode.settings.dat"; const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; +#[derive(Clone, serde::Serialize)] +struct ServerReadyData { + url: String, + password: Option, +} + #[derive(Clone)] struct ServerState { child: Arc>>, - status: futures::future::Shared>>, + status: future::Shared>>, } impl ServerState { pub fn new( child: Option, - status: tokio::sync::oneshot::Receiver>, + status: oneshot::Receiver>, ) -> Self { Self { child: Arc::new(Mutex::new(child)), @@ -80,7 +88,7 @@ async fn get_logs(app: AppHandle) -> Result { } #[tauri::command] -async fn ensure_server_ready(state: State<'_, ServerState>) -> Result { +async fn ensure_server_ready(state: State<'_, ServerState>) -> Result { state .status .clone() @@ -137,13 +145,14 @@ fn get_sidecar_port() -> u32 { }) as u32 } -fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { +fn spawn_sidecar(app: &AppHandle, port: u32, password: &str) -> CommandChild { let log_state = app.state::(); let log_state_clone = log_state.inner().clone(); println!("spawning sidecar on port {port}"); let (mut rx, child) = cli::create_command(app, format!("serve --port {port}").as_str()) + .env("OPENCODE_SERVER_PASSWORD", password) .spawn() .expect("Failed to spawn opencode"); @@ -184,7 +193,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { child } -async fn check_server_health(url: &str) -> bool { +async fn check_server_health(url: &str, password: Option<&str>) -> bool { let health_url = format!("{}/health", url.trim_end_matches('/')); let client = reqwest::Client::builder() .timeout(Duration::from_secs(3)) @@ -194,9 +203,13 @@ async fn check_server_health(url: &str) -> bool { return false; }; - client - .get(&health_url) - .send() + let mut req = client.get(&health_url); + + if let Some(password) = password { + req = req.basic_auth("opencode", Some(password)); + } + + req.send() .await .map(|r| r.status().is_success()) .unwrap_or(false) @@ -267,7 +280,7 @@ pub fn run() { window_builder.build().expect("Failed to create window"); - let (tx, rx) = tokio::sync::oneshot::channel(); + let (tx, rx) = oneshot::channel(); app.manage(ServerState::new(None, rx)); { @@ -344,12 +357,18 @@ fn get_server_url_from_config(config: &cli::Config) -> Option { async fn setup_server_connection( app: &AppHandle, custom_url: Option, -) -> Result<(Option, String), String> { +) -> Result<(Option, ServerReadyData), String> { if let Some(url) = custom_url { loop { - if check_server_health(&url).await { + if check_server_health(&url, None).await { println!("Connected to custom server: {}", url); - return Ok((None, url.clone())); + return Ok(( + None, + ServerReadyData { + url: url.clone(), + password: None, + }, + )); } const RETRY: &str = "Retry"; @@ -374,19 +393,36 @@ async fn setup_server_connection( let local_port = get_sidecar_port(); let local_url = format!("http://127.0.0.1:{local_port}"); - if !check_server_health(&local_url).await { - match spawn_local_server(app, local_port).await { - Ok(child) => Ok(Some(child)), + if !check_server_health(&local_url, None).await { + let password = uuid::Uuid::new_v4().to_string(); + + match spawn_local_server(app, local_port, &password).await { + Ok(child) => Ok(( + Some(child), + ServerReadyData { + url: local_url, + password: Some(password), + }, + )), Err(err) => Err(err), } } else { - Ok(None) + Ok(( + None, + ServerReadyData { + url: local_url, + password: None, + }, + )) } - .map(|child| (child, local_url)) } -async fn spawn_local_server(app: &AppHandle, port: u32) -> Result { - let child = spawn_sidecar(app, port); +async fn spawn_local_server( + app: &AppHandle, + port: u32, + password: &str, +) -> Result { + let child = spawn_sidecar(app, port, password); let url = format!("http://127.0.0.1:{port}"); let timestamp = Instant::now(); @@ -400,7 +436,7 @@ async fn spawn_local_server(app: &AppHandle, port: u32) -> Result): Platform => ({ platform: "desktop", version: pkg.version, @@ -256,7 +255,25 @@ const platform: Platform = { }, // @ts-expect-error - fetch: tauriFetch, + fetch: (input, init) => { + const pw = password() + + const addHeader = (headers: Headers, password: string) => { + headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`) + } + + if (input instanceof Request) { + if (pw) addHeader(input.headers, pw) + return tauriFetch(input) + } else { + const headers = new Headers(init?.headers) + if (pw) addHeader(headers, pw) + return tauriFetch(input, { + ...(init as any), + headers: headers, + }) + } + }, getDefaultServerUrl: async () => { const result = await invoke("get_default_server_url").catch(() => null) @@ -266,7 +283,7 @@ const platform: Platform = { setDefaultServerUrl: async (url: string | null) => { await invoke("set_default_server_url", { url }) }, -} +}) createMenu() @@ -276,26 +293,37 @@ root?.addEventListener("mousewheel", (e) => { }) render(() => { + const [serverPassword, setServerPassword] = createSignal(null) + const platform = createPlatform(() => serverPassword()) + return ( - {ostype() === "macos" && ( -
    - )} - {(serverUrl) => } + {ostype() === "macos" && ( +
    + )} + + {(data) => { + setServerPassword(data().password) + + return + }} + ) }, root!) +type ServerReadyData = { url: string; password: string | null } + // Gate component that waits for the server to be ready -function ServerGate(props: { children: (url: Accessor) => JSX.Element }) { - const [serverUrl] = createResource(() => invoke("ensure_server_ready")) +function ServerGate(props: { children: (data: Accessor) => JSX.Element }) { + const [serverData] = createResource(() => invoke("ensure_server_ready")) return ( // Not using suspense as not all components are compatible with it (undefined refs) @@ -303,7 +331,7 @@ function ServerGate(props: { children: (url: Accessor) => JSX.Element })
    } > - {(serverUrl) => props.children(serverUrl)} + {(data) => props.children(data)} ) } From afb8a0d28a28795dd51128729250d57e8285106b Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 13 Jan 2026 06:01:17 +0000 Subject: [PATCH 148/470] release: v1.1.16 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 787264e4f..10001bb61 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -99,7 +99,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -126,7 +126,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -150,7 +150,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -174,7 +174,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -203,7 +203,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -232,7 +232,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -248,7 +248,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.15", + "version": "1.1.16", "bin": { "opencode": "./bin/opencode", }, @@ -351,7 +351,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -371,7 +371,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.15", + "version": "1.1.16", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -382,7 +382,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -395,7 +395,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -435,7 +435,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "zod": "catalog:", }, @@ -446,7 +446,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 771d9322d..bef67c82c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.15", + "version": "1.1.16", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index c44cfb5e5..9557f8310 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.15", + "version": "1.1.16", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 502537ec4..ecfb20007 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.1.15", + "version": "1.1.16", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 5bf3f0975..8f3b1ddee 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.15", + "version": "1.1.16", "$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 b4d371afd..9572cfde8 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index d8c3fc92a..5cf2b20db 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.15", + "version": "1.1.16", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index d8fc98c61..7dcdb574d 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.15", + "version": "1.1.16", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 51bb13a3e..6ccac0c10 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.15" +version = "1.1.16" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 8c85fe0dd..cc8ae0f18 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.15", + "version": "1.1.16", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 459e6f657..07fee7d73 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.15", + "version": "1.1.16", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 8fc6be7ac..d0b0c5e42 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.1.15", + "version": "1.1.16", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e29be370c..fe34f1775 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.1.15", + "version": "1.1.16", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index c73dc0b8a..1b2d90166 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.15", + "version": "1.1.16", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 5b440f515..b88c747e1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.15", + "version": "1.1.16", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 3be2c9e71..fcd980b4a 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.15", + "version": "1.1.16", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 5eeadf227..5a657fe0d 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.1.15", + "version": "1.1.16", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 2a953baf8..2bfb97a33 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.1.15", + "version": "1.1.16", "publisher": "sst-dev", "repository": { "type": "git", From f1f44644e2803b4c47123ba60d6ed1e91d10e915 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 13 Jan 2026 01:01:06 -0600 Subject: [PATCH 149/470] fix: brew autoupgrade --- packages/opencode/src/installation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 86f2a781c..9e6dd2b9e 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -138,7 +138,7 @@ export namespace Installation { break case "brew": { const formula = await getBrewFormula() - cmd = $`brew install ${formula}`.env({ + cmd = $`brew upgrade ${formula}`.env({ HOMEBREW_NO_AUTO_UPDATE: "1", ...process.env, }) From 68a0947292e90b737e921fd3f8af0a7ad6a769a5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jan 2026 07:01:50 +0000 Subject: [PATCH 150/470] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d0b0c5e42..d1848b4a3 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index fe34f1775..ca24d02aa 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 21990621e243ddd63485d5ad2400c44f00ecb191 Mon Sep 17 00:00:00 2001 From: zerone0x Date: Tue, 13 Jan 2026 15:04:49 +0800 Subject: [PATCH 151/470] fix(tui): prevent question tool keybindings when dialog is open (#8147) Co-authored-by: Claude --- packages/opencode/src/cli/cmd/tui/routes/session/question.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx index c6518ec3f..ccc0e9b12 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx @@ -121,6 +121,9 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { const dialog = useDialog() useKeyboard((evt) => { + // Skip processing if a dialog (e.g., command palette) is open + if (dialog.stack.length > 0) return + // When editing "Other" textarea if (store.editing && !confirm()) { if (evt.name === "escape") { From 2072c8681a78653aee1f221f2761a8c4c517c7a3 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:07:35 -0800 Subject: [PATCH 152/470] fix: remove the symlinkBinary function call that replaces the wrapper script (#8133) Co-authored-by: Chuck Chen <459052+chuckchen@users.noreply.github.com> --- packages/opencode/script/postinstall.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index 78f022c9f..14103895a 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -106,8 +106,11 @@ async function main() { return } - const { binaryPath, binaryName } = findBinary() - symlinkBinary(binaryPath, binaryName) + // On non-Windows platforms, just verify the binary package exists + // Don't replace the wrapper script - it handles binary execution + const { binaryPath } = findBinary() + console.log(`Platform binary verified at: ${binaryPath}`) + console.log("Wrapper script will handle binary execution") } catch (error) { console.error("Failed to setup opencode binary:", error.message) process.exit(1) From 520a814fc20b5c87481f8ba73703c1c2d8142bc5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jan 2026 07:08:25 +0000 Subject: [PATCH 153/470] chore: generate --- packages/opencode/script/postinstall.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index 14103895a..e8b5e995c 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -106,11 +106,11 @@ async function main() { return } - // On non-Windows platforms, just verify the binary package exists - // Don't replace the wrapper script - it handles binary execution - const { binaryPath } = findBinary() - console.log(`Platform binary verified at: ${binaryPath}`) - console.log("Wrapper script will handle binary execution") + // On non-Windows platforms, just verify the binary package exists + // Don't replace the wrapper script - it handles binary execution + const { binaryPath } = findBinary() + console.log(`Platform binary verified at: ${binaryPath}`) + console.log("Wrapper script will handle binary execution") } catch (error) { console.error("Failed to setup opencode binary:", error.message) process.exit(1) From ddd9c71cca1f30a8214174fc10975e2ff3bb4635 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 13 Jan 2026 15:32:54 +0800 Subject: [PATCH 154/470] feat(desktop): Tie desktop & CLI to the same Windows JobObject (#8153) --- packages/desktop/src-tauri/Cargo.lock | 1 + packages/desktop/src-tauri/Cargo.toml | 8 + packages/desktop/src-tauri/src/job_object.rs | 145 +++++++++++++++++++ packages/desktop/src-tauri/src/lib.rs | 14 ++ 4 files changed, 168 insertions(+) create mode 100644 packages/desktop/src-tauri/src/job_object.rs diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 43f24a6ad..e577b4db7 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2816,6 +2816,7 @@ dependencies = [ "tokio", "uuid", "webkit2gtk", + "windows", ] [[package]] diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 3145ae4b2..05422b096 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -44,3 +44,11 @@ uuid = { version = "1.19.0", features = ["v4"] } [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" webkit2gtk = "=2.0.1" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_System_JobObjects", + "Win32_System_Threading", + "Win32_Security" +] } diff --git a/packages/desktop/src-tauri/src/job_object.rs b/packages/desktop/src-tauri/src/job_object.rs new file mode 100644 index 000000000..220aa5db6 --- /dev/null +++ b/packages/desktop/src-tauri/src/job_object.rs @@ -0,0 +1,145 @@ +//! Windows Job Object for reliable child process cleanup. +//! +//! This module provides a wrapper around Windows Job Objects with the +//! `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` flag set. When the job object handle +//! is closed (including when the parent process exits or crashes), Windows +//! automatically terminates all processes assigned to the job. +//! +//! This is more reliable than manual cleanup because it works even if: +//! - The parent process crashes +//! - The parent is killed via Task Manager +//! - The RunEvent::Exit handler fails to run + +use std::io::{Error, Result}; +#[cfg(windows)] +use std::sync::Mutex; +use windows::Win32::Foundation::{CloseHandle, HANDLE}; +use windows::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, + SetInformationJobObject, +}; +use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE}; + +/// A Windows Job Object configured to kill all assigned processes when closed. +/// +/// When this struct is dropped or when the owning process exits (even abnormally), +/// Windows will automatically terminate all processes that have been assigned to it. +pub struct JobObject(HANDLE); + +// SAFETY: HANDLE is just a pointer-sized value, and Windows job objects +// can be safely accessed from multiple threads. +unsafe impl Send for JobObject {} +unsafe impl Sync for JobObject {} + +impl JobObject { + /// Creates a new anonymous job object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` set. + /// + /// When the last handle to this job is closed (including on process exit), + /// Windows will terminate all processes assigned to the job. + pub fn new() -> Result { + unsafe { + // Create an anonymous job object + let job = CreateJobObjectW(None, None).map_err(|e| Error::other(e.message()))?; + + // Configure the job to kill all processes when the handle is closed + let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default(); + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + SetInformationJobObject( + job, + JobObjectExtendedLimitInformation, + &info as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ) + .map_err(|e| Error::other(e.message()))?; + + Ok(Self(job)) + } + } + + /// Assigns a process to this job object by its process ID. + /// + /// Once assigned, the process will be terminated when this job object is dropped + /// or when the owning process exits. + /// + /// # Arguments + /// * `pid` - The process ID of the process to assign + pub fn assign_pid(&self, pid: u32) -> Result<()> { + unsafe { + // Open a handle to the process with the minimum required permissions + // PROCESS_SET_QUOTA and PROCESS_TERMINATE are required by AssignProcessToJobObject + let process = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid) + .map_err(|e| Error::other(e.message()))?; + + // Assign the process to the job + let result = AssignProcessToJobObject(self.0, process); + + // Close our handle to the process - the job object maintains its own reference + let _ = CloseHandle(process); + + result.map_err(|e| Error::other(e.message())) + } + } +} + +impl Drop for JobObject { + fn drop(&mut self) { + unsafe { + // When this handle is closed and it's the last handle to the job, + // Windows will terminate all processes in the job due to KILL_ON_JOB_CLOSE + let _ = CloseHandle(self.0); + } + } +} + +/// Holds the Windows Job Object that ensures child processes are killed when the app exits. +/// On Windows, when the job object handle is closed (including on crash), all assigned +/// processes are automatically terminated by the OS. +#[cfg(windows)] +pub struct JobObjectState { + job: Mutex>, + error: Mutex>, +} + +#[cfg(windows)] +impl JobObjectState { + pub fn new() -> Self { + match JobObject::new() { + Ok(job) => Self { + job: Mutex::new(Some(job)), + error: Mutex::new(None), + }, + Err(e) => { + eprintln!("Failed to create job object: {e}"); + Self { + job: Mutex::new(None), + error: Mutex::new(Some(format!("Failed to create job object: {e}"))), + } + } + } + } + + pub fn assign_pid(&self, pid: u32) { + if let Some(job) = self.job.lock().unwrap().as_ref() { + if let Err(e) = job.assign_pid(pid) { + eprintln!("Failed to assign process {pid} to job object: {e}"); + *self.error.lock().unwrap() = + Some(format!("Failed to assign process to job object: {e}")); + } else { + println!("Assigned process {pid} to job object for automatic cleanup"); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_job_object_creation() { + let job = JobObject::new(); + assert!(job.is_ok(), "Failed to create job object: {:?}", job.err()); + } +} diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index e2682ec71..183220d16 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -1,9 +1,13 @@ mod cli; +#[cfg(windows)] +mod job_object; mod window_customizer; use cli::{install_cli, sync_cli}; use futures::FutureExt; use futures::future; +#[cfg(windows)] +use job_object::*; use std::{ collections::VecDeque, net::TcpListener, @@ -251,6 +255,9 @@ pub fn run() { // Initialize log state app.manage(LogState(Arc::new(Mutex::new(VecDeque::new())))); + #[cfg(windows)] + app.manage(JobObjectState::new()); + let primary_monitor = app.primary_monitor().ok().flatten(); let size = primary_monitor .map(|m| m.size().to_logical(m.scale_factor())) @@ -303,7 +310,14 @@ pub fn run() { let res = match setup_server_connection(&app, custom_url).await { Ok((child, url)) => { + #[cfg(windows)] + if let Some(child) = &child { + let job_state = app.state::(); + job_state.assign_pid(child.pid()); + } + app.state::().set_child(child); + Ok(url) } Err(e) => Err(e), From 1facf7d8e41b2fcd98542f111df5c80fdf5af332 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jan 2026 12:05:22 +0000 Subject: [PATCH 155/470] ignore: update download stats 2026-01-13 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index f0354e19f..ac4b788ba 100644 --- a/STATS.md +++ b/STATS.md @@ -199,3 +199,4 @@ | 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) | | 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) | | 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) | +| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) | From f3b7d2f7860aedec1a4c86511ed799d32474c32c Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 13 Jan 2026 06:55:21 -0600 Subject: [PATCH 156/470] fix(app): file search --- packages/ui/src/hooks/use-filtered-list.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx index b6bd7d5c6..1b3be4b4c 100644 --- a/packages/ui/src/hooks/use-filtered-list.tsx +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -22,18 +22,10 @@ export function useFilteredList(props: FilteredListProps) { const empty: Group[] = [] const [grouped, { refetch }] = createResource( - () => { - // When items is a function (not async filter function), call it to track changes - const itemsValue = - typeof props.items === "function" - ? (props.items as () => T[])() // Call synchronous function to track it - : props.items - - return { - filter: store.filter, - items: itemsValue, - } - }, + () => ({ + filter: store.filter, + items: typeof props.items === "function" ? undefined : props.items, + }), async ({ filter, items }) => { const needle = filter?.toLowerCase() const all = (items ?? (await (props.items as (filter: string) => T[] | Promise)(needle))) || [] From 35cb06e0e49647c91c733b44eaf8fd1d3ead420b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 13 Jan 2026 21:08:19 +0800 Subject: [PATCH 157/470] fix(app): provide pty socket auth if available from desktop (#8210) --- packages/app/src/app.tsx | 2 +- packages/app/src/components/terminal.tsx | 9 ++++++--- packages/desktop/src-tauri/src/lib.rs | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 3f8080972..d0678dc53 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -33,7 +33,7 @@ const Loading = () =>
    Duration::from_secs(7) { + if timestamp.elapsed() > Duration::from_secs(30) { break Err(format!( "Failed to spawn OpenCode Server. Logs:\n{}", get_logs(app.clone()).await.unwrap() From b01eec38d1b0bec375f46a562f8a39f9c72e53ff Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 13 Jan 2026 21:12:00 +0800 Subject: [PATCH 158/470] fix(desktop): set serverPassword --- packages/desktop/src/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 1ad015aff..5ae6047b3 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -304,7 +304,9 @@ render(() => { )} {(data) => { - setServerPassword(data().password) + setServerPassword(data().password); + window.__OPENCODE__ ??= {}; + window.__OPENCODE__.serverPassword = data().password ?? undefined; return }} From a03daa42526f55cbab64cb9bf3407111b899f16b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jan 2026 13:13:45 +0000 Subject: [PATCH 159/470] chore: generate --- packages/desktop/src/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 5ae6047b3..5d699bb90 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -304,9 +304,9 @@ render(() => { )} {(data) => { - setServerPassword(data().password); - window.__OPENCODE__ ??= {}; - window.__OPENCODE__.serverPassword = data().password ?? undefined; + setServerPassword(data().password) + window.__OPENCODE__ ??= {} + window.__OPENCODE__.serverPassword = data().password ?? undefined return }} From 736cd10847c4af2e4ef9ba7b9eadd1e6352ff2f9 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:16:57 +0100 Subject: [PATCH 160/470] fix(ui): track memo-based items in useFilteredList without affecting async function based lists (#8216) Co-authored-by: neriousy --- packages/ui/src/hooks/use-filtered-list.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx index 1b3be4b4c..e265fffef 100644 --- a/packages/ui/src/hooks/use-filtered-list.tsx +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -24,7 +24,11 @@ export function useFilteredList(props: FilteredListProps) { const [grouped, { refetch }] = createResource( () => ({ filter: store.filter, - items: typeof props.items === "function" ? undefined : props.items, + items: typeof props.items === "function" + ? props.items.length === 0 + ? (props.items as () => T[])() + : undefined + : props.items, }), async ({ filter, items }) => { const needle = filter?.toLowerCase() From 067338bc256213909c59f5a37118d2f8099ca5f2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jan 2026 14:17:35 +0000 Subject: [PATCH 161/470] chore: generate --- packages/ui/src/hooks/use-filtered-list.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx index e265fffef..26215e93c 100644 --- a/packages/ui/src/hooks/use-filtered-list.tsx +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -24,11 +24,12 @@ export function useFilteredList(props: FilteredListProps) { const [grouped, { refetch }] = createResource( () => ({ filter: store.filter, - items: typeof props.items === "function" - ? props.items.length === 0 - ? (props.items as () => T[])() - : undefined - : props.items, + items: + typeof props.items === "function" + ? props.items.length === 0 + ? (props.items as () => T[])() + : undefined + : props.items, }), async ({ filter, items }) => { const needle = filter?.toLowerCase() From 3c5a256f0fe620743f9e1a8f7d1546b87950aa5e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 13 Jan 2026 22:38:24 +0800 Subject: [PATCH 162/470] desktop: macos killall opencode-cli on launch --- packages/desktop/src-tauri/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 4bb7b21ec..75ddb6566 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -223,6 +223,11 @@ async fn check_server_health(url: &str, password: Option<&str>) -> bool { pub fn run() { let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); + #[cfg(target_os = "macos")] + let _ = std::process::Command::new("killall") + .arg("opencode-cli") + .output(); + let mut builder = tauri::Builder::default() .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { // Focus existing window when another instance is launched From 29bf731d47da1cda99de2c9890d525045b1bc8e8 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 13 Jan 2026 14:41:54 +0000 Subject: [PATCH 163/470] release: v1.1.17 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 10001bb61..daeb370fd 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -99,7 +99,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -126,7 +126,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -150,7 +150,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -174,7 +174,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -203,7 +203,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -232,7 +232,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -248,7 +248,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.16", + "version": "1.1.17", "bin": { "opencode": "./bin/opencode", }, @@ -351,7 +351,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -371,7 +371,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.16", + "version": "1.1.17", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -382,7 +382,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -395,7 +395,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -435,7 +435,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "zod": "catalog:", }, @@ -446,7 +446,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index bef67c82c..305cb7a12 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.16", + "version": "1.1.17", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 9557f8310..61982f58a 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.16", + "version": "1.1.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index ecfb20007..4385fd87c 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.1.16", + "version": "1.1.17", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 8f3b1ddee..bc9ce254c 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.16", + "version": "1.1.17", "$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 9572cfde8..b53cd3171 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.16", + "version": "1.1.17", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 5cf2b20db..477806f33 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.16", + "version": "1.1.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 7dcdb574d..259a00b6c 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.16", + "version": "1.1.17", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 6ccac0c10..4c4365b75 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.16" +version = "1.1.17" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.16/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index cc8ae0f18..4d109d370 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.16", + "version": "1.1.17", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 07fee7d73..8a3d925c1 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.16", + "version": "1.1.17", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d1848b4a3..fc2db6a51 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.1.16", + "version": "1.1.17", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index ca24d02aa..98e46ac3c 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.1.16", + "version": "1.1.17", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 1b2d90166..dfc322fb7 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.16", + "version": "1.1.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index b88c747e1..e9159b73b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.16", + "version": "1.1.17", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index fcd980b4a..bcbfc0d31 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.16", + "version": "1.1.17", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 5a657fe0d..adafa8540 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.1.16", + "version": "1.1.17", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 2bfb97a33..dab321752 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.1.16", + "version": "1.1.17", "publisher": "sst-dev", "repository": { "type": "git", From c86c2acf4c0e0f9945b3dd83a1a32e1eb9783c86 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 13 Jan 2026 09:57:43 -0500 Subject: [PATCH 164/470] add fullscreen view to permission prompt --- AGENTS.md | 6 +- STYLE_GUIDE.md | 21 +- .../cli/cmd/tui/component/prompt/index.tsx | 40 ++-- .../cli/cmd/tui/routes/session/permission.tsx | 212 +++++++++++------- 4 files changed, 162 insertions(+), 117 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 87d59d4c9..3138f6c5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -- To test opencode in the `packages/opencode` directory you can run `bun dev` -- To regenerate the javascript SDK, run ./packages/sdk/js/script/build.ts +- To test opencode in `packages/opencode`, run `bun dev`. +- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`. - ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. -- the default branch in this repo is `dev` +- The default branch in this repo is `dev`. diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index a46ce221f..52d012fcb 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -1,19 +1,16 @@ ## Style Guide -- Try to keep things in one function unless composable or reusable -- AVOID unnecessary destructuring of variables. instead of doing `const { a, b } -= obj` just reference it as obj.a and obj.b. this preserves context -- AVOID `try`/`catch` where possible -- AVOID using `any` type -- PREFER single word variable names where possible -- Use as many bun apis as possible like Bun.file() +- Keep things in one function unless composable or reusable +- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context +- Avoid `try`/`catch` where possible +- Avoid using the `any` type +- Prefer single word variable names where possible +- Use Bun APIs when possible, like `Bun.file()` # Avoid let statements -we don't like let statements, especially combined with if/else statements. -prefer const - -This is bad: +We don't like `let` statements, especially combined with if/else statements. +Prefer `const`. Good: @@ -32,7 +29,7 @@ else foo = 2 # Avoid else statements -Prefer early returns or even using `iife` to avoid else statements +Prefer early returns or using an `iife` to avoid else statements. Good: diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index d5e0a0aa2..9ad85d08f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -563,25 +563,27 @@ export function Prompt(props: PromptProps) { })), }) } else { - sdk.client.session.prompt({ - sessionID, - ...selectedModel, - messageID, - agent: local.agent.current().name, - model: selectedModel, - variant, - parts: [ - { - id: Identifier.ascending("part"), - type: "text", - text: inputText, - }, - ...nonTextParts.map((x) => ({ - id: Identifier.ascending("part"), - ...x, - })), - ], - }) + sdk.client.session + .prompt({ + sessionID, + ...selectedModel, + messageID, + agent: local.agent.current().name, + model: selectedModel, + variant, + parts: [ + { + id: Identifier.ascending("part"), + type: "text", + text: inputText, + }, + ...nonTextParts.map((x) => ({ + id: Identifier.ascending("part"), + ...x, + })), + ], + }) + .catch(() => {}) } history.append({ ...store.prompt, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index f5b6badb5..c95b42260 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -1,6 +1,6 @@ import { createStore } from "solid-js/store" import { createMemo, For, Match, Show, Switch } from "solid-js" -import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid" +import { Portal, useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import type { TextareaRenderable } from "@opentui/core" import { useKeybind } from "../../context/keybind" import { useTheme, selectedForeground } from "../../context/theme" @@ -11,6 +11,7 @@ import { useSync } from "../../context/sync" import { useTextareaKeybindings } from "../../component/textarea-keybindings" import path from "path" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" +import { Keybind } from "@/util/keybind" import { Locale } from "@/util/locale" type PermissionStage = "permission" | "always" | "reject" @@ -32,7 +33,9 @@ function filetype(input?: string) { } function EditBody(props: { request: PermissionRequest }) { - const { theme, syntax } = useTheme() + const themeState = useTheme() + const theme = themeState.theme + const syntax = themeState.syntax const sync = useSync() const dimensions = useTerminalDimensions() @@ -54,7 +57,7 @@ function EditBody(props: { request: PermissionRequest }) { Edit {normalizePath(filepath())} - + - + ) @@ -172,86 +175,95 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { message: message || undefined, }) }} - onCancel={() => setStore("stage", "permission")} + onCancel={() => { + setStore("stage", "permission") + }} /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } - options={{ once: "Allow once", always: "Allow always", reject: "Reject" }} - escapeKey="reject" - onSelect={(option) => { - if (option === "always") { - setStore("stage", "always") - return - } - if (option === "reject") { - if (session()?.parentID) { - setStore("stage", "reject") - return + {(() => { + const body = ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } - sdk.client.permission.reply({ - reply: "reject", - requestID: props.request.id, - }) - } - sdk.client.permission.reply({ - reply: "once", - requestID: props.request.id, - }) - }} - /> + options={{ once: "Allow once", always: "Allow always", reject: "Reject" }} + escapeKey="reject" + fullscreen + onSelect={(option) => { + if (option === "always") { + setStore("stage", "always") + return + } + if (option === "reject") { + if (session()?.parentID) { + setStore("stage", "reject") + return + } + sdk.client.permission.reply({ + reply: "reject", + requestID: props.request.id, + }) + } + sdk.client.permission.reply({ + reply: "once", + requestID: props.request.id, + }) + }} + /> + ) + + return body + })()} ) @@ -327,14 +339,18 @@ function Prompt>(props: { body: JSX.Element options: T escapeKey?: keyof T + fullscreen?: boolean onSelect: (option: keyof T) => void }) { const { theme } = useTheme() const keybind = useKeybind() + const dimensions = useTerminalDimensions() const keys = Object.keys(props.options) as (keyof T)[] const [store, setStore] = createStore({ selected: keys[0], + expanded: false, }) + const diffKey = Keybind.parse("ctrl+f")[0] useKeyboard((evt) => { if (evt.name === "left" || evt.name == "h") { @@ -360,17 +376,36 @@ function Prompt>(props: { evt.preventDefault() props.onSelect(props.escapeKey) } + + if (props.fullscreen && diffKey && Keybind.match(diffKey, keybind.parse(evt))) { + evt.preventDefault() + evt.stopPropagation() + setStore("expanded", (v) => !v) + } }) - return ( + const hint = createMemo(() => (store.expanded ? "minimize" : "fullscreen")) + const renderer = useRenderer() + + const content = () => ( - - + + {"△"} {props.title} @@ -403,6 +438,11 @@ function Prompt>(props: { + + + {"ctrl+f"} {hint()} + + {"⇆"} select @@ -413,4 +453,10 @@ function Prompt>(props: { ) + + return ( + {content()}}> + {content()} + + ) } From 7d0b52dc29b7c7bc10bd2defe8452400db76bb9c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jan 2026 14:58:51 +0000 Subject: [PATCH 165/470] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index fc2db6a51..7b0507716 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 98e46ac3c..bd89818d1 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 2b77a84c4f3c6e651e0cf224b0c814260ddd8a45 Mon Sep 17 00:00:00 2001 From: usvimal Date: Tue, 13 Jan 2026 23:39:43 +0800 Subject: [PATCH 166/470] fix(desktop): correct health check endpoint URL to /global/health (#8231) --- packages/desktop/src-tauri/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 75ddb6566..0d5b585e8 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -198,7 +198,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32, password: &str) -> CommandChild { } async fn check_server_health(url: &str, password: Option<&str>) -> bool { - let health_url = format!("{}/health", url.trim_end_matches('/')); + let health_url = format!("{}/global/health", url.trim_end_matches('/')); let client = reqwest::Client::builder() .timeout(Duration::from_secs(3)) .build(); From 528291532b1d4192302538f9c27054ff717c6982 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Tue, 13 Jan 2026 12:41:35 -0300 Subject: [PATCH 167/470] feat(desktop): Adding Provider Icons (#8215) --- .../app/src/components/dialog-select-model.tsx | 11 ++++++++++- packages/app/src/components/prompt-input.tsx | 18 ++++++++++++++++-- packages/ui/src/components/button.css | 3 +-- packages/ui/src/components/list.tsx | 12 +++++++++--- packages/ui/src/components/session-turn.tsx | 10 +++++++++- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index d54f9369a..c614c2d49 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -7,6 +7,8 @@ import { Button } from "@opencode-ai/ui/button" import { Tag } from "@opencode-ai/ui/tag" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import type { IconName } from "@opencode-ai/ui/icons/provider" import { DialogSelectProvider } from "./dialog-select-provider" import { DialogManageModels } from "./dialog-manage-models" @@ -35,6 +37,12 @@ const ModelList: Component<{ filterKeys={["provider.name", "name", "id"]} sortBy={(a, b) => a.name.localeCompare(b.name)} groupBy={(x) => x.provider.name} + groupHeader={(group) => ( +
    + + {group.category} +
    + )} sortGroupsBy={(a, b) => { if (a.category === "Recent" && b.category !== "Recent") return -1 if (b.category === "Recent" && a.category !== "Recent") return 1 @@ -52,7 +60,8 @@ const ModelList: Component<{ }} > {(i) => ( -
    +
    + {i.name} Free diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 13f2b00a3..2be8a21c1 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -33,6 +33,8 @@ import { useSync } from "@/context/sync" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import type { IconName } from "@opencode-ai/ui/icons/provider" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" @@ -1560,6 +1562,12 @@ export const PromptInput: Component = (props) => { fallback={ @@ -1569,6 +1577,12 @@ export const PromptInput: Component = (props) => { @@ -1583,10 +1597,10 @@ export const PromptInput: Component = (props) => { > diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css index 800795e87..c25b89af9 100644 --- a/packages/ui/src/components/button.css +++ b/packages/ui/src/components/button.css @@ -123,13 +123,13 @@ &[data-size="normal"] { height: 24px; + line-height: 24px; padding: 0 6px; &[data-icon] { padding: 0 12px 0 4px; } font-size: var(--font-size-small); - line-height: var(--line-height-large); gap: 6px; /* text-12-medium */ @@ -137,7 +137,6 @@ font-size: var(--font-size-small); font-style: normal; font-weight: var(--font-weight-medium); - line-height: var(--line-height-large); /* 166.667% */ letter-spacing: var(--letter-spacing-normal); } diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 1283b3023..8c92728d7 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -10,9 +10,15 @@ export interface ListSearchProps { autofocus?: boolean } +export interface ListGroup { + category: string + items: T[] +} + export interface ListProps extends FilteredListProps { class?: string children: (item: T) => JSX.Element + groupHeader?: (group: ListGroup) => JSX.Element emptyMessage?: string onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void onMove?: (item: T | undefined) => void @@ -116,7 +122,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) setScrollRef, }) - function GroupHeader(props: { category: string }): JSX.Element { + function GroupHeader(groupProps: { category: string; children?: JSX.Element }): JSX.Element { const [stuck, setStuck] = createSignal(false) const [header, setHeader] = createSignal(undefined) @@ -138,7 +144,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) return (
    - {props.category} + {groupProps.children ?? groupProps.category}
    ) } @@ -185,7 +191,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) {(group) => (
    - + {props.groupHeader?.(group)}
    diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 9947578b9..ae1321bac 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -22,6 +22,8 @@ import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" import { FileIcon } from "./file-icon" import { Icon } from "./icon" +import { ProviderIcon } from "./provider-icon" +import type { IconName } from "./provider-icons/types" import { IconButton } from "./icon-button" import { Tooltip } from "./tooltip" import { Card } from "./card" @@ -498,7 +500,13 @@ export function SessionTurn( {(msg() as UserMessage).agent} - {(msg() as UserMessage).model?.modelID} + + + {(msg() as UserMessage).model?.modelID} + {(msg() as UserMessage).variant || "default"}
    From 20b52cad2add67fa49155b49cfe641d7c89715eb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jan 2026 15:42:58 +0000 Subject: [PATCH 168/470] chore: generate --- packages/app/src/components/prompt-input.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 2be8a21c1..f1ca3ee88 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1563,10 +1563,7 @@ export const PromptInput: Component = (props) => { + ) + }} + + +
    + + + +
    +
    + {question()?.question} + {multi() ? " (select all that apply)" : ""} +
    +
    + + {(opt, i) => { + const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false + return ( + + ) + }} + + + +
    + setTimeout(() => el.focus(), 0)} + type="text" + data-slot="custom-input" + placeholder="Type your answer..." + value={input()} + onInput={(e) => { + const inputs = [...store.custom] + inputs[store.tab] = e.currentTarget.value + setStore("custom", inputs) + }} + /> + + +
    +
    +
    +
    +
    + + +
    +
    Review your answers
    + + {(q, index) => { + const value = () => store.answers[index()]?.join(", ") ?? "" + const answered = () => Boolean(value()) + return ( +
    + {q.question} + + {answered() ? value() : "(not answered)"} + +
    + ) + }} +
    +
    +
    + +
    + + + + + + + + + +
    +
    + ) +} diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index acab99fe8..dcb9adb39 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -1,4 +1,13 @@ -import type { Message, Session, Part, FileDiff, SessionStatus, PermissionRequest } from "@opencode-ai/sdk/v2" +import type { + Message, + Session, + Part, + FileDiff, + SessionStatus, + PermissionRequest, + QuestionRequest, + QuestionAnswer, +} from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" @@ -16,6 +25,9 @@ type Data = { permission?: { [sessionID: string]: PermissionRequest[] } + question?: { + [sessionID: string]: QuestionRequest[] + } message: { [sessionID: string]: Message[] } @@ -30,6 +42,10 @@ export type PermissionRespondFn = (input: { response: "once" | "always" | "reject" }) => void +export type QuestionReplyFn = (input: { requestID: string; answers: QuestionAnswer[] }) => void + +export type QuestionRejectFn = (input: { requestID: string }) => void + export type NavigateToSessionFn = (sessionID: string) => void export const { use: useData, provider: DataProvider } = createSimpleContext({ @@ -38,6 +54,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ data: Data directory: string onPermissionRespond?: PermissionRespondFn + onQuestionReply?: QuestionReplyFn + onQuestionReject?: QuestionRejectFn onNavigateToSession?: NavigateToSessionFn }) => { return { @@ -48,6 +66,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return props.directory }, respondToPermission: props.onPermissionRespond, + replyToQuestion: props.onQuestionReply, + rejectQuestion: props.onQuestionReject, navigateToSession: props.onNavigateToSession, } }, From f24251f89e277cab2669730dc1e028573e0fe082 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Jan 2026 13:36:37 -0500 Subject: [PATCH 176/470] sync --- bun.lock | 6 + packages/console/app/package.json | 2 + packages/console/app/src/config.ts | 6 + .../app/src/routes/api/black/setup-intent.ts | 30 ++ .../console/app/src/routes/auth/authorize.ts | 11 +- .../console/app/src/routes/auth/callback.ts | 2 + .../src/routes/{black/index.css => black.css} | 266 ++++++++++++++- packages/console/app/src/routes/black.tsx | 166 +++++++++ .../console/app/src/routes/black/common.tsx | 42 +++ .../console/app/src/routes/black/index.tsx | 318 ++++-------------- .../app/src/routes/black/subscribe.tsx | 244 ++++++++++++++ 11 files changed, 828 insertions(+), 265 deletions(-) create mode 100644 packages/console/app/src/routes/api/black/setup-intent.ts rename packages/console/app/src/routes/{black/index.css => black.css} (62%) create mode 100644 packages/console/app/src/routes/black.tsx create mode 100644 packages/console/app/src/routes/black/common.tsx create mode 100644 packages/console/app/src/routes/black/subscribe.tsx diff --git a/bun.lock b/bun.lock index 10001bb61..563c13a33 100644 --- a/bun.lock +++ b/bun.lock @@ -84,10 +84,12 @@ "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", "@solidjs/start": "catalog:", + "@stripe/stripe-js": "8.6.1", "chart.js": "4.5.1", "nitro": "3.0.1-alpha.1", "solid-js": "catalog:", "solid-list": "0.3.0", + "solid-stripe": "0.8.1", "vite": "catalog:", "zod": "catalog:", }, @@ -1652,6 +1654,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@stripe/stripe-js": ["@stripe/stripe-js@8.6.1", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="], + "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], @@ -3528,6 +3532,8 @@ "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], + "solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="], + "solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 9557f8310..23171daac 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -23,10 +23,12 @@ "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", "@solidjs/start": "catalog:", + "@stripe/stripe-js": "8.6.1", "chart.js": "4.5.1", "nitro": "3.0.1-alpha.1", "solid-js": "catalog:", "solid-list": "0.3.0", + "solid-stripe": "0.8.1", "vite": "catalog:", "zod": "catalog:" }, diff --git a/packages/console/app/src/config.ts b/packages/console/app/src/config.ts index 4ebb2c71a..4396e5117 100644 --- a/packages/console/app/src/config.ts +++ b/packages/console/app/src/config.ts @@ -26,4 +26,10 @@ export const config = { commits: "6,500", monthlyUsers: "650,000", }, + + // Stripe + stripe: { + publishableKey: + "pk_live_51OhXSKEclFNgdHcR9dDfYGwQeKuPfKo0IjA5kWBQIXKMFhE8QFd9bYLdPZC6klRKEgEkxJYSKuZg9U3FKHdLnF4300F9qLqMgP", + }, } as const diff --git a/packages/console/app/src/routes/api/black/setup-intent.ts b/packages/console/app/src/routes/api/black/setup-intent.ts new file mode 100644 index 000000000..eb5571616 --- /dev/null +++ b/packages/console/app/src/routes/api/black/setup-intent.ts @@ -0,0 +1,30 @@ +import type { APIEvent } from "@solidjs/start/server" +import { Billing } from "@opencode-ai/console-core/billing.js" + +export async function POST(event: APIEvent) { + try { + const body = (await event.request.json()) as { plan: string } + const plan = body.plan + + if (!plan || !["20", "100", "200"].includes(plan)) { + return Response.json({ error: "Invalid plan" }, { status: 400 }) + } + + const amount = parseInt(plan) * 100 + + const intent = await Billing.stripe().setupIntents.create({ + payment_method_types: ["card"], + metadata: { + plan, + amount: amount.toString(), + }, + }) + + return Response.json({ + clientSecret: intent.client_secret, + }) + } catch (error) { + console.error("Error creating setup intent:", error) + return Response.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/packages/console/app/src/routes/auth/authorize.ts b/packages/console/app/src/routes/auth/authorize.ts index 166466ef8..6be94b146 100644 --- a/packages/console/app/src/routes/auth/authorize.ts +++ b/packages/console/app/src/routes/auth/authorize.ts @@ -2,6 +2,13 @@ import type { APIEvent } from "@solidjs/start/server" import { AuthClient } from "~/context/auth" export async function GET(input: APIEvent) { - const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code") - return Response.redirect(result.url, 302) + const url = new URL(input.request.url) + // TODO + // input.request.url http://localhost:3001/auth/authorize?continue=/black/subscribe + const result = await AuthClient.authorize( + new URL("/callback/subscribe?foo=bar", input.request.url).toString(), + "code", + ) + // result.url https://auth.frank.dev.opencode.ai/authorize?client_id=app&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fauth%2Fcallback&response_type=code&state=0d3fc834-bcbc-42dc-83ab-c25c2c43c7e3 + return Response.redirect(result.url + "&continue=" + url.searchParams.get("continue"), 302) } diff --git a/packages/console/app/src/routes/auth/callback.ts b/packages/console/app/src/routes/auth/callback.ts index 9b7296791..b03bbdbe5 100644 --- a/packages/console/app/src/routes/auth/callback.ts +++ b/packages/console/app/src/routes/auth/callback.ts @@ -5,6 +5,8 @@ import { useAuthSession } from "~/context/auth" export async function GET(input: APIEvent) { const url = new URL(input.request.url) + console.log("=C=", input.request.url) + throw new Error("Not implemented") try { const code = url.searchParams.get("code") if (!code) throw new Error("No code found") diff --git a/packages/console/app/src/routes/black/index.css b/packages/console/app/src/routes/black.css similarity index 62% rename from packages/console/app/src/routes/black/index.css rename to packages/console/app/src/routes/black.css index 418598792..dfb188ed0 100644 --- a/packages/console/app/src/routes/black/index.css +++ b/packages/console/app/src/routes/black.css @@ -36,24 +36,73 @@ width: 100%; flex-grow: 1; - [data-slot="hero-black"] { - margin-top: 110px; + [data-slot="hero"] { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 8px; + margin-top: 40px; + padding: 0 20px; @media (min-width: 768px) { - margin-top: 150px; + margin-top: 60px; + } + + h1 { + color: rgba(255, 255, 255, 0.92); + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 160%; + margin: 0; + + @media (min-width: 768px) { + font-size: 24px; + } + } + + p { + color: rgba(255, 255, 255, 0.59); + font-size: 15px; + font-style: normal; + font-weight: 400; + line-height: 160%; + margin: 0; + + @media (min-width: 768px) { + font-size: 18px; + } + } + } + + [data-slot="hero-black"] { + margin-top: 40px; + padding: 0 20px; + + @media (min-width: 768px) { + margin-top: 60px; + } + + svg { + width: 100%; + max-width: 540px; + height: auto; + filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.1)); } } [data-slot="cta"] { display: flex; flex-direction: column; - gap: 32px; + gap: 16px; align-items: center; text-align: center; - margin-top: -18px; + margin-top: -40px; + width: 100%; @media (min-width: 768px) { - margin-top: 40px; + margin-top: -20px; } [data-slot="heading"] { @@ -328,6 +377,211 @@ } } } + + /* Subscribe page styles */ + [data-slot="subscribe-form"] { + display: flex; + flex-direction: column; + gap: 32px; + align-items: center; + margin-top: -18px; + width: 100%; + max-width: 540px; + padding: 0 20px; + + @media (min-width: 768px) { + margin-top: 40px; + padding: 0; + } + + [data-slot="form-card"] { + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + padding: 24px; + display: flex; + flex-direction: column; + gap: 20px; + } + + [data-slot="plan-header"] { + display: flex; + flex-direction: column; + gap: 8px; + } + + [data-slot="title"] { + color: rgba(255, 255, 255, 0.92); + font-size: 16px; + font-weight: 400; + margin-bottom: 8px; + } + + [data-slot="icon"] { + color: rgba(255, 255, 255, 0.59); + } + + [data-slot="price"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + } + + [data-slot="amount"] { + color: rgba(255, 255, 255, 0.92); + font-size: 24px; + font-weight: 500; + } + + [data-slot="period"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + + [data-slot="multiplier"] { + color: rgba(255, 255, 255, 0.39); + font-size: 14px; + + &::before { + content: "·"; + margin: 0 8px; + } + } + + [data-slot="divider"] { + height: 1px; + background: rgba(255, 255, 255, 0.17); + } + + [data-slot="section-title"] { + color: rgba(255, 255, 255, 0.92); + font-size: 16px; + font-weight: 400; + } + + [data-slot="checkout-form"] { + display: flex; + flex-direction: column; + gap: 20px; + } + + [data-slot="error"] { + color: #ff6b6b; + font-size: 14px; + } + + [data-slot="submit-button"] { + width: 100%; + height: 48px; + background: rgba(255, 255, 255, 0.92); + border: none; + border-radius: 4px; + color: #000; + font-family: var(--font-mono); + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s ease; + + &:hover:not(:disabled) { + background: #e0e0e0; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + [data-slot="charge-notice"] { + color: #d4a500; + font-size: 14px; + text-align: center; + } + + [data-slot="loading"] { + display: flex; + justify-content: center; + padding: 40px 0; + + p { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + } + + [data-slot="fine-print"] { + color: rgba(255, 255, 255, 0.39); + text-align: center; + font-size: 13px; + font-style: italic; + + a { + color: rgba(255, 255, 255, 0.39); + text-decoration: underline; + } + } + + [data-slot="workspace-picker"] { + [data-slot="workspace-list"] { + width: 100%; + padding: 0; + margin: 0; + list-style: none; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + align-self: stretch; + outline: none; + overflow-y: auto; + max-height: 240px; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + [data-slot="workspace-item"] { + width: 100%; + display: flex; + padding: 8px 12px; + align-items: center; + gap: 8px; + align-self: stretch; + cursor: pointer; + + [data-slot="selected-icon"] { + visibility: hidden; + color: rgba(255, 255, 255, 0.39); + font-family: "IBM Plex Mono", monospace; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; + } + + span:last-child { + color: rgba(255, 255, 255, 0.92); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; + } + + &:hover, + &[data-active="true"] { + background: #161616; + + [data-slot="selected-icon"] { + visibility: visible; + } + } + } + } + } + } } [data-component="footer"] { diff --git a/packages/console/app/src/routes/black.tsx b/packages/console/app/src/routes/black.tsx new file mode 100644 index 000000000..5a5b139dd --- /dev/null +++ b/packages/console/app/src/routes/black.tsx @@ -0,0 +1,166 @@ +import { A, createAsync, RouteSectionProps } from "@solidjs/router" +import { createMemo } from "solid-js" +import { github } from "~/lib/github" +import { config } from "~/config" +import "./black.css" + +export default function BlackLayout(props: RouteSectionProps) { + const githubData = createAsync(() => github()) + const starCount = createMemo(() => + githubData()?.stars + ? new Intl.NumberFormat("en-US", { + notation: "compact", + compactDisplay: "short", + }).format(githubData()!.stars!) + : config.github.starsFormatted.compact, + ) + + return ( +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +

    Access all the world's best coding models

    +

    Including Claude, GPT, Gemini and more

    +
    +
    + + + + + + + + + + + + + + + + + + +
    + {props.children} +
    + +
    + ) +} diff --git a/packages/console/app/src/routes/black/common.tsx b/packages/console/app/src/routes/black/common.tsx new file mode 100644 index 000000000..c1184bd20 --- /dev/null +++ b/packages/console/app/src/routes/black/common.tsx @@ -0,0 +1,42 @@ +import { Match, Switch } from "solid-js" + +export const plans = [ + { id: "20", amount: 20, multiplier: null }, + { id: "100", amount: 100, multiplier: "6x more usage than Black 20" }, + { id: "200", amount: 200, multiplier: "21x more usage than Black 20" }, +] as const + +export type Plan = (typeof plans)[number] + +export function PlanIcon(props: { plan: string }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index f5a375adf..2b452c812 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -1,276 +1,80 @@ -import { A, createAsync, useSearchParams } from "@solidjs/router" -import "./index.css" +import { A, useSearchParams } from "@solidjs/router" import { Title } from "@solidjs/meta" -import { github } from "~/lib/github" import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js" -import { config } from "~/config" - -const plans = [ - { id: "20", amount: 20, multiplier: null }, - { id: "100", amount: 100, multiplier: "6x more usage than Black 20" }, - { id: "200", amount: 200, multiplier: "21x more usage than Black 20" }, -] as const - -function PlanIcon(props: { plan: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} +import { PlanIcon, plans } from "./common" export default function Black() { const [params] = useSearchParams() - const [selected, setSelected] = createSignal(params.plan as string | null) + const [selected, setSelected] = createSignal((params.plan as string) || null) const selectedPlan = createMemo(() => plans.find((p) => p.id === selected())) - const githubData = createAsync(() => github()) - const starCount = createMemo(() => - githubData()?.stars - ? new Intl.NumberFormat("en-US", { - notation: "compact", - compactDisplay: "short", - }).format(githubData()!.stars!) - : config.github.starsFormatted.compact, - ) - return ( -
    + <> opencode -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    -
    -

    - Access all the world's best coding models -

    -

    Including Claude, GPT, Gemini, and more

    -
    - - -
    - - {(plan) => ( - - )} - -
    -

    - Prices shown don't include applicable tax · Terms of Service -

    -
    - - {(plan) => ( -
    -
    +
    + + +
    + + {(plan) => ( + - - Continue - -
    + + )} + +
    +

    + Prices shown don't include applicable tax · Terms of Service +

    + + + {(plan) => ( +
    +
    +
    +
    -

    - Prices shown don't include applicable tax · Terms of Service +

    + ${plan().amount}{" "} + per person billed monthly + + {plan().multiplier} +

    +
      +
    • Your subscription will not start immediately
    • +
    • You will be added to the waitlist and activated soon
    • +
    • Your card will be only charged when your subscription is activated
    • +
    • Usage limits apply, heavily automated use may reach limits sooner
    • +
    • Subscriptions for individuals, contact Enterprise for teams
    • +
    • Limits may be adjusted and plans may be discontinued in the future
    • +
    • Cancel your subscription at anytime
    • +
    +
    + + + Continue + +
    - )} - - -
    -
    - -
    +

    + Prices shown don't include applicable tax · Terms of Service +

    +
    + )} + + + + ) } diff --git a/packages/console/app/src/routes/black/subscribe.tsx b/packages/console/app/src/routes/black/subscribe.tsx new file mode 100644 index 000000000..00ce19ef6 --- /dev/null +++ b/packages/console/app/src/routes/black/subscribe.tsx @@ -0,0 +1,244 @@ +import { A, createAsync, query, redirect, useSearchParams } from "@solidjs/router" +import { Title } from "@solidjs/meta" +import { createEffect, createSignal, For, onMount, Show } from "solid-js" +import { loadStripe } from "@stripe/stripe-js" +import { Elements, PaymentElement, useStripe, useElements } from "solid-stripe" +import { config } from "~/config" +import { PlanIcon, plans } from "./common" +import { getActor } from "~/context/auth" +import { withActor } from "~/context/auth.withActor" +import { Actor } from "@opencode-ai/console-core/actor.js" +import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" +import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { createList } from "solid-list" +import { Modal } from "~/component/modal" + +const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record + +const getWorkspaces = query(async () => { + "use server" + const actor = await getActor() + if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe") + return withActor(async () => { + return Database.use((tx) => + tx + .select({ + id: WorkspaceTable.id, + name: WorkspaceTable.name, + slug: WorkspaceTable.slug, + }) + .from(UserTable) + .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id)) + .where( + and( + eq(UserTable.accountID, Actor.account()), + isNull(WorkspaceTable.timeDeleted), + isNull(UserTable.timeDeleted), + ), + ), + ) + }) +}, "black.subscribe.workspaces") + +function CheckoutForm(props: { plan: string; amount: number }) { + const stripe = useStripe() + const elements = useElements() + const [error, setError] = createSignal(null) + const [loading, setLoading] = createSignal(false) + + const handleSubmit = async (e: Event) => { + e.preventDefault() + if (!stripe() || !elements()) return + + setLoading(true) + setError(null) + + const result = await elements()!.submit() + if (result.error) { + setError(result.error.message ?? "An error occurred") + setLoading(false) + return + } + + const { error: confirmError } = await stripe()!.confirmSetup({ + elements: elements()!, + confirmParams: { + return_url: `${window.location.origin}/black/success?plan=${props.plan}`, + }, + }) + + if (confirmError) { + setError(confirmError.message ?? "An error occurred") + } + setLoading(false) + } + + return ( +
    + + +

    {error()}

    +
    + +

    You will only be charged when your subscription is activated

    + + ) +} + +export default function BlackSubscribe() { + const workspaces = createAsync(() => getWorkspaces()) + const [selectedWorkspace, setSelectedWorkspace] = createSignal(null) + + const [params] = useSearchParams() + const plan = (params.plan as string) || "200" + const planData = plansMap[plan] || plansMap["200"] + + const [clientSecret, setClientSecret] = createSignal(null) + const [stripePromise] = createSignal(loadStripe(config.stripe.publishableKey)) + + // Auto-select if only one workspace + createEffect(() => { + const ws = workspaces() + if (ws?.length === 1 && !selectedWorkspace()) { + setSelectedWorkspace(ws[0].id) + } + }) + + // Keyboard navigation for workspace picker + const { active, setActive, onKeyDown } = createList({ + items: () => workspaces()?.map((w) => w.id) ?? [], + initialActive: null, + }) + + const handleSelectWorkspace = (id: string) => { + setSelectedWorkspace(id) + } + + onMount(async () => { + const response = await fetch("/api/black/setup-intent", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ plan }), + }) + const data = await response.json() + if (data.clientSecret) { + setClientSecret(data.clientSecret) + } + }) + + let listRef: HTMLUListElement | undefined + + // Show workspace picker if multiple workspaces and none selected + const showWorkspacePicker = () => { + const ws = workspaces() + return ws && ws.length > 1 && !selectedWorkspace() + } + + return ( + <> + Subscribe to OpenCode Black +
    +
    +
    +

    Subscribe to OpenCode Black

    +
    + +
    +

    + ${planData.amount} per month + + {planData.multiplier} + +

    +
    +
    +

    Add payment method

    + +

    Loading payment form...

    +
    + } + > + + + + +
    + + {/* Workspace picker modal */} + {}} title="Select a workspace for this plan"> +
    +
      { + if (e.key === "Enter" && active()) { + handleSelectWorkspace(active()!) + } else { + onKeyDown(e) + } + }} + > + + {(workspace) => ( +
    • setActive(workspace.id)} + onClick={() => handleSelectWorkspace(workspace.id)} + > + [*] + {workspace.name || workspace.slug} +
    • + )} +
      +
    +
    +
    +

    + Prices shown don't include applicable tax · Terms of Service +

    +
    + + ) +} From 8ae10f1c9462a583b4de1d0c6170e260f95d3020 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Jan 2026 13:37:48 -0500 Subject: [PATCH 177/470] sync --- packages/console/app/src/routes/auth/callback.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/console/app/src/routes/auth/callback.ts b/packages/console/app/src/routes/auth/callback.ts index b03bbdbe5..9b7296791 100644 --- a/packages/console/app/src/routes/auth/callback.ts +++ b/packages/console/app/src/routes/auth/callback.ts @@ -5,8 +5,6 @@ import { useAuthSession } from "~/context/auth" export async function GET(input: APIEvent) { const url = new URL(input.request.url) - console.log("=C=", input.request.url) - throw new Error("Not implemented") try { const code = url.searchParams.get("code") if (!code) throw new Error("No code found") From 80e1173ef7907e978e36314a0d936de418be2903 Mon Sep 17 00:00:00 2001 From: Daniel Sauer <81422812+sauerdaniel@users.noreply.github.com> Date: Tue, 13 Jan 2026 19:38:34 +0100 Subject: [PATCH 178/470] fix(mcp): close existing client before reassignment to prevent leaks (#8253) --- packages/opencode/src/mcp/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index aca0c6631..4e0968391 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -266,6 +266,13 @@ export namespace MCP { status: s.status, } } + // Close existing client if present to prevent memory leaks + const existingClient = s.clients[name] + if (existingClient) { + await existingClient.close().catch((error) => { + log.error("Failed to close existing MCP client", { name, error }) + }) + } s.clients[name] = result.mcpClient s.status[name] = result.status @@ -523,6 +530,13 @@ export namespace MCP { const s = await state() s.status[name] = result.status if (result.mcpClient) { + // Close existing client if present to prevent memory leaks + const existingClient = s.clients[name] + if (existingClient) { + await existingClient.close().catch((error) => { + log.error("Failed to close existing MCP client", { name, error }) + }) + } s.clients[name] = result.mcpClient } } From b7a1d8f2f5540d6451b93ef6c425314408b17645 Mon Sep 17 00:00:00 2001 From: Github Action Date: Tue, 13 Jan 2026 18:39:01 +0000 Subject: [PATCH 179/470] Update Nix flake.lock and x86_64-linux hash --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 4c953c5a8..7e3d51ab5 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { - "x86_64-linux": "sha256-UCPTTk4b7d2bets7KgCeYBHWAUwUAPUyKm+xDYkSexE=", + "x86_64-linux": "sha256-e3pcCRHba4B5aYIvdteL+PYW2KHO6Ry1qO4DoMn+erE=", "aarch64-darwin": "sha256-Y3o6lovahSWoG9un/l1qxu7hCmIlZXm2LxOLKNiPQfQ=" } } From b68a4a883819f841cba623d8dca531bc94268f63 Mon Sep 17 00:00:00 2001 From: Daniel Sauer <81422812+sauerdaniel@users.noreply.github.com> Date: Tue, 13 Jan 2026 19:43:16 +0100 Subject: [PATCH 180/470] fix(state): delete key from recordsByKey on instance disposal (#8252) --- packages/opencode/src/project/state.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index c1ac23c5d..34a5dbb3e 100644 --- a/packages/opencode/src/project/state.ts +++ b/packages/opencode/src/project/state.ts @@ -58,6 +58,7 @@ export namespace State { tasks.push(task) } entries.clear() + recordsByKey.delete(key) await Promise.all(tasks) disposalFinished = true log.info("state disposal completed", { key }) From f3d4dd5099003070800cc9ec161877634fdd7c0a Mon Sep 17 00:00:00 2001 From: Github Action Date: Tue, 13 Jan 2026 18:43:58 +0000 Subject: [PATCH 181/470] Update aarch64-darwin hash --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 7e3d51ab5..1dbc18960 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { "x86_64-linux": "sha256-e3pcCRHba4B5aYIvdteL+PYW2KHO6Ry1qO4DoMn+erE=", - "aarch64-darwin": "sha256-Y3o6lovahSWoG9un/l1qxu7hCmIlZXm2LxOLKNiPQfQ=" + "aarch64-darwin": "sha256-xF9TVBw8aYloNbQLLd19ywwdPIHyS12ktMPhzO+cYx0=" } } From 5947fe72e412311746c1fd8937035b8a5c5b4b37 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Tue, 13 Jan 2026 10:58:09 -0800 Subject: [PATCH 182/470] docs: document ~/.claude/CLAUDE.md compatibility behavior (#8268) --- packages/web/src/content/docs/rules.mdx | 29 ++++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/web/src/content/docs/rules.mdx b/packages/web/src/content/docs/rules.mdx index 2d02ff47f..3a170019a 100644 --- a/packages/web/src/content/docs/rules.mdx +++ b/packages/web/src/content/docs/rules.mdx @@ -3,7 +3,7 @@ title: Rules description: Set custom instructions for opencode. --- -You can provide custom instructions to opencode by creating an `AGENTS.md` file. This is similar to `CLAUDE.md` or Cursor's rules. It contains instructions that will be included in the LLM's context to customize its behavior for your specific project. +You can provide custom instructions to opencode by creating an `AGENTS.md` file. This is similar to Cursor's rules. It contains instructions that will be included in the LLM's context to customize its behavior for your specific project. --- @@ -58,7 +58,7 @@ opencode also supports reading the `AGENTS.md` file from multiple locations. And ### Project -The ones we have seen above, where the `AGENTS.md` is placed in the project root, are project-specific rules. These only apply when you are working in this directory or its sub-directories. +Place an `AGENTS.md` in your project root for project-specific rules. These only apply when you are working in this directory or its sub-directories. ### Global @@ -66,16 +66,33 @@ You can also have global rules in a `~/.config/opencode/AGENTS.md` file. This ge Since this isn't committed to Git or shared with your team, we recommend using this to specify any personal rules that the LLM should follow. +### Claude Code Compatibility + +For users migrating from Claude Code, OpenCode supports Claude Code's file conventions as fallbacks: + +- **Project rules**: `CLAUDE.md` in your project directory (used if no `AGENTS.md` exists) +- **Global rules**: `~/.claude/CLAUDE.md` (used if no `~/.config/opencode/AGENTS.md` exists) +- **Skills**: `~/.claude/skills/` — see [Agent Skills](/docs/skills/) for details + +To disable Claude Code compatibility, set one of these environment variables: + +```bash +export OPENCODE_DISABLE_CLAUDE_CODE=1 # Disable all .claude support +export OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1 # Disable only ~/.claude/CLAUDE.md +export OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1 # Disable only .claude/skills +``` + --- ## Precedence -So when opencode starts, it looks for: +When opencode starts, it looks for rule files in this order: -1. **Local files** by traversing up from the current directory -2. **Global file** by checking `~/.config/opencode/AGENTS.md` +1. **Local files** by traversing up from the current directory (`AGENTS.md`, `CLAUDE.md`, or `CONTEXT.md`) +2. **Global file** at `~/.config/opencode/AGENTS.md` +3. **Claude Code file** at `~/.claude/CLAUDE.md` (unless disabled) -If you have both global and project-specific rules, opencode will combine them together. +The first matching file wins in each category. For example, if you have both `AGENTS.md` and `CLAUDE.md`, only `AGENTS.md` is used. Similarly, `~/.config/opencode/AGENTS.md` takes precedence over `~/.claude/CLAUDE.md`. --- From 05867f9318498e7ec817d365f7300dd135f77c38 Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Tue, 13 Jan 2026 20:21:39 +0100 Subject: [PATCH 183/470] feat: Add GitLab Duo Agentic Chat Provider Support (#7333) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline --- bun.lock | 37 +++ packages/opencode/package.json | 1 + packages/opencode/src/plugin/index.ts | 7 +- packages/opencode/src/provider/provider.ts | 41 +++ .../test/provider/amazon-bedrock.test.ts | 7 +- .../opencode/test/provider/gitlab-duo.test.ts | 286 ++++++++++++++++++ packages/web/src/content/docs/providers.mdx | 93 ++++++ 7 files changed, 470 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/test/provider/gitlab-duo.test.ts diff --git a/bun.lock b/bun.lock index 0a2868811..a537fa661 100644 --- a/bun.lock +++ b/bun.lock @@ -276,6 +276,7 @@ "@ai-sdk/vercel": "1.0.31", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", + "@gitlab/gitlab-ai-provider": "3.1.0", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", @@ -586,6 +587,10 @@ "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="], + + "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], + "@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="], "@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="], @@ -906,6 +911,10 @@ "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], + "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-S0MVXsogrwbOboA/8L0CY5sBXg2HrrO8gdeUeHd9yLZDPsggFD0FzcSuzO5vBO6geUOpruRa8Hqrbb6WWu7Frw=="], + + "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], "@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="], @@ -1600,6 +1609,8 @@ "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="], "@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="], @@ -2318,6 +2329,10 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -2540,6 +2555,10 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], + + "graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="], + "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], @@ -2768,6 +2787,8 @@ "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], + "iterate-iterator": ["iterate-iterator@1.0.2", "", {}, "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw=="], "iterate-value": ["iterate-value@1.0.2", "", { "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" } }, "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ=="], @@ -2800,6 +2821,8 @@ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -3076,6 +3099,8 @@ "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="], + "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -3518,6 +3543,10 @@ "smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="], + "socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], + + "socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="], + "solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="], "solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="], @@ -3682,6 +3711,8 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], @@ -3874,6 +3905,8 @@ "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -4024,6 +4057,8 @@ "@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], + "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], @@ -4266,6 +4301,8 @@ "editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="], + "engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "esbuild-plugin-copy/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index f2c95d0b3..c0c4e79b6 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -70,6 +70,7 @@ "@ai-sdk/vercel": "1.0.31", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", + "@gitlab/gitlab-ai-provider": "3.1.0", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index b0c9eee2c..8ce6dfd3c 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -14,7 +14,11 @@ import { NamedError } from "@opencode-ai/util/error" export namespace Plugin { const log = Log.create({ service: "plugin" }) - const BUILTIN = ["opencode-copilot-auth@0.0.12", "opencode-anthropic-auth@0.0.8"] + const BUILTIN = [ + "opencode-copilot-auth@0.0.12", + "opencode-anthropic-auth@0.0.8", + "@gitlab/opencode-gitlab-auth@1.3.0", + ] // Built-in plugins that are directly imported (not installed from npm) const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin] @@ -46,6 +50,7 @@ export namespace Plugin { if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) { plugins.push(...BUILTIN) } + for (let plugin of plugins) { // ignore old codex plugin since it is supported first party now if (plugin.includes("opencode-openai-codex-auth")) continue diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 3b76b1e02..9bde1333e 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,4 +1,6 @@ import z from "zod" +import path from "path" +import os from "os" import fuzzysort from "fuzzysort" import { Config } from "../config/config" import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" @@ -35,6 +37,7 @@ import { createGateway } from "@ai-sdk/gateway" import { createTogetherAI } from "@ai-sdk/togetherai" import { createPerplexity } from "@ai-sdk/perplexity" import { createVercel } from "@ai-sdk/vercel" +import { createGitLab } from "@gitlab/gitlab-ai-provider" import { ProviderTransform } from "./transform" export namespace Provider { @@ -60,6 +63,7 @@ export namespace Provider { "@ai-sdk/togetherai": createTogetherAI, "@ai-sdk/perplexity": createPerplexity, "@ai-sdk/vercel": createVercel, + "@gitlab/gitlab-ai-provider": createGitLab, // @ts-ignore (TODO: kill this code so we dont have to maintain it) "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, } @@ -390,6 +394,43 @@ export namespace Provider { }, } }, + async gitlab(input) { + const instanceUrl = Env.get("GITLAB_INSTANCE_URL") || "https://gitlab.com" + + const auth = await Auth.get(input.id) + const apiKey = await (async () => { + if (auth?.type === "oauth") return auth.access + if (auth?.type === "api") return auth.key + return Env.get("GITLAB_TOKEN") + })() + + const config = await Config.get() + const providerConfig = config.provider?.["gitlab"] + + return { + autoload: !!apiKey, + options: { + instanceUrl, + apiKey, + featureFlags: { + duo_agent_platform_agentic_chat: true, + duo_agent_platform: true, + ...(providerConfig?.options?.featureFlags || {}), + }, + }, + async getModel(sdk: ReturnType, modelID: string, options?: { anthropicModel?: string }) { + const anthropicModel = options?.anthropicModel + return sdk.agenticChat(modelID, { + anthropicModel, + featureFlags: { + duo_agent_platform_agentic_chat: true, + duo_agent_platform: true, + ...(providerConfig?.options?.featureFlags || {}), + }, + }) + }, + } + }, "cloudflare-ai-gateway": async (input) => { const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID") const gateway = Env.get("CLOUDFLARE_GATEWAY_ID") diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index d10e85139..05f5bd01f 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -9,7 +9,11 @@ import path from "path" mock.module("../../src/bun/index", () => ({ BunProc: { - install: async (pkg: string) => pkg, + install: async (pkg: string, _version?: string) => { + // Return package name without version for mocking + const lastAtIndex = pkg.lastIndexOf("@") + return lastAtIndex > 0 ? pkg.substring(0, lastAtIndex) : pkg + }, run: async () => { throw new Error("BunProc.run should not be called in tests") }, @@ -28,6 +32,7 @@ mock.module("@aws-sdk/credential-providers", () => ({ const mockPlugin = () => ({}) mock.module("opencode-copilot-auth", () => ({ default: mockPlugin })) mock.module("opencode-anthropic-auth", () => ({ default: mockPlugin })) +mock.module("@gitlab/opencode-gitlab-auth", () => ({ default: mockPlugin })) // Import after mocks are set up const { tmpdir } = await import("../fixture/fixture") diff --git a/packages/opencode/test/provider/gitlab-duo.test.ts b/packages/opencode/test/provider/gitlab-duo.test.ts new file mode 100644 index 000000000..4d5aa9c74 --- /dev/null +++ b/packages/opencode/test/provider/gitlab-duo.test.ts @@ -0,0 +1,286 @@ +import { test, expect, mock } from "bun:test" +import path from "path" + +// === Mocks === +// These mocks prevent real package installations during tests + +mock.module("../../src/bun/index", () => ({ + BunProc: { + install: async (pkg: string, _version?: string) => { + // Return package name without version for mocking + const lastAtIndex = pkg.lastIndexOf("@") + return lastAtIndex > 0 ? pkg.substring(0, lastAtIndex) : pkg + }, + run: async () => { + throw new Error("BunProc.run should not be called in tests") + }, + which: () => process.execPath, + InstallFailedError: class extends Error {}, + }, +})) + +const mockPlugin = () => ({}) +mock.module("opencode-copilot-auth", () => ({ default: mockPlugin })) +mock.module("opencode-anthropic-auth", () => ({ default: mockPlugin })) +mock.module("@gitlab/opencode-gitlab-auth", () => ({ default: mockPlugin })) + +// Import after mocks are set up +const { tmpdir } = await import("../fixture/fixture") +const { Instance } = await import("../../src/project/instance") +const { Provider } = await import("../../src/provider/provider") +const { Env } = await import("../../src/env") +const { Global } = await import("../../src/global") + +test("GitLab Duo: loads provider with API key from environment", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("GITLAB_TOKEN", "test-gitlab-token") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["gitlab"]).toBeDefined() + expect(providers["gitlab"].key).toBe("test-gitlab-token") + }, + }) +}) + +test("GitLab Duo: config instanceUrl option sets baseURL", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + gitlab: { + options: { + instanceUrl: "https://gitlab.example.com", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("GITLAB_TOKEN", "test-token") + Env.set("GITLAB_INSTANCE_URL", "https://gitlab.example.com") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["gitlab"]).toBeDefined() + expect(providers["gitlab"].options?.instanceUrl).toBe("https://gitlab.example.com") + }, + }) +}) + +test("GitLab Duo: loads with OAuth token from auth.json", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + + const authPath = path.join(Global.Path.data, "auth.json") + await Bun.write( + authPath, + JSON.stringify({ + gitlab: { + type: "oauth", + access: "test-access-token", + refresh: "test-refresh-token", + expires: Date.now() + 3600000, + }, + }), + ) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("GITLAB_TOKEN", "") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["gitlab"]).toBeDefined() + }, + }) +}) + +test("GitLab Duo: loads with Personal Access Token from auth.json", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + + const authPath2 = path.join(Global.Path.data, "auth.json") + await Bun.write( + authPath2, + JSON.stringify({ + gitlab: { + type: "api", + key: "glpat-test-pat-token", + }, + }), + ) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("GITLAB_TOKEN", "") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["gitlab"]).toBeDefined() + expect(providers["gitlab"].key).toBe("glpat-test-pat-token") + }, + }) +}) + +test("GitLab Duo: supports self-hosted instance configuration", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + gitlab: { + options: { + instanceUrl: "https://gitlab.company.internal", + apiKey: "glpat-internal-token", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("GITLAB_INSTANCE_URL", "https://gitlab.company.internal") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["gitlab"]).toBeDefined() + expect(providers["gitlab"].options?.instanceUrl).toBe("https://gitlab.company.internal") + }, + }) +}) + +test("GitLab Duo: config apiKey takes precedence over environment variable", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + gitlab: { + options: { + apiKey: "config-token", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("GITLAB_TOKEN", "env-token") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["gitlab"]).toBeDefined() + }, + }) +}) + +test("GitLab Duo: supports feature flags configuration", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + gitlab: { + options: { + featureFlags: { + duo_agent_platform_agentic_chat: true, + duo_agent_platform: true, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("GITLAB_TOKEN", "test-token") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["gitlab"]).toBeDefined() + expect(providers["gitlab"].options?.featureFlags).toBeDefined() + expect(providers["gitlab"].options?.featureFlags?.duo_agent_platform_agentic_chat).toBe(true) + }, + }) +}) + +test("GitLab Duo: has multiple agentic chat models available", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("GITLAB_TOKEN", "test-token") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["gitlab"]).toBeDefined() + const models = Object.keys(providers["gitlab"].models) + expect(models.length).toBeGreaterThan(0) + expect(models).toContain("duo-chat-haiku-4-5") + expect(models).toContain("duo-chat-sonnet-4-5") + expect(models).toContain("duo-chat-opus-4-5") + }, + }) +}) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 80c6f89e1..7af4ab85d 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -557,6 +557,99 @@ Cloudflare AI Gateway lets you access models from OpenAI, Anthropic, Workers AI, --- +### GitLab Duo + +GitLab Duo provides AI-powered agentic chat with native tool calling capabilities through GitLab's Anthropic proxy. + +1. Run the `/connect` command and select GitLab. + + ```txt + /connect + ``` + +2. Choose your authentication method: + + ```txt + ┌ Select auth method + │ + │ OAuth (Recommended) + │ Personal Access Token + └ + ``` + + #### Using OAuth (Recommended) + + Select **OAuth** and your browser will open for authorization. + + #### Using Personal Access Token + 1. Go to [GitLab User Settings > Access Tokens](https://gitlab.com/-/user_settings/personal_access_tokens) + 2. Click **Add new token** + 3. Name: `OpenCode`, Scopes: `api` + 4. Copy the token (starts with `glpat-`) + 5. Enter it in the terminal + +3. Run the `/models` command to see available models. + + ```txt + /models + ``` + + Three Claude-based models are available: + - **duo-chat-haiku-4-5** (Default) - Fast responses for quick tasks + - **duo-chat-sonnet-4-5** - Balanced performance for most workflows + - **duo-chat-opus-4-5** - Most capable for complex analysis + +##### Self-Hosted GitLab + +For self-hosted GitLab instances: + +```bash +GITLAB_INSTANCE_URL=https://gitlab.company.com GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx opencode +``` + +Or add to your bash profile: + +```bash title="~/.bash_profile" +export GITLAB_INSTANCE_URL=https://gitlab.company.com +export GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx +``` + +##### Configuration + +Customize through `opencode.json`: + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "gitlab": { + "options": { + "instanceUrl": "https://gitlab.com", + "featureFlags": { + "duo_agent_platform_agentic_chat": true, + "duo_agent_platform": true + } + } + } + } +} +``` + +##### GitLab API Tools (Optional) + +To access GitLab tools (merge requests, issues, pipelines, CI/CD, etc.): + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "plugin": ["@gitlab/opencode-gitlab-plugin"] +} +``` + +This plugin provides comprehensive GitLab repository management capabilities including MR reviews, issue tracking, pipeline monitoring, and more. + +--- + ### GitHub Copilot To use your GitHub Copilot subscription with opencode: From 797a56873dd70c52caec607dbd6d239af5c92d18 Mon Sep 17 00:00:00 2001 From: Dillon Mulroy Date: Tue, 13 Jan 2026 14:22:26 -0500 Subject: [PATCH 184/470] fix(cli): mcp auth duplicate radio button icon (#8273) --- packages/opencode/src/cli/cmd/mcp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index cfb54081f..cdd741fbc 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -21,7 +21,7 @@ function getAuthStatusIcon(status: MCP.AuthStatus): string { case "expired": return "⚠" case "not_authenticated": - return "○" + return "✗" } } From 1258f7aeea53fa99efdb722407dd1c80bed4dbd8 Mon Sep 17 00:00:00 2001 From: Github Action Date: Tue, 13 Jan 2026 19:22:48 +0000 Subject: [PATCH 185/470] Update Nix flake.lock and x86_64-linux hash --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 4c953c5a8..0bf4aa627 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { - "x86_64-linux": "sha256-UCPTTk4b7d2bets7KgCeYBHWAUwUAPUyKm+xDYkSexE=", + "x86_64-linux": "sha256-x6A/XT1i3bjakfAj0A1wV4n2s9rpflMDceTeppdP6tE=", "aarch64-darwin": "sha256-Y3o6lovahSWoG9un/l1qxu7hCmIlZXm2LxOLKNiPQfQ=" } } From 3a750b08090a593ae49a90cf262049c9f4d45bfd Mon Sep 17 00:00:00 2001 From: Github Action Date: Tue, 13 Jan 2026 19:29:19 +0000 Subject: [PATCH 186/470] Update aarch64-darwin hash --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 0bf4aa627..a25b9376e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { "x86_64-linux": "sha256-x6A/XT1i3bjakfAj0A1wV4n2s9rpflMDceTeppdP6tE=", - "aarch64-darwin": "sha256-Y3o6lovahSWoG9un/l1qxu7hCmIlZXm2LxOLKNiPQfQ=" + "aarch64-darwin": "sha256-RkamQYbpjJqpHHf76em9lPgeI9k4/kaCf7T+4xHaizY=" } } From 96ae5925c324767662ee2a76a1ec866ba9bf3bc0 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 13 Jan 2026 13:33:58 -0600 Subject: [PATCH 187/470] tweak: ensure external dir and bash tool invocations render workdir details --- .../src/cli/cmd/tui/routes/session/index.tsx | 29 ++++++++++++++++++- .../cli/cmd/tui/routes/session/permission.tsx | 18 +++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 10e340d7f..f87b811ae 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -69,6 +69,7 @@ import { Footer } from "./footer.tsx" import { usePromptRef } from "../../context/prompt" import { useExit } from "../../context/exit" import { Filesystem } from "@/util/filesystem" +import { Global } from "@/global" import { PermissionPrompt } from "./permission" import { QuestionPrompt } from "./question" import { DialogExportOptions } from "../../ui/dialog-export-options" @@ -1525,6 +1526,7 @@ function BlockTool(props: { title: string; children: JSX.Element; onClick?: () = function Bash(props: ToolProps) { const { theme } = useTheme() + const sync = useSync() const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? "")) const [expanded, setExpanded] = createSignal(false) const lines = createMemo(() => output().split("\n")) @@ -1534,11 +1536,36 @@ function Bash(props: ToolProps) { return [...lines().slice(0, 10), "…"].join("\n") }) + const workdirDisplay = createMemo(() => { + const workdir = props.input.workdir + if (!workdir || workdir === ".") return undefined + + const base = sync.data.path.directory + if (!base) return undefined + + const absolute = path.resolve(base, workdir) + if (absolute === base) return undefined + + const home = Global.Path.home + if (!home) return absolute + + const match = absolute === home || absolute.startsWith(home + path.sep) + return match ? absolute.replace(home, "~") : absolute + }) + + const title = createMemo(() => { + const desc = props.input.description ?? "Shell" + const wd = workdirDisplay() + if (!wd) return `# ${desc}` + if (desc.includes(wd)) return `# ${desc}` + return `# ${desc} in ${wd}` + }) + return ( setExpanded((prev) => !prev) : undefined} > diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index c95b42260..9cde65d2e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -226,7 +226,23 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { - + {(() => { + const meta = props.request.metadata ?? {} + const parent = typeof meta["parentDir"] === "string" ? meta["parentDir"] : undefined + const filepath = typeof meta["filepath"] === "string" ? meta["filepath"] : undefined + const pattern = props.request.patterns?.[0] + const derived = + typeof pattern === "string" + ? pattern.includes("*") + ? path.dirname(pattern) + : pattern + : undefined + + const raw = parent ?? filepath ?? derived + const dir = normalizePath(raw) + + return + })()} From 33ba064c40925670b4b4a1286e2afd5ef26df862 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 13 Jan 2026 13:52:09 -0600 Subject: [PATCH 188/470] tweak: external dir permission rendering in tui --- .../cli/cmd/tui/routes/session/permission.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index 9cde65d2e..eab2adb10 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -13,15 +13,26 @@ import path from "path" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { Keybind } from "@/util/keybind" import { Locale } from "@/util/locale" +import { Global } from "@/global" type PermissionStage = "permission" | "always" | "reject" function normalizePath(input?: string) { if (!input) return "" - if (path.isAbsolute(input)) { - return path.relative(process.cwd(), input) || "." + + const cwd = process.cwd() + const home = Global.Path.home + const absolute = path.isAbsolute(input) ? input : path.resolve(cwd, input) + const relative = path.relative(cwd, absolute) + + if (!relative) return "." + if (!relative.startsWith("..")) return relative + + // outside cwd - use ~ or absolute + if (home && (absolute === home || absolute.startsWith(home + path.sep))) { + return absolute.replace(home, "~") } - return input + return absolute } function filetype(input?: string) { From 1550ae47c0beb02a6d3a7162a00465c0207ac50f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 13 Jan 2026 13:57:34 -0600 Subject: [PATCH 189/470] add family to gpt 5.2 codex in codex plugin --- packages/opencode/src/plugin/codex.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 4e2b28379..91e66197f 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -387,6 +387,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { headers: {}, release_date: "2025-12-18", variants: {} as Record>, + family: "gpt-codex", } model.variants = ProviderTransform.variants(model) provider.models["gpt-5.2-codex"] = model From 66b7a4991ee5903d0239c0d7b98c95b9c5f9e43c Mon Sep 17 00:00:00 2001 From: Joe Harrison <22684038+josephbharrison@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:06:38 -0500 Subject: [PATCH 190/470] fix(prompt-input): handle Shift+Enter before IME check to prevent stuck state (#8275) --- packages/app/src/components/prompt-input.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index f1ca3ee88..2f85652a9 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -364,6 +364,12 @@ export const PromptInput: Component = (props) => { if (!isFocused()) setStore("popover", null) }) + // Safety: reset composing state on focus change to prevent stuck state + // This handles edge cases where compositionend event may not fire + createEffect(() => { + if (!isFocused()) setComposing(false) + }) + type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string } const agentList = createMemo(() => @@ -881,6 +887,14 @@ export const PromptInput: Component = (props) => { } } + // Handle Shift+Enter BEFORE IME check - Shift+Enter is never used for IME input + // and should always insert a newline regardless of composition state + if (event.key === "Enter" && event.shiftKey) { + addPart({ type: "text", content: "\n", start: 0, end: 0 }) + event.preventDefault() + return + } + if (event.key === "Enter" && isImeComposing(event)) { return } @@ -944,11 +958,7 @@ export const PromptInput: Component = (props) => { return } - if (event.key === "Enter" && event.shiftKey) { - addPart({ type: "text", content: "\n", start: 0, end: 0 }) - event.preventDefault() - return - } + // Note: Shift+Enter is handled earlier, before IME check if (event.key === "Enter" && !event.shiftKey) { handleSubmit(event) } From 0a3c72d6787aa3cf39b9517e32f0ad5d8dbb6184 Mon Sep 17 00:00:00 2001 From: Dax Date: Tue, 13 Jan 2026 15:55:48 -0500 Subject: [PATCH 191/470] feat: add plan mode with enter/exit tools (#8281) --- .../plans/1768330644696-gentle-harbor.md | 320 ++++++++++++++++++ packages/opencode/src/agent/agent.ts | 6 +- .../src/cli/cmd/tui/routes/session/index.tsx | 17 + .../cli/cmd/tui/routes/session/question.tsx | 61 ++-- packages/opencode/src/flag/flag.ts | 1 + packages/opencode/src/question/index.ts | 1 + packages/opencode/src/session/index.ts | 9 + packages/opencode/src/session/prompt.ts | 143 +++++++- .../src/session/prompt/build-switch.txt | 2 + packages/opencode/src/tool/plan-enter.txt | 14 + packages/opencode/src/tool/plan-exit.txt | 13 + packages/opencode/src/tool/plan.ts | 130 +++++++ packages/opencode/src/tool/registry.ts | 2 + packages/opencode/test/util/lock.test.ts | 72 ++++ packages/sdk/js/src/v2/gen/types.gen.ts | 5 + packages/util/src/slug.ts | 74 ++++ 16 files changed, 824 insertions(+), 46 deletions(-) create mode 100644 .opencode/plans/1768330644696-gentle-harbor.md create mode 100644 packages/opencode/src/tool/plan-enter.txt create mode 100644 packages/opencode/src/tool/plan-exit.txt create mode 100644 packages/opencode/src/tool/plan.ts create mode 100644 packages/opencode/test/util/lock.test.ts create mode 100644 packages/util/src/slug.ts diff --git a/.opencode/plans/1768330644696-gentle-harbor.md b/.opencode/plans/1768330644696-gentle-harbor.md new file mode 100644 index 000000000..9e3e668b4 --- /dev/null +++ b/.opencode/plans/1768330644696-gentle-harbor.md @@ -0,0 +1,320 @@ +# Plan: Implement enter_plan and exit_plan Tools + +## Summary + +The plan mode workflow in `prompt.ts` references `exit_plan` tool that doesn't exist. We need to implement two tools: + +1. **`exit_plan`** - Called when the AI finishes planning; uses the Question module to ask the user if they want to switch to build mode (yes/no). **Only available in plan mode.** If user says yes, creates a synthetic user message with the "build" agent to trigger the mode switch in the loop. +2. **`enter_plan`** - Called to enter plan mode. **Only available in build mode.** If user says yes, creates a synthetic user message with the "plan" agent. + +## Key Insight: How Mode Switching Works + +Looking at `prompt.ts:455-478`, the session loop determines the current agent from the last user message's `agent` field (line 510: `const agent = await Agent.get(lastUser.agent)`). + +To switch modes, we need to: + +1. Ask the user for confirmation +2. If confirmed, create a synthetic user message with the **new agent** specified +3. The loop will pick up this new user message and use the new agent + +## Files to Modify + +| File | Action | +| ------------------------------------------ | --------------------------------------------------------------- | +| `packages/opencode/src/tool/plan.ts` | **CREATE** - New file with both tools | +| `packages/opencode/src/tool/exitplan.txt` | **CREATE** - Description for exit_plan tool | +| `packages/opencode/src/tool/enterplan.txt` | **CREATE** - Description for enter_plan tool | +| `packages/opencode/src/tool/registry.ts` | **MODIFY** - Register the new tools | +| `packages/opencode/src/agent/agent.ts` | **MODIFY** - Add permission rules to restrict tool availability | + +## Implementation Details + +### 1. Create `packages/opencode/src/tool/plan.ts` + +```typescript +import z from "zod" +import { Tool } from "./tool" +import { Question } from "../question" +import { Session } from "../session" +import { MessageV2 } from "../session/message-v2" +import { Identifier } from "../id/id" +import { Provider } from "../provider/provider" +import EXIT_DESCRIPTION from "./exitplan.txt" +import ENTER_DESCRIPTION from "./enterplan.txt" + +export const ExitPlanTool = Tool.define("exit_plan", { + description: EXIT_DESCRIPTION, + parameters: z.object({}), + async execute(_params, ctx) { + const answers = await Question.ask({ + sessionID: ctx.sessionID, + questions: [ + { + question: "Planning is complete. Would you like to switch to build mode and start implementing?", + header: "Build Mode", + options: [ + { label: "Yes", description: "Switch to build mode and start implementing the plan" }, + { label: "No", description: "Stay in plan mode to continue refining the plan" }, + ], + }, + ], + tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined, + }) + + const answer = answers[0]?.[0] + const shouldSwitch = answer === "Yes" + + // If user wants to switch, create a synthetic user message with the new agent + if (shouldSwitch) { + // Get model from the last user message in the session + const model = await getLastModel(ctx.sessionID) + + const userMsg: MessageV2.User = { + id: Identifier.ascending("message"), + sessionID: ctx.sessionID, + role: "user", + time: { + created: Date.now(), + }, + agent: "build", // Switch to build agent + model, + } + await Session.updateMessage(userMsg) + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: userMsg.id, + sessionID: ctx.sessionID, + type: "text", + text: "User has approved the plan. Switch to build mode and begin implementing the plan.", + synthetic: true, + } satisfies MessageV2.TextPart) + } + + return { + title: shouldSwitch ? "Switching to build mode" : "Staying in plan mode", + output: shouldSwitch + ? "User confirmed to switch to build mode. A new message has been created to switch you to build mode. Begin implementing the plan." + : "User chose to stay in plan mode. Continue refining the plan or address any concerns.", + metadata: { + switchToBuild: shouldSwitch, + answer, + }, + } + }, +}) + +export const EnterPlanTool = Tool.define("enter_plan", { + description: ENTER_DESCRIPTION, + parameters: z.object({}), + async execute(_params, ctx) { + const answers = await Question.ask({ + sessionID: ctx.sessionID, + questions: [ + { + question: + "Would you like to switch to plan mode? In plan mode, the AI will only research and create a plan without making changes.", + header: "Plan Mode", + options: [ + { label: "Yes", description: "Switch to plan mode for research and planning" }, + { label: "No", description: "Stay in build mode to continue making changes" }, + ], + }, + ], + tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined, + }) + + const answer = answers[0]?.[0] + const shouldSwitch = answer === "Yes" + + // If user wants to switch, create a synthetic user message with the new agent + if (shouldSwitch) { + const model = await getLastModel(ctx.sessionID) + + const userMsg: MessageV2.User = { + id: Identifier.ascending("message"), + sessionID: ctx.sessionID, + role: "user", + time: { + created: Date.now(), + }, + agent: "plan", // Switch to plan agent + model, + } + await Session.updateMessage(userMsg) + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: userMsg.id, + sessionID: ctx.sessionID, + type: "text", + text: "User has requested to enter plan mode. Switch to plan mode and begin planning.", + synthetic: true, + } satisfies MessageV2.TextPart) + } + + return { + title: shouldSwitch ? "Switching to plan mode" : "Staying in build mode", + output: shouldSwitch + ? "User confirmed to switch to plan mode. A new message has been created to switch you to plan mode. Begin planning." + : "User chose to stay in build mode. Continue with the current task.", + metadata: { + switchToPlan: shouldSwitch, + answer, + }, + } + }, +}) + +// Helper to get the model from the last user message +async function getLastModel(sessionID: string) { + for await (const item of MessageV2.stream(sessionID)) { + if (item.info.role === "user" && item.info.model) return item.info.model + } + return Provider.defaultModel() +} +``` + +### 2. Create `packages/opencode/src/tool/exitplan.txt` + +``` +Use this tool when you have completed the planning phase and are ready to exit plan mode. + +This tool will ask the user if they want to switch to build mode to start implementing the plan. + +Call this tool: +- After you have written a complete plan to the plan file +- After you have clarified any questions with the user +- When you are confident the plan is ready for implementation + +Do NOT call this tool: +- Before you have created or finalized the plan +- If you still have unanswered questions about the implementation +- If the user has indicated they want to continue planning +``` + +### 3. Create `packages/opencode/src/tool/enterplan.txt` + +``` +Use this tool to suggest entering plan mode when the user's request would benefit from planning before implementation. + +This tool will ask the user if they want to switch to plan mode. + +Call this tool when: +- The user's request is complex and would benefit from planning first +- You want to research and design before making changes +- The task involves multiple files or significant architectural decisions + +Do NOT call this tool: +- For simple, straightforward tasks +- When the user explicitly wants immediate implementation +- When already in plan mode +``` + +### 4. Modify `packages/opencode/src/tool/registry.ts` + +Add import and register tools: + +```typescript +// Add import at top (around line 27) +import { ExitPlanTool, EnterPlanTool } from "./plan" + +// Add to the all() function return array (around line 110-112) +return [ + // ... existing tools + ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), + ...(config.experimental?.batch_tool === true ? [BatchTool] : []), + ExitPlanTool, + EnterPlanTool, + ...custom, +] +``` + +### 5. Modify `packages/opencode/src/agent/agent.ts` + +Add permission rules to control which agent can use which tool: + +**In the `defaults` ruleset (around line 47-63):** + +```typescript +const defaults = PermissionNext.fromConfig({ + "*": "allow", + doom_loop: "ask", + // Add these new defaults - both denied by default + exit_plan: "deny", + enter_plan: "deny", + external_directory: { + // ... existing + }, + // ... rest of existing defaults +}) +``` + +**In the `build` agent (around line 67-79):** + +```typescript +build: { + name: "build", + options: {}, + permission: PermissionNext.merge( + defaults, + PermissionNext.fromConfig({ + question: "allow", + enter_plan: "allow", // Allow build agent to suggest plan mode + }), + user, + ), + mode: "primary", + native: true, +}, +``` + +**In the `plan` agent (around line 80-96):** + +```typescript +plan: { + name: "plan", + options: {}, + permission: PermissionNext.merge( + defaults, + PermissionNext.fromConfig({ + question: "allow", + exit_plan: "allow", // Allow plan agent to exit plan mode + edit: { + "*": "deny", + ".opencode/plans/*.md": "allow", + }, + }), + user, + ), + mode: "primary", + native: true, +}, +``` + +## Design Decisions + +1. **Synthetic user message for mode switching**: When the user confirms a mode switch, a synthetic user message is created with the new agent specified. The loop picks this up on the next iteration and switches to the new agent. This follows the existing pattern in `prompt.ts:455-478`. + +2. **Permission-based tool availability**: Uses the existing permission system to control which tools are available to which agents. `exit_plan` is only available in plan mode, `enter_plan` only in build mode. + +3. **Question-based confirmation**: Both tools use the Question module for consistent UX. + +4. **Model preservation**: The synthetic user message preserves the model from the previous user message. + +## Verification + +1. Run `bun dev` in `packages/opencode` +2. Start a session in build mode + - Verify `exit_plan` is NOT available (denied by permission) + - Verify `enter_plan` IS available +3. Call `enter_plan` in build mode + - Verify the question prompt appears + - Select "Yes" and verify: + - A synthetic user message is created with `agent: "plan"` + - The next assistant response is from the plan agent + - The plan mode system reminder appears +4. In plan mode, call `exit_plan` + - Verify the question prompt appears + - Select "Yes" and verify: + - A synthetic user message is created with `agent: "build"` + - The next assistant response is from the build agent +5. Test "No" responses - verify no mode switch occurs diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index ea9d3e3ba..6847d29ab 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -53,6 +53,8 @@ export namespace Agent { [Truncate.GLOB]: "allow", }, question: "deny", + plan_enter: "deny", + plan_exit: "deny", // mirrors github.com/github/gitignore Node.gitignore pattern for .env files read: { "*": "allow", @@ -71,6 +73,7 @@ export namespace Agent { defaults, PermissionNext.fromConfig({ question: "allow", + plan_enter: "allow", }), user, ), @@ -84,9 +87,10 @@ export namespace Agent { defaults, PermissionNext.fromConfig({ question: "allow", + plan_exit: "allow", edit: { "*": "deny", - ".opencode/plan/*.md": "allow", + ".opencode/plans/*.md": "allow", }, }), user, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index f87b811ae..b6916bc5a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -196,6 +196,23 @@ export function Session() { } }) + let lastSwitch: string | undefined = undefined + sdk.event.on("message.part.updated", (evt) => { + const part = evt.properties.part + if (part.type !== "tool") return + if (part.sessionID !== route.sessionID) return + if (part.state.status !== "completed") return + if (part.id === lastSwitch) return + + if (part.tool === "plan_exit") { + local.agent.set("build") + lastSwitch = part.id + } else if (part.tool === "plan_enter") { + local.agent.set("plan") + lastSwitch = part.id + } + }) + let scroll: ScrollBoxRenderable let prompt: PromptRef const keybind = useKeybind() diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx index ccc0e9b12..5e8ce2380 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx @@ -32,7 +32,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { const question = createMemo(() => questions()[store.tab]) const confirm = createMemo(() => !single() && store.tab === questions().length) const options = createMemo(() => question()?.options ?? []) - const other = createMemo(() => store.selected === options().length) + const custom = createMemo(() => question()?.custom !== false) + const other = createMemo(() => custom() && store.selected === options().length) const input = createMemo(() => store.custom[store.tab] ?? "") const multi = createMemo(() => question()?.multiple === true) const customPicked = createMemo(() => { @@ -203,7 +204,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { } } else { const opts = options() - const total = opts.length + 1 // options + "Other" + const total = opts.length + (custom() ? 1 : 0) if (evt.name === "up" || evt.name === "k") { evt.preventDefault() @@ -298,35 +299,37 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { ) }} - moveTo(options().length)} onMouseUp={() => selectOption()}> - - - - {options().length + 1}. Type your own answer - + + moveTo(options().length)} onMouseUp={() => selectOption()}> + + + + {options().length + 1}. Type your own answer + + + {customPicked() ? "✓" : ""} - {customPicked() ? "✓" : ""} + + +