From 20088a87b0ef37f769eb8096faac89dd4a190af3 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 13 Jan 2026 07:38:27 +0530 Subject: [PATCH 001/534] 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 4ccaacd542..3b76b1e029 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 002/534] 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 53b501ba91..170d444808 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 003/534] 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 87ecf4997d..2fd26dd01c 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 004/534] 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 d3136952bc..6faaf399ae 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 e6203d6657..d8b1bea30b 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 3de7735bde..6dc5e99e91 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 005/534] 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 b68078f142..7ace4e4a26 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 222bac3f8f..d621a6e26b 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 006/534] 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 1e584ca576..1368ef202a 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 726b6c8346..e69de29bb2 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 dbbd3c3b2f..aed07a630f 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 2f8781e988..9b7296791d 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 7fbe5199a7..9aaac37e22 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 eaab9dbef2..215cae698f 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 e0931efd95..a910c2efd1 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 4450c6cb69..96fada3e3c 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 4450c6cb69..96fada3e3c 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 4450c6cb69..96fada3e3c 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 4450c6cb69..96fada3e3c 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 4450c6cb69..96fada3e3c 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 6e8b8e67e6..035a5fc21d 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 007/534] 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 c5d36826c2..98adcdeb13 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 008/534] 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 fe24847856..28e8d97ab6 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 009/534] 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 ec5ef0c437..ef6b0c4fc9 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 010/534] 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 127406d1d9..bf4a6035bd 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 011/534] 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 dc8f937ff5..7d93682bf3 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 a0656c5fc6..ddac1f2286 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 3d0cd5e92b..3e14250b1a 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 495a0baea4..24ff9e7e09 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 885d0afce8..c3019f0b97 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 92953ea19c..43f24a6adf 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 8033d4f147..3145ae4b20 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 b479ed0b61..e2682ec71c 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 012/534] 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 787264e4fa..10001bb619 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 771d9322d9..bef67c82c8 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 c44cfb5e59..9557f83104 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 502537ec4a..ecfb200079 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 5bf3f09750..8f3b1ddeef 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 b4d371afd4..9572cfde8e 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 d8c3fc92a7..5cf2b20dbe 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 d8fc98c61e..7dcdb574d6 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 51bb13a3eb..6ccac0c10b 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 8c85fe0dd9..cc8ae0f18c 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 459e6f6572..07fee7d730 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 8fc6be7ac0..d0b0c5e422 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 e29be370c0..fe34f1775b 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 c73dc0b8a2..1b2d901662 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 5b440f515d..b88c747e16 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 3be2c9e717..fcd980b4a1 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 5eeadf2270..5a657fe0d7 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 2a953baf81..2bfb97a334 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 013/534] 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 86f2a781c8..9e6dd2b9e9 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 014/534] 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 d0b0c5e422..d1848b4a36 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 fe34f1775b..ca24d02aa5 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 015/534] 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 c6518ec3fc..ccc0e9b125 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 016/534] 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 78f022c9f8..14103895a7 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 017/534] 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 14103895a7..e8b5e995cc 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 018/534] 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 43f24a6adf..e577b4db78 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 3145ae4b20..05422b0968 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 0000000000..220aa5db66 --- /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 e2682ec71c..183220d16b 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 019/534] ignore: update download stats 2026-01-13 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index f0354e19fe..ac4b788bae 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 020/534] 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 b6bd7d5c6c..1b3be4b4ca 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 021/534] 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 3f80809727..d0678dc536 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 022/534] 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 1ad015affb..5ae6047b35 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 023/534] 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 5ae6047b35..5d699bb90c 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 024/534] 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 1b3be4b4ca..e265fffef6 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 025/534] 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 e265fffef6..26215e93cb 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 026/534] 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 4bb7b21ec4..75ddb65666 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 027/534] 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 10001bb619..daeb370fde 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 bef67c82c8..305cb7a121 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 9557f83104..61982f58ac 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 ecfb200079..4385fd87c4 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 8f3b1ddeef..bc9ce254cb 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 9572cfde8e..b53cd3171a 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 5cf2b20dbe..477806f33c 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 7dcdb574d6..259a00b6c6 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 6ccac0c10b..4c4365b75d 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 cc8ae0f18c..4d109d370c 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 07fee7d730..8a3d925c16 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 d1848b4a36..fc2db6a510 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 ca24d02aa5..98e46ac3c6 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 1b2d901662..dfc322fb74 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 b88c747e16..e9159b73b5 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 fcd980b4a1..bcbfc0d31b 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 5a657fe0d7..adafa85409 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 2bfb97a334..dab3217524 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 028/534] 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 87d59d4c92..3138f6c5ec 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 a46ce221fb..52d012fcb9 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 d5e0a0aa2a..9ad85d08f0 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 f5b6badb58..c95b42260b 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 029/534] 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 fc2db6a510..7b05077161 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 98e46ac3c6..bd89818d1f 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 030/534] 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 75ddb65666..0d5b585e87 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 031/534] 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 d54f9369af..c614c2d497 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 13f2b00a37..2be8a21c1d 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 800795e878..c25b89af99 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 1283b30232..8c92728d7b 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 9947578b90..ae1321bac1 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 032/534] 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 2be8a21c1d..f1ca3ee888 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 acab99fe8f..dcb9adb39c 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 040/534] 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 10001bb619..563c13a33a 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 9557f83104..23171daac8 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 4ebb2c71ab..4396e51171 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 0000000000..eb55716160 --- /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 166466ef85..6be94b1469 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 9b7296791d..b03bbdbe55 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 418598792f..dfb188ed02 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 0000000000..5a5b139dd5 --- /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 0000000000..c1184bd20c --- /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 f5a375adf8..2b452c8129 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 0000000000..00ce19ef69 --- /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 041/534] 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 b03bbdbe55..9b7296791d 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 042/534] 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 aca0c66315..4e0968391f 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 043/534] 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 4c953c5a87..7e3d51ab57 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 044/534] 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 c1ac23c5d2..34a5dbb3e7 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 045/534] 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 7e3d51ab57..1dbc18960a 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 046/534] 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 2d02ff47f9..3a170019a7 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 047/534] 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 0a28688119..a537fa6614 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 f2c95d0b3e..c0c4e79b69 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 b0c9eee2c2..8ce6dfd3c3 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 3b76b1e029..9bde1333ea 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 d10e851391..05f5bd01f8 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 0000000000..4d5aa9c746 --- /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 80c6f89e15..7af4ab85db 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 048/534] 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 cfb54081f6..cdd741fbc7 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 049/534] 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 4c953c5a87..0bf4aa6273 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 050/534] 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 0bf4aa6273..a25b9376e5 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 051/534] 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 10e340d7f8..f87b811ae8 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 c95b42260b..9cde65d2e6 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 052/534] 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 9cde65d2e6..eab2adb100 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 053/534] 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 4e2b283795..91e66197fc 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 054/534] 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 f1ca3ee888..2f85652a93 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 055/534] 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 0000000000..9e3e668b4a --- /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 ea9d3e3ba1..6847d29abe 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 f87b811ae8..b6916bc5a5 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 ccc0e9b125..5e8ce23807 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() ? "✓" : ""} + + +