From bd139b4bd61f4d62cc1314a05b87b704a185b5e1 Mon Sep 17 00:00:00 2001 From: Robb Tolliver Date: Wed, 24 Dec 2025 17:02:35 -0700 Subject: [PATCH 001/164] docs: Corrected the number of built-in subagents in documentation (#6133) --- packages/web/src/content/docs/agents.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 6c1b15239c9..1922fece77f 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -42,7 +42,7 @@ OpenCode comes with two built-in subagents, **General** and **Explore**. We'll l ## Built-in -OpenCode comes with two built-in primary agents and one built-in subagent. +OpenCode comes with two built-in primary agents and two built-in subagents. --- From 8eac72341f56e2def45d347edda59eb9cb3b64d6 Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Thu, 25 Dec 2025 00:18:33 +0000 Subject: [PATCH 002/164] docs: update skills to use canonical `~/.config/opencode` location (#6132) --- packages/web/src/content/docs/skills.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/content/docs/skills.mdx b/packages/web/src/content/docs/skills.mdx index c1d433a839f..c559c067c4c 100644 --- a/packages/web/src/content/docs/skills.mdx +++ b/packages/web/src/content/docs/skills.mdx @@ -14,7 +14,7 @@ Create one folder per skill name and put a `SKILL.md` inside it. OpenCode searches these locations: - Project config: `.opencode/skill//SKILL.md` -- Global config: `~/.opencode/skill//SKILL.md` +- Global config: `~/.config/opencode/skill//SKILL.md` - Claude-compatible: `.claude/skills//SKILL.md` --- @@ -24,7 +24,7 @@ OpenCode searches these locations: For project-local paths, OpenCode walks up from your current working directory until it reaches the git worktree. It loads any matching `skill/*/SKILL.md` in `.opencode/` and any matching `.claude/skills/*/SKILL.md` along the way. -Global definitions are also loaded from `~/.opencode/skill/*/SKILL.md`. +Global definitions are also loaded from `~/.config/opencode/skill/*/SKILL.md`. --- From a5301e2ab7b592f90cb91212af7c2af3329601a0 Mon Sep 17 00:00:00 2001 From: Ahmed Mansour Date: Thu, 25 Dec 2025 03:00:22 +0100 Subject: [PATCH 003/164] fix: correct Content-Type headers for static assets on app.opencode.ai (#6113) --- packages/app/public/_headers | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/app/public/_headers diff --git a/packages/app/public/_headers b/packages/app/public/_headers new file mode 100644 index 00000000000..f5157b1debc --- /dev/null +++ b/packages/app/public/_headers @@ -0,0 +1,17 @@ +/assets/*.js + Content-Type: application/javascript + +/assets/*.mjs + Content-Type: application/javascript + +/assets/*.css + Content-Type: text/css + +/*.js + Content-Type: application/javascript + +/*.mjs + Content-Type: application/javascript + +/*.css + Content-Type: text/css From e71bc8c0b03c422208e89d73d5d4d6a0fe013941 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:11:34 -0600 Subject: [PATCH 004/164] fix(desktop): show server connection failure --- packages/app/src/context/global-sync.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index b7ad1189c0a..cbfe37ee507 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -307,7 +307,10 @@ function createGlobalSync() { }) async function bootstrap() { - const health = await globalSDK.client.global.health().then((x) => x.data) + const health = await globalSDK.client.global + .health() + .then((x) => x.data) + .catch(() => undefined) if (!health?.healthy) { setGlobalStore( "error", From 76ac1ccb6b4023ff67cc2dd130317148b6c753d7 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:25:36 -0600 Subject: [PATCH 005/164] chore: show version on error page --- packages/app/src/context/platform.tsx | 3 +++ packages/app/src/entry.tsx | 2 ++ packages/app/src/pages/error.tsx | 27 ++++++++++++++++----------- packages/app/tsconfig.json | 2 ++ packages/desktop/src/index.tsx | 2 ++ packages/desktop/tsconfig.json | 3 ++- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 73d4c7f3ed9..2b710e6f2b1 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -5,6 +5,9 @@ export type Platform = { /** Platform discriminator */ platform: "web" | "tauri" + /** App version */ + version?: string + /** Open a URL in the default browser */ openLink(url: string): void diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index ecbce9815b9..cbcac355fff 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -2,6 +2,7 @@ import { render } from "solid-js/web" import { App } from "@/app" import { Platform, PlatformProvider } from "@/context/platform" +import pkg from "../package.json" const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { @@ -12,6 +13,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { const platform: Platform = { platform: "web", + version: pkg.version, openLink(url: string) { window.open(url, "_blank") }, diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx index 9914279adad..37bd5ccd3cb 100644 --- a/packages/app/src/pages/error.tsx +++ b/packages/app/src/pages/error.tsx @@ -1,7 +1,7 @@ import { TextField } from "@opencode-ai/ui/text-field" import { Logo } from "@opencode-ai/ui/logo" import { Button } from "@opencode-ai/ui/button" -import { Component } from "solid-js" +import { Component, Show } from "solid-js" import { usePlatform } from "@/context/platform" import { Icon } from "@opencode-ai/ui/icon" @@ -138,16 +138,21 @@ export const ErrorPage: Component = (props) => { -
- Please report this error to the OpenCode team - +
+
+ Please report this error to the OpenCode team + +
+ +

Version: {platform.version}

+
diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index db04f79cae5..e2a27dd5d8a 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -11,6 +11,7 @@ "jsx": "preserve", "jsxImportSource": "solid-js", "allowJs": true, + "resolveJsonModule": true, "strict": true, "noEmit": false, "emitDeclarationOnly": true, @@ -20,5 +21,6 @@ "@/*": ["./src/*"] } }, + "include": ["src", "package.json"], "exclude": ["dist", "ts-dist"] } diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index b3dbebd04c4..58aca8fd172 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -13,6 +13,7 @@ import { createMenu } from "./menu" import { check, Update } from "@tauri-apps/plugin-updater" import { invoke } from "@tauri-apps/api/core" import { relaunch } from "@tauri-apps/plugin-process" +import pkg from "../package.json" const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { @@ -25,6 +26,7 @@ let update: Update | null = null const platform: Platform = { platform: "tauri", + version: pkg.version, async openDirectoryPickerDialog(opts) { const result = await open({ diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json index 64a6bc35783..62cbe4ee4b5 100644 --- a/packages/desktop/tsconfig.json +++ b/packages/desktop/tsconfig.json @@ -9,6 +9,7 @@ "jsx": "preserve", "jsxImportSource": "solid-js", "allowJs": true, + "resolveJsonModule": true, "strict": true, "isolatedModules": true, "noEmit": true, @@ -16,5 +17,5 @@ "outDir": "node_modules/.ts-dist" }, "references": [{ "path": "../app" }], - "include": ["src"] + "include": ["src", "package.json"] } From 8a9b4245b4dc75838e3b5ad0ff329c492a89cc41 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:30:52 -0600 Subject: [PATCH 006/164] chore: cleanup dead code --- packages/app/src/components/file-tree.tsx | 10 ++++---- packages/app/src/context/global-sync.tsx | 8 ------ packages/app/src/context/local.tsx | 30 +++++++++++------------ 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 0841c71d1d9..3439d366cee 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -2,7 +2,7 @@ import { useLocal, type LocalFile } from "@/context/local" import { Collapsible } from "@opencode-ai/ui/collapsible" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Tooltip } from "@opencode-ai/ui/tooltip" -import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js" +import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js" import { Dynamic } from "solid-js/web" export default function FileTree(props: { @@ -57,14 +57,14 @@ export default function FileTree(props: { "text-text-muted/40": p.node.ignored, "text-text-muted/80": !p.node.ignored, // "!text-text": local.file.active()?.path === p.node.path, - "!text-primary": local.file.changed(p.node.path), + // "!text-primary": local.file.changed(p.node.path), }} > {p.node.name} - - - + {/* */} + {/* */} + {/* */} ) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index cbfe37ee507..32728650b09 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -5,8 +5,6 @@ import { type Part, type Config, type Path, - type File, - type FileNode, type Project, type FileDiff, type Todo, @@ -50,8 +48,6 @@ type State = { part: { [messageID: string]: Part[] } - node: FileNode[] - changes: File[] } function createGlobalSync() { @@ -92,8 +88,6 @@ function createGlobalSync() { limit: 5, message: {}, part: {}, - node: [], - changes: [], }) children[directory] = createStore(globalStore.children[directory]) bootstrapInstance(directory) @@ -155,8 +149,6 @@ function createGlobalSync() { session: () => loadSessions(directory), status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)), config: () => sdk.config.get().then((x) => setStore("config", x.data!)), - changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)), - node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)), } await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e)))) .then(() => setStore("ready", true)) diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 69807a2f443..ea3fdb0d5d3 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -276,11 +276,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const [store, setStore] = createStore<{ node: Record }>({ - node: Object.fromEntries(sync.data.node.map((x) => [x.path, x])), + node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])), }) - const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path))) - const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b))) + // const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path))) + // const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b))) // createEffect((prev: FileStatus[]) => { // const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path)) @@ -308,16 +308,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ // return sync.data.changes // }, sync.data.changes) - const changed = (path: string) => { - const node = store.node[path] - if (node?.status) return true - const set = changeset() - if (set.has(path)) return true - for (const p of set) { - if (p.startsWith(path ? path + "/" : "")) return true - } - return false - } + // const changed = (path: string) => { + // const node = store.node[path] + // if (node?.status) return true + // const set = changeset() + // if (set.has(path)) return true + // for (const p of set) { + // if (p.startsWith(path ? path + "/" : "")) return true + // } + // return false + // } // const resetNode = (path: string) => { // setStore("node", path, { @@ -466,8 +466,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ setChangeIndex(path: string, index: number | undefined) { setStore("node", path, "selectedChange", index) }, - changes, - changed, + // changes, + // changed, children(path: string) { return Object.values(store.node).filter( (x) => From ad852d918656322acf2dc6645085fe3f06cff6f5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:37:12 -0600 Subject: [PATCH 007/164] chore: toast on file load error --- packages/app/src/context/local.tsx | 32 ++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index ea3fdb0d5d3..33b78cbecb6 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -9,6 +9,7 @@ import { base64Encode } from "@opencode-ai/util/encode" import { useProviders } from "@/hooks/use-providers" import { DateTime } from "luxon" import { persisted } from "@/utils/persist" +import { showToast } from "@opencode-ai/ui/toast" export type LocalFile = FileNode & Partial<{ @@ -336,17 +337,26 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const load = async (path: string) => { const relativePath = relative(path) - await sdk.client.file.read({ path: relativePath }).then((x) => { - if (!store.node[relativePath]) return - setStore( - "node", - relativePath, - produce((draft) => { - draft.loaded = true - draft.content = x.data - }), - ) - }) + await sdk.client.file + .read({ path: relativePath }) + .then((x) => { + if (!store.node[relativePath]) return + setStore( + "node", + relativePath, + produce((draft) => { + draft.loaded = true + draft.content = x.data + }), + ) + }) + .catch((e) => { + showToast({ + variant: "error", + title: "Failed to load file", + description: e.message, + }) + }) } const fetch = async (path: string) => { From 2284a4e6dfc18126d9a2589e987ff546da7ef25c Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 25 Dec 2025 02:40:19 +0000 Subject: [PATCH 008/164] release: v1.0.199 --- 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 93aea8d21df..081dc6434ea 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -77,7 +77,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -105,7 +105,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -132,7 +132,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -156,7 +156,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -180,7 +180,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@opencode-ai/app": "workspace:*", "@solid-primitives/storage": "catalog:", @@ -207,7 +207,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -236,7 +236,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -252,7 +252,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.198", + "version": "1.0.199", "bin": { "opencode": "./bin/opencode", }, @@ -346,7 +346,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -366,7 +366,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.198", + "version": "1.0.199", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -377,7 +377,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -390,7 +390,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -425,7 +425,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.198", + "version": "1.0.199", "dependencies": { "zod": "catalog:", }, @@ -436,7 +436,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.198", + "version": "1.0.199", "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 2dfa55cddbc..b506f9933d6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.0.198", + "version": "1.0.199", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 3f211edb6ac..3211a718b0f 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.198", + "version": "1.0.199", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index dba4fa92c60..eb42f2cf1fd 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.0.198", + "version": "1.0.199", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 8b50ffbc471..580e7857f7a 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.198", + "version": "1.0.199", "$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 d20fdaa26ed..ee07f6771bc 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.198", + "version": "1.0.199", "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 1997ac4625d..3cf34f6da1c 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.0.198", + "version": "1.0.199", "type": "module", "scripts": { "typecheck": "tsgo -b", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 31fd64c57e1..f135002221f 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.0.198", + "version": "1.0.199", "private": true, "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 2b0261f833a..87db2b0a067 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.0.198" +version = "1.0.199" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.198/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.198/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.198/opencode-linux-arm64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.198/opencode-linux-x64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.198/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 1331f1c4360..e2444791b14 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.198", + "version": "1.0.199", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index a76e20b6fe1..ceadad786fa 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.198", + "version": "1.0.199", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 6ffcf30e768..1c6b2607ec3 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.0.198", + "version": "1.0.199", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -24,4 +24,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 38dcb4b2f5e..cf4211c24c7 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.0.198", + "version": "1.0.199", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index cffd5f1eeeb..0e44647d6f2 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.198", + "version": "1.0.199", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 64eea27ab86..86fae5d9961 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.198", + "version": "1.0.199", "type": "module", "exports": { "./*": "./src/components/*.tsx", diff --git a/packages/util/package.json b/packages/util/package.json index caa62da83fd..94bfb7bfac0 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.198", + "version": "1.0.199", "private": true, "type": "module", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index e687a0b392c..448a93941aa 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.198", + "version": "1.0.199", "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 ce3a1512d07..c8e6dd10670 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.0.198", + "version": "1.0.199", "publisher": "sst-dev", "repository": { "type": "git", From b1d2fb53190a4ed4cc8e6c08d811f474b5a8f5d4 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:16:44 -0600 Subject: [PATCH 009/164] fix(desktop): reconcile session diff updates --- packages/app/src/context/global-sync.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 32728650b09..085c5067cfa 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -215,13 +215,13 @@ function createGlobalSync() { break } case "session.diff": - setStore("session_diff", event.properties.sessionID, event.properties.diff) + setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff)) break case "todo.updated": - setStore("todo", event.properties.sessionID, event.properties.todos) + setStore("todo", event.properties.sessionID, reconcile(event.properties.todos)) break case "session.status": { - setStore("session_status", event.properties.sessionID, event.properties.status) + setStore("session_status", event.properties.sessionID, reconcile(event.properties.status)) break } case "message.updated": { From 2178deef914fa35db957811076559907806dd054 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:16:58 -0600 Subject: [PATCH 010/164] fix(desktop): override agent model --- packages/app/src/context/local.tsx | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 33b78cbecb6..600a0e4b160 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,5 +1,5 @@ import { createStore, produce, reconcile } from "solid-js/store" -import { batch, createEffect, createMemo } from "solid-js" +import { batch, createMemo } from "solid-js" import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "@opencode-ai/ui/context" @@ -62,24 +62,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } } - // Automatically update model when agent changes - createEffect(() => { - const value = agent.current() - if (value.model) { - if (isModelValid(value.model)) - model.set({ - providerID: value.model.providerID, - modelID: value.model.modelID, - }) - // else - // toast.show({ - // type: "warning", - // message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`, - // duration: 3000, - // }) - } - }) - const agent = (() => { const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) const [store, setStore] = createStore<{ From b746e831e2ae19463cf9c8aca1d9ceea09261afd Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 25 Dec 2025 04:17:39 +0000 Subject: [PATCH 011/164] 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 1c6b2607ec3..3a3eac4066b 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,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 cf4211c24c7..c08c9106250 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 5c49b4cbfc4bbcada6832db3fdb0e38223b893b0 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:36:40 -0600 Subject: [PATCH 012/164] fix(desktop): scroll jank in session turn and review --- packages/app/src/context/global-sync.tsx | 2 +- packages/ui/src/components/session-turn.tsx | 127 +++++++++++--------- 2 files changed, 74 insertions(+), 55 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 085c5067cfa..10607b1d23f 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -215,7 +215,7 @@ function createGlobalSync() { break } case "session.diff": - setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff)) + setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" })) break case "todo.updated": setStore("todo", event.properties.sessionID, reconcile(event.properties.todos)) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index a160f4ba3a6..a0368b0d492 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -124,77 +124,96 @@ export function SessionTurn( return allMessages().filter((m) => m.role === "assistant" && m.parentID === msg.id) as AssistantMessage[] }) - const assistantParts = createMemo(() => { - const result: PartType[] = [] - for (const m of assistantMessages()) { - const msgParts = data.store.part[m.id] - if (msgParts) { - for (const p of msgParts) { - if (p) result.push(p) - } - } - } - return result - }) - const lastAssistantMessage = createMemo(() => assistantMessages().at(-1)) const error = createMemo(() => assistantMessages().find((m) => m.error)?.error) const lastTextPart = createMemo(() => { - const ap = assistantParts() - for (let i = ap.length - 1; i >= 0; i--) { - if (ap[i]?.type === "text") return ap[i] + const msgs = assistantMessages() + for (let mi = msgs.length - 1; mi >= 0; mi--) { + const msgParts = data.store.part[msgs[mi].id] ?? [] + for (let pi = msgParts.length - 1; pi >= 0; pi--) { + const part = msgParts[pi] + if (part?.type === "text") return part as TextPart + } } return undefined }) - const hasSteps = createMemo(() => assistantParts().some((p) => p?.type === "tool")) - - const isShellMode = createMemo(() => { - const p = parts() - const ap = assistantParts() - if (p.every((part) => part?.type === "text" && part?.synthetic) && ap.length === 1) { - const assistantPart = ap[0] - if (assistantPart?.type === "tool" && assistantPart?.tool === "bash") return true + const hasSteps = createMemo(() => { + for (const m of assistantMessages()) { + const msgParts = data.store.part[m.id] + if (!msgParts) continue + for (const p of msgParts) { + if (p?.type === "tool") return true + } } return false }) + const shellModePart = createMemo(() => { + const p = parts() + if (!p.every((part) => part?.type === "text" && part?.synthetic)) return + + const msgs = assistantMessages() + if (msgs.length !== 1) return + + const msgParts = data.store.part[msgs[0].id] ?? [] + if (msgParts.length !== 1) return + + const assistantPart = msgParts[0] + if (assistantPart?.type === "tool" && assistantPart.tool === "bash") return assistantPart + }) + + const isShellMode = createMemo(() => !!shellModePart()) + const rawStatus = createMemo(() => { - const ap = assistantParts() - const currentTask = ap.findLast( - (p) => - p && - p.type === "tool" && - p.tool === "task" && - p.state && - "metadata" in p.state && - p.state.metadata && - p.state.metadata.sessionId && - p.state.status === "running", - ) as ToolPart | undefined - - let resolvedParts = ap - if (currentTask?.state && "metadata" in currentTask.state && currentTask.state.metadata?.sessionId) { - const taskMessages = data.store.message[currentTask.state.metadata.sessionId as string]?.filter( - (m) => m.role === "assistant", - ) - if (taskMessages) { - const taskParts: PartType[] = [] - for (const m of taskMessages) { - const msgParts = data.store.part[m.id] - if (msgParts) { - for (const p of msgParts) { - if (p) taskParts.push(p) - } - } + const msgs = assistantMessages() + let last: PartType | undefined + let currentTask: ToolPart | undefined + + for (let mi = msgs.length - 1; mi >= 0; mi--) { + const msgParts = data.store.part[msgs[mi].id] ?? [] + for (let pi = msgParts.length - 1; pi >= 0; pi--) { + const part = msgParts[pi] + if (!part) continue + if (!last) last = part + + if ( + part.type === "tool" && + part.tool === "task" && + part.state && + "metadata" in part.state && + part.state.metadata?.sessionId && + part.state.status === "running" + ) { + currentTask = part as ToolPart + break + } + } + if (currentTask) break + } + + const taskSessionId = + currentTask?.state && "metadata" in currentTask.state + ? (currentTask.state.metadata?.sessionId as string | undefined) + : undefined + + if (taskSessionId) { + const taskMessages = data.store.message[taskSessionId] ?? [] + for (let mi = taskMessages.length - 1; mi >= 0; mi--) { + const msg = taskMessages[mi] + if (!msg || msg.role !== "assistant") continue + + const msgParts = data.store.part[msg.id] ?? [] + for (let pi = msgParts.length - 1; pi >= 0; pi--) { + const part = msgParts[pi] + if (part) return computeStatusFromPart(part) } - if (taskParts.length > 0) resolvedParts = taskParts } } - return computeStatusFromPart(resolvedParts.at(-1)) + return computeStatusFromPart(last) }) const status = createMemo( @@ -367,7 +386,7 @@ export function SessionTurn( > - + {/* Title (sticky) */} From 6f9bea4e1f3d139feefd0f88de260b04f78caaef Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 25 Dec 2025 04:39:32 +0000 Subject: [PATCH 013/164] release: v1.0.200 --- 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 081dc6434ea..03fe5cb736f 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -77,7 +77,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -105,7 +105,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -132,7 +132,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -156,7 +156,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -180,7 +180,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@opencode-ai/app": "workspace:*", "@solid-primitives/storage": "catalog:", @@ -207,7 +207,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -236,7 +236,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -252,7 +252,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.199", + "version": "1.0.200", "bin": { "opencode": "./bin/opencode", }, @@ -346,7 +346,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -366,7 +366,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.199", + "version": "1.0.200", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -377,7 +377,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -390,7 +390,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -425,7 +425,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.199", + "version": "1.0.200", "dependencies": { "zod": "catalog:", }, @@ -436,7 +436,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.199", + "version": "1.0.200", "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 b506f9933d6..4158e71f6e1 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.0.199", + "version": "1.0.200", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 3211a718b0f..f7a28a23064 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.199", + "version": "1.0.200", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index eb42f2cf1fd..c61b546e351 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.0.199", + "version": "1.0.200", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 580e7857f7a..0c9861f7af9 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.199", + "version": "1.0.200", "$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 ee07f6771bc..b357cb7cff0 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.199", + "version": "1.0.200", "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 3cf34f6da1c..34a42212e6e 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.0.199", + "version": "1.0.200", "type": "module", "scripts": { "typecheck": "tsgo -b", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index f135002221f..9488f7e7818 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.0.199", + "version": "1.0.200", "private": true, "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 87db2b0a067..41038da77bd 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.0.199" +version = "1.0.200" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-linux-arm64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-linux-x64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index e2444791b14..ab31ffe75ff 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.199", + "version": "1.0.200", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ceadad786fa..35747567d79 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.199", + "version": "1.0.200", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 3a3eac4066b..5b42c1eaeb8 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.0.199", + "version": "1.0.200", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -24,4 +24,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 c08c9106250..a62f7697325 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.0.199", + "version": "1.0.200", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 0e44647d6f2..c705e643a9c 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.199", + "version": "1.0.200", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 86fae5d9961..03344a83f92 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.199", + "version": "1.0.200", "type": "module", "exports": { "./*": "./src/components/*.tsx", diff --git a/packages/util/package.json b/packages/util/package.json index 94bfb7bfac0..53c6508d08d 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.199", + "version": "1.0.200", "private": true, "type": "module", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 448a93941aa..e283e16e8e1 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.199", + "version": "1.0.200", "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 c8e6dd10670..6723a56bc09 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.0.199", + "version": "1.0.200", "publisher": "sst-dev", "repository": { "type": "git", From f397c92ddfb09a6eee1e84563547b833f62af6c8 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 25 Dec 2025 00:58:47 -0500 Subject: [PATCH 014/164] remove list tool --- packages/opencode/src/tool/registry.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 69a45432dc5..f975d52a0d4 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -95,7 +95,6 @@ export namespace ToolRegistry { ReadTool, GlobTool, GrepTool, - ListTool, EditTool, WriteTool, TaskTool, From 388d40e41f4084fed5766c8dc066ba6e69da7509 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 25 Dec 2025 05:59:27 +0000 Subject: [PATCH 015/164] 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 5b42c1eaeb8..66ed0380e14 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,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 a62f7697325..d2cc28dfbe8 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From f057b22e20b24532a3735cc399a3fdd6f6bfaec0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 25 Dec 2025 12:04:54 +0000 Subject: [PATCH 016/164] ignore: update download stats 2025-12-25 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 0a187668954..d3c6f57ad40 100644 --- a/STATS.md +++ b/STATS.md @@ -180,3 +180,4 @@ | 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) | | 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) | | 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) | +| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) | From 5b5b8c57d913e31c4366b90dd9e0e84077d9e9ed Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 08:04:37 -0600 Subject: [PATCH 017/164] fix(desktop): so many prompt input fixes, merry christmas --- packages/app/src/components/prompt-input.tsx | 148 +++++++++++-------- 1 file changed, 85 insertions(+), 63 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 94d4ae97e8a..33e1f48900e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -103,7 +103,6 @@ export const PromptInput: Component = (props) => { imageAttachments: ImageAttachmentPart[] mode: "normal" | "shell" applyingHistory: boolean - userHasEdited: boolean }>({ popover: null, historyIndex: -1, @@ -113,7 +112,6 @@ export const PromptInput: Component = (props) => { imageAttachments: [], mode: "normal", applyingHistory: false, - userHasEdited: false, }) const MAX_HISTORY = 100 @@ -150,7 +148,6 @@ export const PromptInput: Component = (props) => { const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => { const length = position === "start" ? 0 : promptLength(p) setStore("applyingHistory", true) - setStore("userHasEdited", false) prompt.set(p, length) requestAnimationFrame(() => { editorRef.focus() @@ -279,11 +276,7 @@ export const PromptInput: Component = (props) => { }) createEffect(() => { - if (isFocused()) { - handleInput() - } else { - setStore("popover", null) - } + if (!isFocused()) setStore("popover", null) }) const handleFileSelect = (path: string | undefined) => { @@ -363,7 +356,12 @@ export const PromptInput: Component = (props) => { () => prompt.current(), (currentParts) => { const domParts = parseFromDOM() - if (isPromptEqual(currentParts, domParts)) return + const normalized = Array.from(editorRef.childNodes).every((node) => { + if (node.nodeType === Node.TEXT_NODE) return true + if (node.nodeType !== Node.ELEMENT_NODE) return false + return (node as HTMLElement).dataset.type === "file" + }) + if (normalized && isPromptEqual(currentParts, domParts)) return const selection = window.getSelection() let cursorPosition: number | null = null @@ -395,34 +393,21 @@ export const PromptInput: Component = (props) => { ) const parseFromDOM = (): Prompt => { - const newParts: Prompt = [] + const parts: Prompt = [] let position = 0 + let buffer = "" - const pushText = (content: string) => { + const flushText = () => { + const content = buffer.replace(/\r\n?/g, "\n") + buffer = "" if (!content) return - newParts.push({ type: "text", content, start: position, end: position + content.length }) + parts.push({ type: "text", content, start: position, end: position + content.length }) position += content.length } - const rangeText = (range: Range) => { - const fragment = range.cloneContents() - const container = document.createElement("div") - container.append(fragment) - return container.innerText - } - - const files = Array.from(editorRef.querySelectorAll("[data-type=file]")) - let last: HTMLElement | undefined - - files.forEach((file) => { - const before = document.createRange() - before.selectNodeContents(editorRef) - if (last) before.setStartAfter(last) - before.setEndBefore(file) - pushText(rangeText(before)) - + const pushFile = (file: HTMLElement) => { const content = file.textContent ?? "" - newParts.push({ + parts.push({ type: "file", path: file.dataset.path!, content, @@ -430,16 +415,44 @@ export const PromptInput: Component = (props) => { end: position + content.length, }) position += content.length - last = file + } + + const visit = (node: Node) => { + if (node.nodeType === Node.TEXT_NODE) { + buffer += node.textContent ?? "" + return + } + if (node.nodeType !== Node.ELEMENT_NODE) return + + const el = node as HTMLElement + if (el.dataset.type === "file") { + flushText() + pushFile(el) + return + } + if (el.tagName === "BR") { + buffer += "\n" + return + } + + for (const child of Array.from(el.childNodes)) { + visit(child) + } + } + + const children = Array.from(editorRef.childNodes) + children.forEach((child, index) => { + const isBlock = child.nodeType === Node.ELEMENT_NODE && ["DIV", "P"].includes((child as HTMLElement).tagName) + visit(child) + if (isBlock && index < children.length - 1) { + buffer += "\n" + } }) - const after = document.createRange() - after.selectNodeContents(editorRef) - if (last) after.setStartAfter(last) - pushText(rangeText(after)) + flushText() - if (newParts.length === 0) newParts.push(...DEFAULT_PROMPT) - return newParts + if (parts.length === 0) parts.push(...DEFAULT_PROMPT) + return parts } const handleInput = () => { @@ -452,7 +465,6 @@ export const PromptInput: Component = (props) => { if (shouldReset) { setStore("popover", null) - setStore("userHasEdited", false) if (store.historyIndex >= 0 && !store.applyingHistory) { setStore("historyIndex", -1) setStore("savedPrompt", null) @@ -487,10 +499,6 @@ export const PromptInput: Component = (props) => { setStore("savedPrompt", null) } - if (!store.applyingHistory) { - setStore("userHasEdited", true) - } - prompt.set(rawParts, cursorPosition) } @@ -516,27 +524,39 @@ export const PromptInput: Component = (props) => { const gap = document.createTextNode(" ") const range = selection.getRangeAt(0) - if (atMatch) { - let runningLength = 0 - - const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null) - let currentNode = walker.nextNode() - while (currentNode) { - const textContent = currentNode.textContent || "" - if (runningLength + textContent.length >= atMatch.index!) { - const localStart = atMatch.index! - runningLength - const localEnd = cursorPosition - runningLength - if (currentNode === range.startContainer || runningLength + textContent.length >= cursorPosition) { - range.setStart(currentNode, localStart) - range.setEnd(currentNode, Math.min(localEnd, textContent.length)) - break - } + const setEdge = (edge: "start" | "end", offset: number) => { + let remaining = offset + const nodes = Array.from(editorRef.childNodes) + + for (const node of nodes) { + const length = node.textContent?.length ?? 0 + const isText = node.nodeType === Node.TEXT_NODE + const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file" + + if (isText && remaining <= length) { + if (edge === "start") range.setStart(node, remaining) + if (edge === "end") range.setEnd(node, remaining) + return + } + + if (isFile && remaining <= length) { + if (edge === "start" && remaining === 0) range.setStartBefore(node) + if (edge === "start" && remaining > 0) range.setStartAfter(node) + if (edge === "end" && remaining === 0) range.setEndBefore(node) + if (edge === "end" && remaining > 0) range.setEndAfter(node) + return } - runningLength += textContent.length - currentNode = walker.nextNode() + + remaining -= length } } + if (atMatch) { + const start = atMatch.index ?? cursorPosition - atMatch[0].length + setEdge("start", start) + setEdge("end", cursorPosition) + } + range.deleteContents() range.insertNode(gap) range.insertNode(pill) @@ -584,8 +604,6 @@ export const PromptInput: Component = (props) => { } const navigateHistory = (direction: "up" | "down") => { - if (store.userHasEdited) return false - const entries = store.mode === "shell" ? shellHistory.entries : history.entries const current = store.historyIndex @@ -692,6 +710,11 @@ export const PromptInput: Component = (props) => { return } + if (event.key === "Enter" && event.shiftKey) { + addPart({ type: "text", content: "\n", start: 0, end: 0 }) + event.preventDefault() + return + } if (event.key === "Enter" && !event.shiftKey) { handleSubmit(event) } @@ -717,7 +740,6 @@ export const PromptInput: Component = (props) => { addToHistory(currentPrompt, store.mode) setStore("historyIndex", -1) setStore("savedPrompt", null) - setStore("userHasEdited", false) let existing = info() if (!existing) { @@ -968,7 +990,7 @@ export const PromptInput: Component = (props) => { onKeyDown={handleKeyDown} classList={{ "w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, - "[&>[data-type=file]]:text-icon-info-active": true, + "[&_[data-type=file]]:text-icon-info-active": true, "font-mono!": store.mode === "shell", }} /> From 8aa35206838fec34c5ab0640cb5bef57cbadb78c Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 25 Dec 2025 14:07:19 +0000 Subject: [PATCH 018/164] release: v1.0.201 --- 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 03fe5cb736f..c0a490ba0ae 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -77,7 +77,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -105,7 +105,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -132,7 +132,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -156,7 +156,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -180,7 +180,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@opencode-ai/app": "workspace:*", "@solid-primitives/storage": "catalog:", @@ -207,7 +207,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -236,7 +236,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -252,7 +252,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.200", + "version": "1.0.201", "bin": { "opencode": "./bin/opencode", }, @@ -346,7 +346,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -366,7 +366,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.200", + "version": "1.0.201", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -377,7 +377,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -390,7 +390,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -425,7 +425,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.200", + "version": "1.0.201", "dependencies": { "zod": "catalog:", }, @@ -436,7 +436,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.200", + "version": "1.0.201", "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 4158e71f6e1..de314c18ac7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.0.200", + "version": "1.0.201", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index f7a28a23064..30bb4d7fd3f 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.200", + "version": "1.0.201", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index c61b546e351..e6eedfbfd9f 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.0.200", + "version": "1.0.201", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 0c9861f7af9..42857251be8 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.200", + "version": "1.0.201", "$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 b357cb7cff0..3f8ec54edea 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.200", + "version": "1.0.201", "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 34a42212e6e..f994d660e78 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.0.200", + "version": "1.0.201", "type": "module", "scripts": { "typecheck": "tsgo -b", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 9488f7e7818..c291831453a 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.0.200", + "version": "1.0.201", "private": true, "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 41038da77bd..35555ba021f 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.0.200" +version = "1.0.201" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-linux-arm64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-linux-x64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.200/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index ab31ffe75ff..9eafac4e39b 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.200", + "version": "1.0.201", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 35747567d79..569a863a75f 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.200", + "version": "1.0.201", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 66ed0380e14..f55e8d9d3a1 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.0.200", + "version": "1.0.201", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -24,4 +24,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 d2cc28dfbe8..7c3d5ab73d3 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.0.200", + "version": "1.0.201", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index c705e643a9c..766ea72c34e 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.200", + "version": "1.0.201", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 03344a83f92..91db04d1463 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.200", + "version": "1.0.201", "type": "module", "exports": { "./*": "./src/components/*.tsx", diff --git a/packages/util/package.json b/packages/util/package.json index 53c6508d08d..336837181b8 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.200", + "version": "1.0.201", "private": true, "type": "module", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index e283e16e8e1..7e36b9e03e8 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.200", + "version": "1.0.201", "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 6723a56bc09..eddcb6c8923 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.0.200", + "version": "1.0.201", "publisher": "sst-dev", "repository": { "type": "git", From 650bd76370a48098139898ddecc14abe47ca2c60 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 14:31:10 -0600 Subject: [PATCH 019/164] feat(desktop): better indicator that session is busy --- packages/app/src/pages/layout.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 838b8ee947d..e8dd152e1fc 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -472,7 +472,12 @@ export default function Layout(props: ParentProps) { class="flex flex-col min-w-0 text-left w-full focus:outline-none" >
- + {props.session.title}
From 603dae562ac78f48895782e2125805116e18a8f3 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 18:46:57 -0600 Subject: [PATCH 020/164] chore(ui): radio group primitive --- packages/ui/src/components/radio-group.css | 160 +++++++++++++++++++++ packages/ui/src/components/radio-group.tsx | 75 ++++++++++ packages/ui/src/styles/index.css | 1 + 3 files changed, 236 insertions(+) create mode 100644 packages/ui/src/components/radio-group.css create mode 100644 packages/ui/src/components/radio-group.tsx diff --git a/packages/ui/src/components/radio-group.css b/packages/ui/src/components/radio-group.css new file mode 100644 index 00000000000..38773b819ad --- /dev/null +++ b/packages/ui/src/components/radio-group.css @@ -0,0 +1,160 @@ +[data-component="radio-group"] { + display: flex; + flex-direction: column; + gap: calc(var(--spacing) * 2); + + [data-slot="radio-group-wrapper"] { + all: unset; + background-color: var(--surface-base); + border-radius: var(--radius-md); + box-shadow: inset 0 0 0 1px var(--border-weak-base); + margin: 0; + padding: 0; + position: relative; + width: fit-content; + } + + [data-slot="radio-group-items"] { + display: inline-flex; + list-style: none; + flex-direction: row; + } + + [data-slot="radio-group-indicator"] { + background: var(--button-secondary-base); + border-radius: var(--radius-md); + box-shadow: + var(--shadow-xs), + inset 0 0 0 var(--indicator-focus-width, 0px) var(--border-selected), + inset 0 0 0 1px var(--border-base); + content: ""; + opacity: var(--indicator-opacity, 1); + position: absolute; + transition: + opacity 300ms ease-in-out, + box-shadow 100ms ease-in-out, + width 150ms ease, + height 150ms ease, + transform 150ms ease; + } + + [data-slot="radio-group-item"] { + position: relative; + } + + /* Separator between items */ + [data-slot="radio-group-item"]:not(:first-of-type)::before { + background: var(--border-weak-base); + border-radius: var(--radius-xs); + content: ""; + inset: 6px 0; + position: absolute; + transition: opacity 150ms ease; + width: 1px; + transform: translateX(-0.5px); + } + + /* Hide separator when item or previous item is checked */ + [data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])::before, + [data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked]) + + [data-slot="radio-group-item"]::before { + opacity: 0; + } + + [data-slot="radio-group-item-label"] { + color: var(--text-weak); + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-medium); + border-radius: var(--radius-md); + cursor: pointer; + display: flex; + flex-wrap: nowrap; + gap: calc(var(--spacing) * 1); + line-height: 1; + padding: 6px 12px; + place-content: center; + position: relative; + transition-duration: 150ms; + transition-property: color, opacity; + transition-timing-function: ease-in-out; + user-select: none; + } + + [data-slot="radio-group-item-input"] { + all: unset; + } + + /* Checked state */ + [data-slot="radio-group-item-input"][data-checked] + [data-slot="radio-group-item-label"] { + color: var(--text-strong); + } + + /* Disabled state */ + [data-slot="radio-group-item-input"][data-disabled] + [data-slot="radio-group-item-label"] { + cursor: not-allowed; + opacity: 0.5; + } + + /* Hover state for unchecked, enabled items */ + [data-slot="radio-group-item-input"]:not([data-checked], [data-disabled]) + [data-slot="radio-group-item-label"] { + cursor: pointer; + user-select: none; + } + + [data-slot="radio-group-item-input"]:not([data-checked], [data-disabled]) + + [data-slot="radio-group-item-label"]:hover { + color: var(--text-base); + } + + [data-slot="radio-group-item-input"]:not([data-checked], [data-disabled]) + + [data-slot="radio-group-item-label"]:active { + opacity: 0.7; + } + + /* Focus state */ + [data-slot="radio-group-wrapper"]:has([data-slot="radio-group-item-input"]:focus-visible) + [data-slot="radio-group-indicator"] { + --indicator-focus-width: 2px; + } + + /* Hide indicator when nothing is checked */ + [data-slot="radio-group-wrapper"]:not(:has([data-slot="radio-group-item-input"][data-checked])) + [data-slot="radio-group-indicator"] { + --indicator-opacity: 0; + } + + /* Vertical orientation */ + &[aria-orientation="vertical"] [data-slot="radio-group-items"] { + flex-direction: column; + } + + &[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before { + height: 1px; + width: auto; + inset: 0 6px; + transform: translateY(-0.5px); + } + + /* Small size variant */ + &[data-size="small"] { + [data-slot="radio-group-item-label"] { + font-size: 12px; + padding: 4px 8px; + } + + [data-slot="radio-group-item"]:not(:first-of-type)::before { + inset: 4px 0; + } + + &[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before { + inset: 0 4px; + } + } + + /* Disabled root state */ + &[data-disabled] { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/packages/ui/src/components/radio-group.tsx b/packages/ui/src/components/radio-group.tsx new file mode 100644 index 00000000000..e1812d61a7f --- /dev/null +++ b/packages/ui/src/components/radio-group.tsx @@ -0,0 +1,75 @@ +import { SegmentedControl as Kobalte } from "@kobalte/core/segmented-control" +import { For, splitProps } from "solid-js" +import type { ComponentProps, JSX } from "solid-js" + +export type RadioGroupProps = Omit< + ComponentProps, + "value" | "defaultValue" | "onChange" | "children" +> & { + options: T[] + current?: T + defaultValue?: T + value?: (x: T) => string + label?: (x: T) => JSX.Element | string + onSelect?: (value: T | undefined) => void + class?: ComponentProps<"div">["class"] + classList?: ComponentProps<"div">["classList"] + size?: "small" | "medium" +} + +export function RadioGroup(props: RadioGroupProps) { + const [local, others] = splitProps(props, [ + "class", + "classList", + "options", + "current", + "defaultValue", + "value", + "label", + "onSelect", + "size", + ]) + + const getValue = (item: T): string => { + if (local.value) return local.value(item) + return String(item) + } + + const getLabel = (item: T): JSX.Element | string => { + if (local.label) return local.label(item) + return String(item) + } + + const findOption = (v: string): T | undefined => { + return local.options.find((opt) => getValue(opt) === v) + } + + return ( + local.onSelect?.(findOption(v))} + > +
+ +
+ + {(option) => ( + + + {getLabel(option)} + + )} + +
+
+
+ ) +} diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index c4302a4d394..5782d2a2929 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -29,6 +29,7 @@ @import "../components/message-nav.css" layer(components); @import "../components/popover.css" layer(components); @import "../components/progress-circle.css" layer(components); +@import "../components/radio-group.css" layer(components); @import "../components/resize-handle.css" layer(components); @import "../components/select.css" layer(components); @import "../components/spinner.css" layer(components); From 42f2bc719940312fd0017205a807f414156f133c Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:02:43 -0600 Subject: [PATCH 021/164] fix(desktop): can't collapse project with active session --- packages/app/src/pages/layout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index e8dd152e1fc..4a29329a87e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -9,6 +9,7 @@ import { ParentProps, Show, Switch, + untrack, type JSX, } from "solid-js" import { DateTime } from "luxon" @@ -323,7 +324,7 @@ export default function Layout(props: ParentProps) { const id = params.id setStore("lastSession", directory, id) notification.session.markViewed(id) - layout.projects.expand(directory) + untrack(() => layout.projects.expand(directory)) requestAnimationFrame(() => scrollToSession(id)) }) From d0a1b5ef96422d863c3fd1e9443486bb58f6c7b2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 01:03:22 +0000 Subject: [PATCH 022/164] 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 f55e8d9d3a1..805e9d3c187 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,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 7c3d5ab73d3..3b3c187860c 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 583751ecae8514642feacd0f440b1957c3ffcca9 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:07:30 -0600 Subject: [PATCH 023/164] fix(desktop): markdown rendering perf --- packages/ui/src/components/markdown.tsx | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index 380a3c8a46d..7615d1737a3 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -1,21 +1,6 @@ import { useMarked } from "../context/marked" import { ComponentProps, createResource, splitProps } from "solid-js" -function strip(text: string): string { - const trimmed = text.trim() - const match = trimmed.match(/^<([A-Za-z]\w*)>/) - if (!match) return text - - const tagName = match[1] - const closingTag = `` - if (trimmed.endsWith(closingTag)) { - const content = trimmed.slice(match[0].length, -closingTag.length) - return content.trim() - } - - return text -} - export function Markdown( props: ComponentProps<"div"> & { text: string @@ -26,7 +11,7 @@ export function Markdown( const [local, others] = splitProps(props, ["text", "class", "classList"]) const marked = useMarked() const [html] = createResource( - () => strip(local.text), + () => local.text, async (markdown) => { return marked.parse(markdown) }, From 5420702f693b08001d0d1edb2010b03648dbaa59 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:07:39 -0600 Subject: [PATCH 024/164] fix(desktop): missing keybinds in tooltips --- packages/app/src/pages/layout.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 4a29329a87e..5efba6d994b 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -519,7 +519,15 @@ export default function Layout(props: ParentProps) { + } + > archiveSession(props.session)} />
@@ -584,7 +592,15 @@ export default function Layout(props: ParentProps) { - + + New session + {command.keybind("session.new")} +
+ } + > From 7469cba7cf25ee1593fa9d1afefe6c0a5b553c3f Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:21:04 -0600 Subject: [PATCH 025/164] fix(desktop): move session context to top-right --- packages/app/src/components/prompt-input.tsx | 6 +++-- .../src/components/session-context-usage.tsx | 25 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 33e1f48900e..7a1a3fdfb86 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -989,7 +989,7 @@ export const PromptInput: Component = (props) => { onInput={handleInput} onKeyDown={handleKeyDown} classList={{ - "w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, + "w-full px-5 py-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, "[&_[data-type=file]]:text-icon-info-active": true, "font-mono!": store.mode === "shell", }} @@ -1001,6 +1001,9 @@ export const PromptInput: Component = (props) => { : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`} +
+ +
@@ -1057,7 +1060,6 @@ export const PromptInput: Component = (props) => { -
{(ctx) => ( -
- Tokens - {ctx().tokens} +
+
+ Tokens + {ctx().tokens}
-
- Usage - {ctx().percentage ?? 0}% +
+ Usage + {ctx().percentage ?? 0}%
-
- Cost - {cost()} +
+ Cost + {cost()}
} placement="top" > -
- {`${ctx().percentage ?? 0}%`} +
+ {/* {`${ctx().percentage ?? 0}%`} */}
)} From e9c2f1f3f3ebcebf4609c57144f6fa44933587d0 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:22:16 -0600 Subject: [PATCH 026/164] fix(desktop): padding --- packages/app/src/components/prompt-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 7a1a3fdfb86..03fa02fe35d 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -995,7 +995,7 @@ export const PromptInput: Component = (props) => { }} /> -
+
{store.mode === "shell" ? "Enter shell command..." : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`} From aaf9a5d4345f609853e3b3e2a51cb5c8d2e6ed63 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:45:20 -0600 Subject: [PATCH 027/164] fix(desktop): user message display --- packages/ui/src/components/message-part.css | 6 +----- packages/ui/src/components/session-turn.css | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index ffeb4cb2859..6daf1a8b513 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -72,14 +72,10 @@ } [data-slot="user-message-text"] { - display: -webkit-box; white-space: pre-wrap; - line-clamp: 3; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; overflow: hidden; background: var(--surface-inset-base); - padding: 2px 6px; + padding: 6px 12px; border-radius: 4px; border: 0.5px solid var(--border-weak-base); } diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 3861312ccd4..63c77e5ac78 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -26,7 +26,7 @@ align-items: flex-start; align-self: stretch; min-width: 0; - gap: 42px; + gap: 28px; overflow-anchor: none; } From b307075063ff04108164cedbfb9c2a9e2e310be9 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 25 Dec 2025 20:06:41 -0600 Subject: [PATCH 028/164] chore: brain icon --- packages/ui/src/components/icon.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 75a737d88d1..45ccee8f9bf 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -6,6 +6,7 @@ const icons = { "arrow-left": ``, archive: ``, "bubble-5": ``, + brain: ``, "bullet-list": ``, "check-small": ``, "chevron-down": ``, From effa7b45cfadc8a53b073f1375281a9de6ada670 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 26 Dec 2025 02:11:47 +0000 Subject: [PATCH 029/164] release: v1.0.202 --- 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 c0a490ba0ae..915f17cc127 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -77,7 +77,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -105,7 +105,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -132,7 +132,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -156,7 +156,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -180,7 +180,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@opencode-ai/app": "workspace:*", "@solid-primitives/storage": "catalog:", @@ -207,7 +207,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -236,7 +236,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -252,7 +252,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.201", + "version": "1.0.202", "bin": { "opencode": "./bin/opencode", }, @@ -346,7 +346,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -366,7 +366,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.201", + "version": "1.0.202", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -377,7 +377,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -390,7 +390,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -425,7 +425,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.201", + "version": "1.0.202", "dependencies": { "zod": "catalog:", }, @@ -436,7 +436,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.201", + "version": "1.0.202", "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 de314c18ac7..9fd9369598f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.0.201", + "version": "1.0.202", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 30bb4d7fd3f..9ff58134511 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.201", + "version": "1.0.202", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index e6eedfbfd9f..960f8772a02 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.0.201", + "version": "1.0.202", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 42857251be8..7dfdf6cd6bc 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.201", + "version": "1.0.202", "$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 3f8ec54edea..28112c8fe61 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.201", + "version": "1.0.202", "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 f994d660e78..768178999bd 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.0.201", + "version": "1.0.202", "type": "module", "scripts": { "typecheck": "tsgo -b", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index c291831453a..39fd7392a9d 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.0.201", + "version": "1.0.202", "private": true, "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 35555ba021f..bd74587abcd 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.0.201" +version = "1.0.202" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-linux-arm64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-linux-x64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.201/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 9eafac4e39b..fa5ba170e4d 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.201", + "version": "1.0.202", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 569a863a75f..ef6e3a16a46 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.201", + "version": "1.0.202", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 805e9d3c187..a24c1c0d812 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.0.201", + "version": "1.0.202", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -24,4 +24,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 3b3c187860c..a63c4122bb2 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.0.201", + "version": "1.0.202", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 766ea72c34e..6cf2eecea7f 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.201", + "version": "1.0.202", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 91db04d1463..7f3b511625b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.201", + "version": "1.0.202", "type": "module", "exports": { "./*": "./src/components/*.tsx", diff --git a/packages/util/package.json b/packages/util/package.json index 336837181b8..1c7b762858e 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.201", + "version": "1.0.202", "private": true, "type": "module", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 7e36b9e03e8..b406f93c871 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.201", + "version": "1.0.202", "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 eddcb6c8923..0628630e55b 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.0.201", + "version": "1.0.202", "publisher": "sst-dev", "repository": { "type": "git", From d9f0f582774fb75745077af29005fb9769bcbb0c Mon Sep 17 00:00:00 2001 From: Marco Date: Fri, 26 Dec 2025 04:04:43 +0100 Subject: [PATCH 030/164] feat: haskell lsp support (#6141) --- packages/opencode/src/lsp/language.ts | 1 + packages/opencode/src/lsp/server.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/opencode/src/lsp/language.ts b/packages/opencode/src/lsp/language.ts index 620944a8e07..d279f7d64e7 100644 --- a/packages/opencode/src/lsp/language.ts +++ b/packages/opencode/src/lsp/language.ts @@ -39,6 +39,7 @@ export const LANGUAGE_EXTENSIONS: Record = { ".hbs": "handlebars", ".handlebars": "handlebars", ".hs": "haskell", + ".lhs": "haskell", ".html": "html", ".htm": "html", ".ini": "ini", diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index b432e5a5d0a..0610aa2d07e 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1892,4 +1892,22 @@ export namespace LSPServer { } }, } + + export const HLS: Info = { + id: "haskell-language-server", + extensions: [".hs", ".lhs"], + root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]), + async spawn(root) { + const bin = Bun.which("haskell-language-server-wrapper") + if (!bin) { + log.info("haskell-language-server-wrapper not found, please install haskell-language-server") + return + } + return { + process: spawn(bin, ["--lsp"], { + cwd: root, + }), + } + }, + } } From 8886c78dced9c053adf83b413f86af8ae683fddf Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 03:05:15 +0000 Subject: [PATCH 031/164] 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 a24c1c0d812..3b91c43c4d7 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,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 a63c4122bb2..9f0993f4c3d 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From f59d274d0fbd25bc41f2ac4fe187cffafb1f946a Mon Sep 17 00:00:00 2001 From: Donghyun Shin <59863085+apersomany@users.noreply.github.com> Date: Fri, 26 Dec 2025 12:17:54 +0900 Subject: [PATCH 032/164] fix(lsp): make JDTLS use the correct config directory on Windows (#6121) --- packages/opencode/src/lsp/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 0610aa2d07e..ba51ba663a3 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1175,7 +1175,7 @@ export namespace LSPServer { case "linux": return "config_linux" case "win32": - return "config_windows" + return "config_win" default: return "config_linux" } From 281ce4c0c3703c0cab5b5bed411561fe2a3bdc5a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 25 Dec 2025 23:06:46 -0500 Subject: [PATCH 033/164] prompt update to prevent searching via bash tool --- packages/opencode/src/tool/bash.txt | 178 +++++++++++----------------- 1 file changed, 68 insertions(+), 110 deletions(-) diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index c14b6d75b30..a81deb62bf2 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -2,143 +2,103 @@ Executes a given bash command in a persistent shell session with optional timeou All commands run in ${directory} by default. Use the `workdir` parameter if you need to run a command in a different directory. +IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead. + Before executing the command, please follow these steps: 1. Directory Verification: - - If the command will create new directories or files, first use the List tool to verify the parent directory exists and is the correct location - - For example, before running "mkdir foo/bar", first use List to check that "foo" exists and is the intended parent directory + - If the command will create new directories or files, first use `ls` to verify the parent directory exists and is the correct location + - For example, before running "mkdir foo/bar", first use `ls foo` to check that "foo" exists and is the intended parent directory 2. Command Execution: - - Always quote file paths that contain spaces with double quotes (e.g., rm "path with spaces/file.txt") + - Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt") - Examples of proper quoting: - - mkdir "/Users/name/My Documents" (correct) - - mkdir /Users/name/My Documents (incorrect - will fail) + - cd "/Users/name/My Documents" (correct) + - cd /Users/name/My Documents (incorrect - will fail) - python "/path/with spaces/script.py" (correct) - python /path/with spaces/script.py (incorrect - will fail) - After ensuring proper quoting, execute the command. - Capture the output of the command. Usage notes: - - The command argument is required. - - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). - If not specified, commands will timeout after 120000ms (2 minutes). - - The description argument is required. You must write a clear, concise description of what this command does in 5-10 words. - - If the output exceeds 30000 characters, output will be truncated before being - returned to you. - - Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or - `echo` commands, unless explicitly instructed or when these commands are truly necessary - for the task. Instead, always prefer using the dedicated tools for these commands: - - File search: Use Glob (NOT find or ls) - - Content search: Use Grep (NOT grep or rg) - - Read files: Use Read (NOT cat/head/tail) - - Edit files: Use Edit (NOT sed/awk) - - Write files: Use Write (NOT echo >/cat < - pytest /foo/bar/tests - - - cd /foo/bar && pytest tests - - -# Working Directory - -The `workdir` parameter sets the working directory for command execution. Prefer using `workdir` over `cd &&` command chains when you simply need to run a command in a different directory. - - -workdir="/foo/bar", command="pytest tests" - - -command="pytest /foo/bar/tests" - - -command="cd /foo/bar && pytest tests" - + - The command argument is required. + - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes). + - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. + - If the output exceeds 30000 characters, output will be truncated before being returned to you. + - You can use the `run_in_background` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the Bash tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter. + + - Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands: + - File search: Use Glob (NOT find or ls) + - Content search: Use Grep (NOT grep or rg) + - Read files: Use Read (NOT cat/head/tail) + - Edit files: Use Edit (NOT sed/awk) + - Write files: Use Write (NOT echo >/cat < + pytest /foo/bar/tests + + + cd /foo/bar && pytest tests + # Committing changes with git -IMPORTANT: ONLY COMMIT IF THE USER ASKS YOU TO. - -If and only if the user asks you to create a new git commit, follow these steps carefully: - -1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel, each using the Bash tool: - - Run a git status command to see all untracked files. - - Run a git diff command to see both staged and unstaged changes that will be committed. - - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style. - -2. Analyze all staged changes (both previously staged and newly added) and draft a commit message. When analyzing: - -- List the files that have been changed or added -- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.) -- Brainstorm the purpose or motivation behind these changes -- Assess the impact of these changes on the overall project -- Check for any sensitive information that shouldn't be committed -- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what" -- Ensure your language is clear, concise, and to the point -- Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.) -- Ensure the message is not generic (avoid words like "Update" or "Fix" without context) -- Review the draft message to ensure it accurately reflects the changes and their purpose +Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully: -3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel: +Git Safety Protocol: +- NEVER update the git config +- NEVER run destructive/irreversible git commands (like push --force, hard reset, etc) unless the user explicitly requests them +- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it +- NEVER run force push to main/master, warn the user if they request it +- Avoid git commit --amend. ONLY use --amend when ALL conditions are met: + (1) User explicitly requested amend, OR commit SUCCEEDED but pre-commit hook auto-modified files that need including + (2) HEAD commit was created by you in this conversation (verify: git log -1 --format='%an %ae') + (3) Commit has NOT been pushed to remote (verify: git status shows "Your branch is ahead") +- CRITICAL: If commit FAILED or was REJECTED by hook, NEVER amend - fix the issue and create a NEW commit +- CRITICAL: If you already pushed to remote, NEVER amend unless user explicitly requests it (requires force push) +- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. + +1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel, each using the Bash tool: + - Run a git status command to see all untracked files. + - Run a git diff command to see both staged and unstaged changes that will be committed. + - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style. +2. Analyze all staged changes (both previously staged and newly added) and draft a commit message: + - Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.). + - Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files + - Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what" + - Ensure it accurately reflects the changes and their purpose +3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands: - Add relevant untracked files to the staging area. - - Run git status to make sure the commit succeeded. - -4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them. + - Create the commit with a message + - Run git status after the commit completes to verify success. + Note: git status depends on the commit completing, so run it sequentially after the commit. +4. If the commit fails due to pre-commit hook, fix the issue and create a NEW commit (see amend rules above) Important notes: -- Use the git context at the start of this conversation to determine which files are relevant to your commit. Be careful not to stage and commit files (e.g. with `git add .`) that aren't relevant to your commit. -- NEVER update the git config -- DO NOT run additional commands to read or explore code, beyond what is available in the git context -- DO NOT push to the remote repository +- NEVER run additional commands to read or explore code, besides git bash commands +- NEVER use the TodoWrite or Task tools +- DO NOT push to the remote repository unless the user explicitly asks you to do so - IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported. - If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit -- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them. -- Return an empty response - the user will see the git output directly # Creating pull requests Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed. IMPORTANT: When the user asks you to create a pull request, follow these steps carefully: -1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch: +1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch: - Run a git status command to see all untracked files - Run a git diff command to see both staged and unstaged changes that will be committed - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote - - Run a git log command and `git diff main...HEAD` to understand the full commit history for the current branch (from the time it diverged from the `main` branch) - -2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary. Wrap your analysis process in tags: - - -- List the commits since diverging from the main branch -- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.) -- Brainstorm the purpose or motivation behind these changes -- Assess the impact of these changes on the overall project -- Do not use tools to explore code, beyond what is available in the git context -- Check for any sensitive information that shouldn't be committed -- Draft a concise (1-2 bullet points) pull request summary that focuses on the "why" rather than the "what" -- Ensure the summary accurately reflects all changes since diverging from the main branch -- Ensure your language is clear, concise, and to the point -- Ensure the summary accurately reflects the changes and their purpose (ie. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.) -- Ensure the summary is not generic (avoid words like "Update" or "Fix" without context) -- Review the draft summary to ensure it accurately reflects the changes and their purpose - - -3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel: + - Run a git log command and `git diff [base-branch]...HEAD` to understand the full commit history for the current branch (from the time it diverged from the base branch) +2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary +3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands in parallel: - Create new branch if needed - Push to remote with -u flag if needed - Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting. @@ -146,12 +106,10 @@ IMPORTANT: When the user asks you to create a pull request, follow these steps c gh pr create --title "the pr title" --body "$(cat <<'EOF' ## Summary <1-3 bullet points> -EOF -)" Important: -- NEVER update the git config +- DO NOT use the TodoWrite or Task tools - Return the PR URL when you're done, so the user can see it # Other common operations From 7cc4b24ac2a57ab46e0dcbc3f1cb91d5f1c011be Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 26 Dec 2025 04:10:11 +0000 Subject: [PATCH 034/164] release: v1.0.203 --- 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 915f17cc127..796cd5661e4 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -77,7 +77,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -105,7 +105,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -132,7 +132,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -156,7 +156,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -180,7 +180,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@opencode-ai/app": "workspace:*", "@solid-primitives/storage": "catalog:", @@ -207,7 +207,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -236,7 +236,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -252,7 +252,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.202", + "version": "1.0.203", "bin": { "opencode": "./bin/opencode", }, @@ -346,7 +346,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -366,7 +366,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.202", + "version": "1.0.203", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -377,7 +377,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -390,7 +390,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -425,7 +425,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.202", + "version": "1.0.203", "dependencies": { "zod": "catalog:", }, @@ -436,7 +436,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.202", + "version": "1.0.203", "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 9fd9369598f..4fc9678e70e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.0.202", + "version": "1.0.203", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 9ff58134511..a12dc87f24d 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.202", + "version": "1.0.203", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 960f8772a02..4f6d2717fb7 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.0.202", + "version": "1.0.203", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 7dfdf6cd6bc..572a86ddd5e 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.202", + "version": "1.0.203", "$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 28112c8fe61..1b2869dd9ec 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.202", + "version": "1.0.203", "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 768178999bd..4bdb5ce3886 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.0.202", + "version": "1.0.203", "type": "module", "scripts": { "typecheck": "tsgo -b", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 39fd7392a9d..a89e5df7ef7 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.0.202", + "version": "1.0.203", "private": true, "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index bd74587abcd..e21818e4629 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.0.202" +version = "1.0.203" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-linux-arm64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-linux-x64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index fa5ba170e4d..160e78b35fd 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.202", + "version": "1.0.203", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ef6e3a16a46..f04f0bd8715 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.202", + "version": "1.0.203", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 3b91c43c4d7..94930fa446a 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.0.202", + "version": "1.0.203", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -24,4 +24,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 9f0993f4c3d..f1e0f77a750 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.0.202", + "version": "1.0.203", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 6cf2eecea7f..4c2f8eb7356 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.202", + "version": "1.0.203", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 7f3b511625b..0e7da54bdcb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.202", + "version": "1.0.203", "type": "module", "exports": { "./*": "./src/components/*.tsx", diff --git a/packages/util/package.json b/packages/util/package.json index 1c7b762858e..c5df6f176bc 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.202", + "version": "1.0.203", "private": true, "type": "module", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index b406f93c871..2fb471239b7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.202", + "version": "1.0.203", "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 0628630e55b..1b4cf99f985 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.0.202", + "version": "1.0.203", "publisher": "sst-dev", "repository": { "type": "git", From 46c7a41d5f9d1e40e31a81b00e4fcd422843c69e Mon Sep 17 00:00:00 2001 From: JackNorris Date: Fri, 26 Dec 2025 04:24:48 +0000 Subject: [PATCH 035/164] fix: only show diagnostics block when errors exist (#6175) --- packages/opencode/src/tool/edit.ts | 4 ++-- packages/opencode/src/tool/write.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index b49bd7abe00..62679974648 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -142,8 +142,8 @@ export const EditTool = Tool.define("edit", { const diagnostics = await LSP.diagnostics() const normalizedFilePath = Filesystem.normalizePath(filePath) const issues = diagnostics[normalizedFilePath] ?? [] - if (issues.length > 0) { - const errors = issues.filter((item) => item.severity === 1) + const errors = issues.filter((item) => item.severity === 1) + if (errors.length > 0) { 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` : "" diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 6b8fd3dd111..a0e87299a44 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -83,11 +83,11 @@ export const WriteTool = Tool.define("write", { const normalizedFilepath = Filesystem.normalizePath(filepath) let projectDiagnosticsCount = 0 for (const [file, issues] of Object.entries(diagnostics)) { - if (issues.length === 0) continue - const sorted = issues.toSorted((a, b) => (a.severity ?? 4) - (b.severity ?? 4)) - const limited = sorted.slice(0, MAX_DIAGNOSTICS_PER_FILE) + const errors = issues.filter((item) => item.severity === 1) + if (errors.length === 0) continue + const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE) const suffix = - issues.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${issues.length - MAX_DIAGNOSTICS_PER_FILE} more` : "" + 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` continue From b6a264819e5ce5a59906aba07258c0ea9034786a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 04:25:19 +0000 Subject: [PATCH 036/164] 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 94930fa446a..63abdf3e471 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,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 f1e0f77a750..292e8163acd 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 3522c460e3dc4f6e66da9ff2d62a2b1b1e59a2ff Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 25 Dec 2025 22:46:12 -0600 Subject: [PATCH 037/164] tweak: update transform for gemini models so that topP and topK match gemini-cli values --- packages/opencode/src/provider/transform.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index d86fe90222d..407f7351b5b 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -214,7 +214,7 @@ export namespace ProviderTransform { const id = model.id.toLowerCase() if (id.includes("qwen")) return 0.55 if (id.includes("claude")) return undefined - if (id.includes("gemini-3-pro")) return 1.0 + if (id.includes("gemini")) return 1.0 if (id.includes("glm-4.6")) return 1.0 if (id.includes("glm-4.7")) return 1.0 if (id.includes("minimax-m2")) return 1.0 @@ -232,12 +232,14 @@ export namespace ProviderTransform { if (id.includes("m2.1")) return 0.9 return 0.95 } + if (id.includes("gemini")) return 0.95 return undefined } export function topK(model: Provider.Model) { const id = model.id.toLowerCase() if (id.includes("minimax-m2")) return 20 + if (id.includes("gemini")) return 64 return undefined } From 2946a6d9a78466c19637cb75473769b1deb6f03e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 12:10:30 +0000 Subject: [PATCH 038/164] ignore: update download stats 2025-12-26 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index d3c6f57ad40..77a9d296cfc 100644 --- a/STATS.md +++ b/STATS.md @@ -181,3 +181,4 @@ | 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) | | 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) | | 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) | +| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) | From cf388847786fca1c9820f2a386056ddf0dce08c0 Mon Sep 17 00:00:00 2001 From: Didier Durand <2927957+didier-durand@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:21:33 +0100 Subject: [PATCH 039/164] doc: fix typos in various files (#6196) --- README.md | 4 ++-- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- .../opencode/src/session/prompt/plan-reminder-anthropic.txt | 2 +- packages/opencode/src/tool/grep.txt | 2 +- packages/web/astro.config.mjs | 2 +- packages/web/src/content/docs/formatters.mdx | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5295810b6f0..b68195abdbe 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ you can switch between these using the `Tab` key. - Asks permission before running bash commands - Ideal for exploring unfamiliar codebases or planning changes -Also, included is a **general** subagent for complex searches and multi-step tasks. +Also, included is a **general** subagent for complex searches and multistep tasks. This is used internally and can be invoked using `@general` in messages. Learn more about [agents](https://opencode.ai/docs/agents). @@ -98,7 +98,7 @@ If you are working on a project that's related to OpenCode and is using "opencod ### FAQ -#### How is this different than Claude Code? +#### How is this different from Claude Code? It's very similar to Claude Code in terms of capability. Here are the key differences: diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 13c95d9b9ea..5214b0c1a9a 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -539,7 +539,7 @@ function App() { sdk.event.on(SessionApi.Event.Error.type, (evt) => { const error = evt.properties.error const message = (() => { - if (!error) return "An error occured" + if (!error) return "An error occurred" if (typeof error === "object") { const data = error.data diff --git a/packages/opencode/src/session/prompt/plan-reminder-anthropic.txt b/packages/opencode/src/session/prompt/plan-reminder-anthropic.txt index a5c2f267e07..28f1e629dbe 100644 --- a/packages/opencode/src/session/prompt/plan-reminder-anthropic.txt +++ b/packages/opencode/src/session/prompt/plan-reminder-anthropic.txt @@ -1,7 +1,7 @@ # Plan Mode - System Reminder -Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received. +Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received. --- diff --git a/packages/opencode/src/tool/grep.txt b/packages/opencode/src/tool/grep.txt index 6067ef27b9d..adf583695ae 100644 --- a/packages/opencode/src/tool/grep.txt +++ b/packages/opencode/src/tool/grep.txt @@ -5,4 +5,4 @@ - Returns file paths and line numbers with at least one match sorted by modification time - Use this tool when you need to find files containing specific patterns - If you need to identify/count the number of matches within files, use the Bash tool with `rg` (ripgrep) directly. Do NOT use `grep`. -- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead +- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 7ecf2bfd9d6..dba43d02fa3 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -36,7 +36,7 @@ export default defineConfig({ expressiveCode: { themes: ["github-light", "github-dark"] }, social: [ { icon: "github", label: "GitHub", href: config.github }, - { icon: "discord", label: "Dscord", href: config.discord }, + { icon: "discord", label: "Discord", href: config.discord }, ], editLink: { baseUrl: `${config.github}/edit/dev/packages/web/`, diff --git a/packages/web/src/content/docs/formatters.mdx b/packages/web/src/content/docs/formatters.mdx index c2c01836bb3..885a95da914 100644 --- a/packages/web/src/content/docs/formatters.mdx +++ b/packages/web/src/content/docs/formatters.mdx @@ -30,7 +30,7 @@ OpenCode comes with several built-in formatters for popular languages and framew | ocamlformat | .ml, .mli | `ocamlformat` command available and `.ocamlformat` config file | | terraform | .tf, .tfvars | `terraform` command available | | gleam | .gleam | `gleam` command available | -| oxfmt (Experimental) | .js, .jsx, .ts, .tsx | `oxfmt` dependency in `package.json` and an [experiental env variable flag](/docs/cli/#experimental) | +| oxfmt (Experimental) | .js, .jsx, .ts, .tsx | `oxfmt` dependency in `package.json` and an [experimental env variable flag](/docs/cli/#experimental) | So if your project has `prettier` in your `package.json`, OpenCode will automatically use it. From dd569c927a1795804d4f3bd6753ad904c4723252 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 16:22:05 +0000 Subject: [PATCH 040/164] chore: generate --- packages/web/src/content/docs/formatters.mdx | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/web/src/content/docs/formatters.mdx b/packages/web/src/content/docs/formatters.mdx index 885a95da914..fa915f116d4 100644 --- a/packages/web/src/content/docs/formatters.mdx +++ b/packages/web/src/content/docs/formatters.mdx @@ -11,25 +11,25 @@ OpenCode automatically formats files after they are written or edited using lang OpenCode comes with several built-in formatters for popular languages and frameworks. Below is a list of the formatters, supported file extensions, and commands or config options it needs. -| Formatter | Extensions | Requirements | -| -------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| gofmt | .go | `gofmt` command available | -| mix | .ex, .exs, .eex, .heex, .leex, .neex, .sface | `mix` command available | -| prettier | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, and [more](https://prettier.io/docs/en/index.html) | `prettier` dependency in `package.json` | -| biome | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, and [more](https://biomejs.dev/) | `biome.json(c)` config file | -| zig | .zig, .zon | `zig` command available | -| clang-format | .c, .cpp, .h, .hpp, .ino, and [more](https://clang.llvm.org/docs/ClangFormat.html) | `.clang-format` config file | -| ktlint | .kt, .kts | `ktlint` command available | -| ruff | .py, .pyi | `ruff` command available with config | -| uv | .py, .pyi | `uv` command available | -| rubocop | .rb, .rake, .gemspec, .ru | `rubocop` command available | -| standardrb | .rb, .rake, .gemspec, .ru | `standardrb` command available | -| htmlbeautifier | .erb, .html.erb | `htmlbeautifier` command available | -| air | .R | `air` command available | -| dart | .dart | `dart` command available | -| ocamlformat | .ml, .mli | `ocamlformat` command available and `.ocamlformat` config file | -| terraform | .tf, .tfvars | `terraform` command available | -| gleam | .gleam | `gleam` command available | +| Formatter | Extensions | Requirements | +| -------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| gofmt | .go | `gofmt` command available | +| mix | .ex, .exs, .eex, .heex, .leex, .neex, .sface | `mix` command available | +| prettier | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, and [more](https://prettier.io/docs/en/index.html) | `prettier` dependency in `package.json` | +| biome | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, and [more](https://biomejs.dev/) | `biome.json(c)` config file | +| zig | .zig, .zon | `zig` command available | +| clang-format | .c, .cpp, .h, .hpp, .ino, and [more](https://clang.llvm.org/docs/ClangFormat.html) | `.clang-format` config file | +| ktlint | .kt, .kts | `ktlint` command available | +| ruff | .py, .pyi | `ruff` command available with config | +| uv | .py, .pyi | `uv` command available | +| rubocop | .rb, .rake, .gemspec, .ru | `rubocop` command available | +| standardrb | .rb, .rake, .gemspec, .ru | `standardrb` command available | +| htmlbeautifier | .erb, .html.erb | `htmlbeautifier` command available | +| air | .R | `air` command available | +| dart | .dart | `dart` command available | +| ocamlformat | .ml, .mli | `ocamlformat` command available and `.ocamlformat` config file | +| terraform | .tf, .tfvars | `terraform` command available | +| gleam | .gleam | `gleam` command available | | oxfmt (Experimental) | .js, .jsx, .ts, .tsx | `oxfmt` dependency in `package.json` and an [experimental env variable flag](/docs/cli/#experimental) | So if your project has `prettier` in your `package.json`, OpenCode will automatically use it. From 26e7043718fbf5fbf08eecd04ff8ed5edd82e33e Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 26 Dec 2025 10:29:48 -0600 Subject: [PATCH 041/164] feat(core): optional mdns service (#6192) Co-authored-by: Github Action --- bun.lock | 11 ++++ flake.lock | 6 +-- nix/hashes.json | 2 +- packages/opencode/package.json | 1 + packages/opencode/src/cli/cmd/acp.ts | 30 ++++------- packages/opencode/src/cli/cmd/serve.ts | 25 +++------ packages/opencode/src/cli/cmd/tui/spawn.ts | 28 ++++------ packages/opencode/src/cli/cmd/tui/thread.ts | 22 +++----- packages/opencode/src/cli/cmd/tui/worker.ts | 2 +- packages/opencode/src/cli/cmd/web.ts | 31 ++++------- packages/opencode/src/cli/network.ts | 42 +++++++++++++++ packages/opencode/src/config/config.ts | 12 +++++ packages/opencode/src/server/mdns.ts | 57 +++++++++++++++++++++ packages/opencode/src/server/server.ts | 33 ++++++++++-- packages/web/src/content/docs/cli.mdx | 18 ++++--- packages/web/src/content/docs/config.mdx | 25 +++++++++ packages/web/src/content/docs/server.mdx | 9 ++-- 17 files changed, 238 insertions(+), 116 deletions(-) create mode 100644 packages/opencode/src/cli/network.ts create mode 100644 packages/opencode/src/server/mdns.ts diff --git a/bun.lock b/bun.lock index 796cd5661e4..818dee4f228 100644 --- a/bun.lock +++ b/bun.lock @@ -292,6 +292,7 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", + "bonjour-service": "1.3.0", "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", @@ -1081,6 +1082,8 @@ "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], @@ -2003,6 +2006,8 @@ "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], @@ -2247,6 +2252,8 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -3023,6 +3030,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], "mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="], @@ -3595,6 +3604,8 @@ "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], + "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], diff --git a/flake.lock b/flake.lock index 4ff2c1d0e11..8bba6eeb3df 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1766532406, - "narHash": "sha256-acLU/ag9VEoKkzOD202QASX25nG1eArXg5A0mHjKgxM=", + "lastModified": 1766747458, + "narHash": "sha256-m63jjuo/ygo8ztkCziYh5OOIbTSXUDkKbqw3Vuqu4a4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8142186f001295e5a3239f485c8a49bf2de2695a", + "rev": "c633f572eded8c4f3c75b8010129854ed404a6ce", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index 66c0baaf791..4363085de3b 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-hotsyeWJA6/dP6DvZTN1Ak2RSKcsyvXlXPI/jexBHME=" + "nodeModules": "sha256-okbViEKf1mRSmzbJgKdB9SJ875q84Bwu8d3ChHuaQ1g=" } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index f04f0bd8715..ef2b822e86e 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -81,6 +81,7 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", + "bonjour-service": "1.3.0", "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index c607e5f5bb7..2db64e3b1af 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -3,8 +3,10 @@ import { bootstrap } from "../bootstrap" import { cmd } from "./cmd" import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" import { ACP } from "@/acp/agent" +import { Config } from "@/config/config" import { Server } from "@/server/server" import { createOpencodeClient } from "@opencode-ai/sdk/v2" +import { withNetworkOptions, resolveNetworkOptions } from "../network" const log = Log.create({ service: "acp-command" }) @@ -19,29 +21,17 @@ export const AcpCommand = cmd({ command: "acp", describe: "start ACP (Agent Client Protocol) server", builder: (yargs) => { - return yargs - .option("cwd", { - describe: "working directory", - type: "string", - default: process.cwd(), - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }) + return withNetworkOptions(yargs).option("cwd", { + describe: "working directory", + type: "string", + default: process.cwd(), + }) }, handler: async (args) => { await bootstrap(process.cwd(), async () => { - const server = Server.listen({ - port: args.port, - hostname: args.hostname, - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}`, diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 3af3316a9d3..0fd7aa88f32 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,29 +1,16 @@ +import { Config } from "../../config/config" import { Server } from "../../server/server" import { cmd } from "./cmd" +import { withNetworkOptions, resolveNetworkOptions } from "../network" export const ServeCommand = cmd({ command: "serve", - builder: (yargs) => - yargs - .option("port", { - alias: ["p"], - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), + builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { - const hostname = args.hostname - const port = args.port - const server = Server.listen({ - port, - hostname, - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) console.log(`opencode server listening on http://${server.hostname}:${server.port}`) await new Promise(() => {}) await server.stop() diff --git a/packages/opencode/src/cli/cmd/tui/spawn.ts b/packages/opencode/src/cli/cmd/tui/spawn.ts index fa679529890..7ab846428d7 100644 --- a/packages/opencode/src/cli/cmd/tui/spawn.ts +++ b/packages/opencode/src/cli/cmd/tui/spawn.ts @@ -1,33 +1,23 @@ import { cmd } from "@/cli/cmd/cmd" +import { Config } from "@/config/config" import { Instance } from "@/project/instance" import path from "path" import { Server } from "@/server/server" import { upgrade } from "@/cli/upgrade" +import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" export const TuiSpawnCommand = cmd({ command: "spawn [project]", builder: (yargs) => - yargs - .positional("project", { - type: "string", - describe: "path to start opencode in", - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), + withNetworkOptions(yargs).positional("project", { + type: "string", + describe: "path to start opencode in", + }), handler: async (args) => { upgrade() - const server = Server.listen({ - port: args.port, - hostname: "127.0.0.1", - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) const bin = process.execPath const cmd = [] let cwd = process.cwd() diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 3cf8937a974..f75e3bd6511 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -6,6 +6,8 @@ import path from "path" import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" +import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import { Config } from "@/config/config" declare global { const OPENCODE_WORKER_PATH: string @@ -15,7 +17,7 @@ export const TuiThreadCommand = cmd({ command: "$0 [project]", describe: "start opencode tui", builder: (yargs) => - yargs + withNetworkOptions(yargs) .positional("project", { type: "string", describe: "path to start opencode in", @@ -36,23 +38,12 @@ export const TuiThreadCommand = cmd({ describe: "session id to continue", }) .option("prompt", { - alias: ["p"], type: "string", describe: "prompt to use", }) .option("agent", { type: "string", describe: "agent to use", - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", }), handler: async (args) => { // Resolve relative paths against PWD to preserve behavior when using --cwd flag @@ -87,10 +78,9 @@ export const TuiThreadCommand = cmd({ process.on("unhandledRejection", (e) => { Log.Default.error(e) }) - const server = await client.call("server", { - port: args.port, - hostname: args.hostname, - }) + const config = await Config.get() + const networkOpts = resolveNetworkOptions(args, config) + const server = await client.call("server", networkOpts) const prompt = await iife(async () => { const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined if (!args.prompt) return piped diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 76f78f3faa8..3ffc45ae884 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -30,7 +30,7 @@ process.on("uncaughtException", (e) => { let server: Bun.Server export const rpc = { - async server(input: { port: number; hostname: string }) { + async server(input: { port: number; hostname: string; mdns?: boolean }) { if (server) await server.stop(true) try { server = Server.listen(input) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 3d3036b1b07..adede03859c 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -1,6 +1,8 @@ +import { Config } from "../../config/config" import { Server } from "../../server/server" import { UI } from "../ui" import { cmd } from "./cmd" +import { withNetworkOptions, resolveNetworkOptions } from "../network" import open from "open" import { networkInterfaces } from "os" @@ -28,32 +30,17 @@ function getNetworkIPs() { export const WebCommand = cmd({ command: "web", - builder: (yargs) => - yargs - .option("port", { - alias: ["p"], - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), + builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { - const hostname = args.hostname - const port = args.port - const server = Server.listen({ - port, - hostname, - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) UI.empty() UI.println(UI.logo(" ")) UI.empty() - if (hostname === "0.0.0.0") { + if (opts.hostname === "0.0.0.0") { // Show localhost for local access const localhostUrl = `http://localhost:${server.port}` UI.println(UI.Style.TEXT_INFO_BOLD + " Local access: ", UI.Style.TEXT_NORMAL, localhostUrl) @@ -70,6 +57,10 @@ export const WebCommand = cmd({ } } + if (opts.mdns) { + UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, "opencode.local") + } + // Open localhost in browser open(localhostUrl.toString()).catch(() => {}) } else { diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts new file mode 100644 index 00000000000..661688572bd --- /dev/null +++ b/packages/opencode/src/cli/network.ts @@ -0,0 +1,42 @@ +import type { Argv, InferredOptionTypes } from "yargs" +import type { Config } from "../config/config" + +const options = { + port: { + type: "number" as const, + describe: "port to listen on", + default: 0, + }, + hostname: { + type: "string" as const, + describe: "hostname to listen on", + default: "127.0.0.1", + }, + mdns: { + type: "boolean" as const, + describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)", + default: false, + }, +} + +export type NetworkOptions = InferredOptionTypes + +export function withNetworkOptions(yargs: Argv) { + return yargs.options(options) +} + +export function resolveNetworkOptions(args: NetworkOptions, config?: Config.Info) { + const portExplicitlySet = process.argv.includes("--port") + const hostnameExplicitlySet = process.argv.includes("--hostname") + const mdnsExplicitlySet = process.argv.includes("--mdns") + + const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns) + const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port) + const hostname = hostnameExplicitlySet + ? args.hostname + : mdns && !config?.server?.hostname + ? "0.0.0.0" + : (config?.server?.hostname ?? args.hostname) + + return { hostname, port, mdns } +} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ba9d1973025..9187ed6745a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -587,6 +587,17 @@ export namespace Config { .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), }) + export const Server = z + .object({ + port: z.number().int().positive().optional().describe("Port to listen on"), + hostname: z.string().optional().describe("Hostname to listen on"), + mdns: z.boolean().optional().describe("Enable mDNS service discovery"), + }) + .strict() + .meta({ + ref: "ServerConfig", + }) + export const Layout = z.enum(["auto", "stretch"]).meta({ ref: "LayoutConfig", }) @@ -634,6 +645,7 @@ export namespace Config { theme: z.string().optional().describe("Theme name to use for the interface"), keybinds: Keybinds.optional().describe("Custom keybind configurations"), tui: TUI.optional().describe("TUI specific settings"), + server: Server.optional().describe("Server configuration for opencode serve and web commands"), command: z .record(z.string(), Command) .optional() diff --git a/packages/opencode/src/server/mdns.ts b/packages/opencode/src/server/mdns.ts new file mode 100644 index 00000000000..45e61d361ac --- /dev/null +++ b/packages/opencode/src/server/mdns.ts @@ -0,0 +1,57 @@ +import { Log } from "@/util/log" +import Bonjour from "bonjour-service" + +const log = Log.create({ service: "mdns" }) + +export namespace MDNS { + let bonjour: Bonjour | undefined + let currentPort: number | undefined + + export function publish(port: number, name = "opencode") { + if (currentPort === port) return + if (bonjour) unpublish() + + try { + bonjour = new Bonjour() + const service = bonjour.publish({ + name, + type: "http", + port, + txt: { path: "/" }, + }) + + service.on("up", () => { + log.info("mDNS service published", { name, port }) + }) + + service.on("error", (err) => { + log.error("mDNS service error", { error: err }) + }) + + currentPort = port + } catch (err) { + log.error("mDNS publish failed", { error: err }) + if (bonjour) { + try { + bonjour.destroy() + } catch {} + } + bonjour = undefined + currentPort = undefined + } + } + + export function unpublish() { + if (bonjour) { + try { + bonjour.unpublishAll() + bonjour.destroy() + } catch (err) { + log.error("mDNS unpublish failed", { error: err }) + } + bonjour = undefined + currentPort = undefined + log.info("mDNS service unpublished") + } + } +} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index c74dbbb41ef..65393e12897 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -45,9 +45,11 @@ import { Snapshot } from "@/snapshot" import { SessionSummary } from "@/session/summary" import { SessionStatus } from "@/session/status" import { upgradeWebSocket, websocket } from "hono/bun" +import type { BunWebSocketData } from "hono/bun" import { errors } from "./error" import { Pty } from "@/pty" import { Installation } from "@/installation" +import { MDNS } from "./mdns" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false @@ -2623,20 +2625,41 @@ export namespace Server { return result } - export function listen(opts: { port: number; hostname: string }) { + export function listen(opts: { port: number; hostname: string; mdns?: boolean }) { const args = { hostname: opts.hostname, idleTimeout: 0, fetch: App().fetch, websocket: websocket, } as const - if (opts.port === 0) { + const tryServe = (port: number) => { try { - return Bun.serve({ ...args, port: 4096 }) + return Bun.serve({ ...args, port }) } catch { - // port 4096 not available, fall through to use port 0 + return undefined } } - return Bun.serve({ ...args, port: opts.port }) + const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port) + if (!server) throw new Error(`Failed to start server on port ${opts.port}`) + + const shouldPublishMDNS = + opts.mdns && + server.port && + opts.hostname !== "127.0.0.1" && + opts.hostname !== "localhost" && + opts.hostname !== "::1" + if (shouldPublishMDNS) { + MDNS.publish(server.port!) + } else if (opts.mdns) { + log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") + } + + const originalStop = server.stop.bind(server) + server.stop = async (closeActiveConnections?: boolean) => { + if (shouldPublishMDNS) MDNS.unpublish() + return originalStop(closeActiveConnections) + } + + return server } } diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index e4e40ac7a4c..4a826e5b3ff 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -335,10 +335,11 @@ This starts an HTTP server that provides API access to opencode functionality wi #### Flags -| Flag | Short | Description | -| ------------ | ----- | --------------------- | -| `--port` | `-p` | Port to listen on | -| `--hostname` | | Hostname to listen on | +| Flag | Description | +| ------------ | --------------------- | +| `--port` | Port to listen on | +| `--hostname` | Hostname to listen on | +| `--mdns` | Enable mDNS discovery | --- @@ -428,10 +429,11 @@ This starts an HTTP server and opens a web browser to access OpenCode through a #### Flags -| Flag | Short | Description | -| ------------ | ----- | --------------------- | -| `--port` | `-p` | Port to listen on | -| `--hostname` | | Hostname to listen on | +| Flag | Description | +| ------------ | --------------------- | +| `--port` | Port to listen on | +| `--hostname` | Hostname to listen on | +| `--mdns` | Enable mDNS discovery | --- diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index ebaff36bb15..d7f8031782c 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -120,6 +120,31 @@ Available options: --- +### Server + +You can configure server settings for the `opencode serve` and `opencode web` commands through the `server` option. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "server": { + "port": 4096, + "hostname": "0.0.0.0", + "mdns": true + } +} +``` + +Available options: + +- `port` - Port to listen on. +- `hostname` - Hostname to listen on. When `mdns` is enabled and no hostname is set, defaults to `0.0.0.0`. +- `mdns` - Enable mDNS service discovery. This allows other devices on the network to discover your OpenCode server. + +[Learn more about the server here](/docs/server). + +--- + ### Tools You can manage the tools an LLM can use through the `tools` option. diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx index 427d8f505ff..c63917f792e 100644 --- a/packages/web/src/content/docs/server.mdx +++ b/packages/web/src/content/docs/server.mdx @@ -18,10 +18,11 @@ opencode serve [--port ] [--hostname ] #### Options -| Flag | Short | Description | Default | -| ------------ | ----- | --------------------- | ----------- | -| `--port` | `-p` | Port to listen on | `4096` | -| `--hostname` | `-h` | Hostname to listen on | `127.0.0.1` | +| Flag | Description | Default | +| ------------ | --------------------- | ----------- | +| `--port` | Port to listen on | `4096` | +| `--hostname` | Hostname to listen on | `127.0.0.1` | +| `--mdns` | Enable mDNS discovery | `false` | --- From 54588b4570728749705ebba027a07129a47e86b9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 16:30:20 +0000 Subject: [PATCH 042/164] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 19 +++++++++++++++++++ packages/sdk/openapi.json | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 0a31394ed9c..87aac3287e0 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1151,6 +1151,24 @@ export type KeybindsConfig = { tips_toggle?: string } +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { + /** + * Port to listen on + */ + port?: number + /** + * Hostname to listen on + */ + hostname?: string + /** + * Enable mDNS service discovery + */ + mdns?: boolean +} + export type AgentConfig = { model?: string temperature?: number @@ -1410,6 +1428,7 @@ export type Config = { */ diff_style?: "auto" | "stacked" } + server?: ServerConfig /** * Command configuration, see https://opencode.ai/docs/commands */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 96ba0720c73..dd20a93ec1b 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7687,6 +7687,27 @@ }, "additionalProperties": false }, + "ServerConfig": { + "description": "Server configuration for opencode serve and web commands", + "type": "object", + "properties": { + "port": { + "description": "Port to listen on", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "hostname": { + "description": "Hostname to listen on", + "type": "string" + }, + "mdns": { + "description": "Enable mDNS service discovery", + "type": "boolean" + } + }, + "additionalProperties": false + }, "AgentConfig": { "type": "object", "properties": { @@ -8170,6 +8191,9 @@ } } }, + "server": { + "$ref": "#/components/schemas/ServerConfig" + }, "command": { "description": "Command configuration, see https://opencode.ai/docs/commands", "type": "object", From 2333af6ed3085721cdc244b153097bd0d1b353a8 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Fri, 26 Dec 2025 13:49:05 -0300 Subject: [PATCH 043/164] Desktop: MCP UI (#6162) Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> --- .../app/src/components/dialog-select-mcp.tsx | 91 +++++++++++++++++++ .../src/components/session-lsp-indicator.tsx | 40 ++++++++ .../src/components/session-mcp-indicator.tsx | 36 ++++++++ packages/app/src/components/status-bar.tsx | 14 +++ packages/app/src/context/global-sync.tsx | 10 ++ packages/app/src/pages/session.tsx | 17 ++++ packages/ui/src/components/icon.tsx | 1 + 7 files changed, 209 insertions(+) create mode 100644 packages/app/src/components/dialog-select-mcp.tsx create mode 100644 packages/app/src/components/session-lsp-indicator.tsx create mode 100644 packages/app/src/components/session-mcp-indicator.tsx create mode 100644 packages/app/src/components/status-bar.tsx diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx new file mode 100644 index 00000000000..c29cd827e3b --- /dev/null +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -0,0 +1,91 @@ +import { Component, createMemo, createSignal, Show } from "solid-js" +import { useSync } from "@/context/sync" +import { useSDK } from "@/context/sdk" +import { Dialog } from "@opencode-ai/ui/dialog" +import { List } from "@opencode-ai/ui/list" +import { Switch } from "@opencode-ai/ui/switch" + +export const DialogSelectMcp: Component = () => { + const sync = useSync() + const sdk = useSDK() + const [loading, setLoading] = createSignal(null) + + const items = createMemo(() => + Object.entries(sync.data.mcp ?? {}) + .map(([name, status]) => ({ name, status: status.status })) + .sort((a, b) => a.name.localeCompare(b.name)), + ) + + const toggle = async (name: string) => { + if (loading()) return + setLoading(name) + const status = sync.data.mcp[name] + if (status?.status === "connected") { + await sdk.client.mcp.disconnect({ name }) + } else { + await sdk.client.mcp.connect({ name }) + } + const result = await sdk.client.mcp.status() + if (result.data) sync.set("mcp", result.data) + setLoading(null) + } + + const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length) + const totalCount = createMemo(() => items().length) + + return ( + + x?.name ?? ""} + items={items} + filterKeys={["name", "status"]} + sortBy={(a, b) => a.name.localeCompare(b.name)} + onSelect={(x) => { + if (x) toggle(x.name) + }} + > + {(i) => { + const mcpStatus = () => sync.data.mcp[i.name] + const status = () => mcpStatus()?.status + const error = () => { + const s = mcpStatus() + return s?.status === "failed" ? s.error : undefined + } + const enabled = () => status() === "connected" + return ( +
+
+
+ {i.name} + + connected + + + failed + + + needs auth + + + disabled + + + ... + +
+ + {error()} + +
+
e.stopPropagation()}> + toggle(i.name)} /> +
+
+ ) + }} +
+
+ ) +} diff --git a/packages/app/src/components/session-lsp-indicator.tsx b/packages/app/src/components/session-lsp-indicator.tsx new file mode 100644 index 00000000000..98d6d6dfd76 --- /dev/null +++ b/packages/app/src/components/session-lsp-indicator.tsx @@ -0,0 +1,40 @@ +import { createMemo, Show } from "solid-js" +import { Icon } from "@opencode-ai/ui/icon" +import { useSync } from "@/context/sync" +import { Tooltip } from "@opencode-ai/ui/tooltip" + +export function SessionLspIndicator() { + const sync = useSync() + + const lspStats = createMemo(() => { + const lsp = sync.data.lsp ?? [] + const connected = lsp.filter((s) => s.status === "connected").length + const hasError = lsp.some((s) => s.status === "error") + const total = lsp.length + return { connected, hasError, total } + }) + + const tooltipContent = createMemo(() => { + const lsp = sync.data.lsp ?? [] + if (lsp.length === 0) return "No LSP servers" + return lsp.map((s) => s.name).join(", ") + }) + + return ( + 0}> + +
+ 0, + }} + /> + {lspStats().connected} LSP +
+
+
+ ) +} diff --git a/packages/app/src/components/session-mcp-indicator.tsx b/packages/app/src/components/session-mcp-indicator.tsx new file mode 100644 index 00000000000..17a6f2e1af0 --- /dev/null +++ b/packages/app/src/components/session-mcp-indicator.tsx @@ -0,0 +1,36 @@ +import { createMemo, Show } from "solid-js" +import { Button } from "@opencode-ai/ui/button" +import { Icon } from "@opencode-ai/ui/icon" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useSync } from "@/context/sync" +import { DialogSelectMcp } from "@/components/dialog-select-mcp" + +export function SessionMcpIndicator() { + const sync = useSync() + const dialog = useDialog() + + const mcpStats = createMemo(() => { + const mcp = sync.data.mcp ?? {} + const entries = Object.entries(mcp) + const enabled = entries.filter(([, status]) => status.status === "connected").length + const failed = entries.some(([, status]) => status.status === "failed") + const total = entries.length + return { enabled, failed, total } + }) + + return ( + 0}> + + + ) +} diff --git a/packages/app/src/components/status-bar.tsx b/packages/app/src/components/status-bar.tsx new file mode 100644 index 00000000000..e0e25c60b8b --- /dev/null +++ b/packages/app/src/components/status-bar.tsx @@ -0,0 +1,14 @@ +import { Show, type ParentProps } from "solid-js" +import { usePlatform } from "@/context/platform" + +export function StatusBar(props: ParentProps) { + const platform = usePlatform() + return ( +
+ + v{platform.version} + +
{props.children}
+
+ ) +} diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 10607b1d23f..7a9dc8dc425 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -12,6 +12,8 @@ import { type ProviderListResponse, type ProviderAuthResponse, type Command, + type McpStatus, + type LspStatus, createOpencodeClient, } from "@opencode-ai/sdk/v2/client" import { createStore, produce, reconcile } from "solid-js/store" @@ -41,6 +43,10 @@ type State = { todo: { [sessionID: string]: Todo[] } + mcp: { + [name: string]: McpStatus + } + lsp: LspStatus[] limit: number message: { [sessionID: string]: Message[] @@ -85,6 +91,8 @@ function createGlobalSync() { session_status: {}, session_diff: {}, todo: {}, + mcp: {}, + lsp: [], limit: 5, message: {}, part: {}, @@ -149,6 +157,8 @@ function createGlobalSync() { session: () => loadSessions(directory), status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)), config: () => sdk.config.get().then((x) => setStore("config", x.data!)), + mcp: () => sdk.mcp.status().then((x) => setStore("mcp", x.data ?? {})), + lsp: () => sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])), } await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e)))) .then(() => setStore("ready", true)) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index d7eaccc2ad9..019cc305c1a 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -49,6 +49,7 @@ import { checksum } from "@opencode-ai/util/encode" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectFile } from "@/components/dialog-select-file" import { DialogSelectModel } from "@/components/dialog-select-model" +import { DialogSelectMcp } from "@/components/dialog-select-mcp" import { useCommand } from "@/context/command" import { useNavigate, useParams } from "@solidjs/router" import { UserMessage } from "@opencode-ai/sdk/v2" @@ -56,6 +57,9 @@ import { useSDK } from "@/context/sdk" import { usePrompt } from "@/context/prompt" import { extractPromptFromParts } from "@/utils/prompt" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" +import { StatusBar } from "@/components/status-bar" +import { SessionMcpIndicator } from "@/components/session-mcp-indicator" +import { SessionLspIndicator } from "@/components/session-lsp-indicator" export default function Page() { const layout = useLayout() @@ -274,6 +278,15 @@ export default function Page() { slash: "model", onSelect: () => dialog.show(() => ), }, + { + id: "mcp.toggle", + title: "Toggle MCPs", + description: "Toggle MCPs", + category: "MCP", + keybind: "mod+;", + slash: "mcp", + onSelect: () => dialog.show(() => ), + }, { id: "agent.cycle", title: "Cycle agent", @@ -921,6 +934,10 @@ export default function Page() {
+ + + +
) } diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 45ccee8f9bf..5e1a8e32afc 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -18,6 +18,7 @@ const icons = { console: ``, expand: ``, collapse: ``, + code: ``, "code-lines": ``, "circle-ban-sign": ``, "edit-small-2": ``, From f1ab427f0efcdeaf5184f1f070eb2f4b0512edd8 Mon Sep 17 00:00:00 2001 From: Roberto Carvajal Date: Fri, 26 Dec 2025 14:08:45 -0300 Subject: [PATCH 044/164] fix(dep): Update package.json - fix perplexity provider version (#6199) Co-authored-by: Aiden Cline --- bun.lock | 14 +++++++------- package.json | 7 ------- packages/opencode/package.json | 7 +++++++ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bun.lock b/bun.lock index 818dee4f228..593033e34f4 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,6 @@ "": { "name": "opencode", "dependencies": { - "@ai-sdk/cerebras": "1.0.33", - "@ai-sdk/cohere": "2.0.21", - "@ai-sdk/deepinfra": "1.0.30", - "@ai-sdk/gateway": "2.0.23", - "@ai-sdk/groq": "2.0.33", - "@ai-sdk/perplexity": "2.0.22", - "@ai-sdk/togetherai": "1.0.30", "@aws-sdk/client-s3": "3.933.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", @@ -263,14 +256,21 @@ "@ai-sdk/amazon-bedrock": "3.0.57", "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/azure": "2.0.73", + "@ai-sdk/cerebras": "1.0.33", + "@ai-sdk/cohere": "2.0.21", + "@ai-sdk/deepinfra": "1.0.30", + "@ai-sdk/gateway": "2.0.23", "@ai-sdk/google": "2.0.44", "@ai-sdk/google-vertex": "3.0.81", + "@ai-sdk/groq": "2.0.33", "@ai-sdk/mcp": "0.0.8", "@ai-sdk/mistral": "2.0.26", "@ai-sdk/openai": "2.0.71", "@ai-sdk/openai-compatible": "1.0.27", + "@ai-sdk/perplexity": "2.0.22", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", + "@ai-sdk/togetherai": "1.0.30", "@ai-sdk/xai": "2.0.42", "@clack/prompts": "1.0.0-alpha.1", "@hono/standard-validator": "0.1.5", diff --git a/package.json b/package.json index 7346a4ca853..aa7031bec72 100644 --- a/package.json +++ b/package.json @@ -67,13 +67,6 @@ "turbo": "2.5.6" }, "dependencies": { - "@ai-sdk/cerebras": "1.0.33", - "@ai-sdk/cohere": "2.0.21", - "@ai-sdk/deepinfra": "1.0.30", - "@ai-sdk/gateway": "2.0.23", - "@ai-sdk/groq": "2.0.33", - "@ai-sdk/perplexity": "2.0.22", - "@ai-sdk/togetherai": "1.0.30", "@aws-sdk/client-s3": "3.933.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ef2b822e86e..55656660e06 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -61,6 +61,13 @@ "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "@ai-sdk/xai": "2.0.42", + "@ai-sdk/cerebras": "1.0.33", + "@ai-sdk/cohere": "2.0.21", + "@ai-sdk/deepinfra": "1.0.30", + "@ai-sdk/gateway": "2.0.23", + "@ai-sdk/groq": "2.0.33", + "@ai-sdk/perplexity": "2.0.22", + "@ai-sdk/togetherai": "1.0.30", "@clack/prompts": "1.0.0-alpha.1", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", From 155ba794cf974ed4473844c5611736178f1e5f7d Mon Sep 17 00:00:00 2001 From: Ayush Walekar Date: Fri, 26 Dec 2025 22:39:06 +0530 Subject: [PATCH 045/164] chore: createOpencodeServer expose `logLevel` (#6202) --- packages/opencode/src/config/config.ts | 1 + packages/sdk/js/src/gen/types.gen.ts | 4 ++++ packages/sdk/js/src/server.ts | 5 ++++- packages/sdk/js/src/v2/gen/types.gen.ts | 4 ++++ packages/sdk/js/src/v2/server.ts | 5 ++++- 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 9187ed6745a..3350cf8f6a6 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -644,6 +644,7 @@ export namespace Config { $schema: z.string().optional().describe("JSON schema reference for configuration validation"), theme: z.string().optional().describe("Theme name to use for the interface"), keybinds: Keybinds.optional().describe("Custom keybind configurations"), + logLevel: Log.Level.optional().describe("Log level"), tui: TUI.optional().describe("TUI specific settings"), server: Server.optional().describe("Server configuration for opencode serve and web commands"), command: z diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 06993d3f930..32f33f66219 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1174,6 +1174,10 @@ export type Config = { */ theme?: string keybinds?: KeybindsConfig + /** + * Log level + */ + logLevel?: "DEBUG" | "INFO" | "WARN" | "ERROR" /** * TUI specific settings */ diff --git a/packages/sdk/js/src/server.ts b/packages/sdk/js/src/server.ts index a09e14ab2aa..174131ccfd5 100644 --- a/packages/sdk/js/src/server.ts +++ b/packages/sdk/js/src/server.ts @@ -28,7 +28,10 @@ export async function createOpencodeServer(options?: ServerOptions) { options ?? {}, ) - const proc = spawn(`opencode`, [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`], { + const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`] + if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`) + + const proc = spawn(`opencode`, args, { signal: options.signal, env: { ...process.env, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 87aac3287e0..2fda3375e78 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1406,6 +1406,10 @@ export type Config = { */ theme?: string keybinds?: KeybindsConfig + /** + * Log level + */ + logLevel?: "DEBUG" | "INFO" | "WARN" | "ERROR" /** * TUI specific settings */ diff --git a/packages/sdk/js/src/v2/server.ts b/packages/sdk/js/src/v2/server.ts index a09e14ab2aa..174131ccfd5 100644 --- a/packages/sdk/js/src/v2/server.ts +++ b/packages/sdk/js/src/v2/server.ts @@ -28,7 +28,10 @@ export async function createOpencodeServer(options?: ServerOptions) { options ?? {}, ) - const proc = spawn(`opencode`, [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`], { + const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`] + if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`) + + const proc = spawn(`opencode`, args, { signal: options.signal, env: { ...process.env, From 634559760a99669446f0d17319c6a725427fe8a7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 17:09:31 +0000 Subject: [PATCH 046/164] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 10 ++++++---- packages/sdk/openapi.json | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 2fda3375e78..6b5cfc818d0 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1151,6 +1151,11 @@ export type KeybindsConfig = { tips_toggle?: string } +/** + * Log level + */ +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" + /** * Server configuration for opencode serve and web commands */ @@ -1406,10 +1411,7 @@ export type Config = { */ theme?: string keybinds?: KeybindsConfig - /** - * Log level - */ - logLevel?: "DEBUG" | "INFO" | "WARN" | "ERROR" + logLevel?: LogLevel /** * TUI specific settings */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index dd20a93ec1b..2a2984854f8 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7687,6 +7687,11 @@ }, "additionalProperties": false }, + "LogLevel": { + "description": "Log level", + "type": "string", + "enum": ["DEBUG", "INFO", "WARN", "ERROR"] + }, "ServerConfig": { "description": "Server configuration for opencode serve and web commands", "type": "object", @@ -8164,6 +8169,9 @@ "keybinds": { "$ref": "#/components/schemas/KeybindsConfig" }, + "logLevel": { + "$ref": "#/components/schemas/LogLevel" + }, "tui": { "description": "TUI specific settings", "type": "object", From d5b47d91282abbd61647ab67d62673ccccfdfb1a Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 26 Dec 2025 17:09:53 +0000 Subject: [PATCH 047/164] Update Nix flake.lock and hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 4363085de3b..f43b14684c3 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-okbViEKf1mRSmzbJgKdB9SJ875q84Bwu8d3ChHuaQ1g=" + "nodeModules": "sha256-lloUZt5mLyNWAcbQrJB4wGUKvKu24WFEhOLfZD5/FMg=" } From 8416db03ef34a1ecf187faa81fb5ce7fe8ee4ec7 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 11:23:31 -0600 Subject: [PATCH 048/164] tweak: make install script handle 404s better --- install | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/install b/install index e89ca9fb70f..702fb4a534c 100755 --- a/install +++ b/install @@ -155,8 +155,18 @@ if [ -z "$requested_version" ]; then exit 1 fi else + # Strip leading 'v' if present + requested_version="${requested_version#v}" url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename" specific_version=$requested_version + + # Verify the release exists before downloading + http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/sst/opencode/releases/tag/v${requested_version}") + if [ "$http_status" = "404" ]; then + echo -e "${RED}Error: Release v${requested_version} not found${NC}" + echo -e "${MUTED}Available releases: https://github.com/sst/opencode/releases${NC}" + exit 1 + fi fi print_message() { From b0e4408ecfd1f93bc95b7b46a8df861c13ee768a Mon Sep 17 00:00:00 2001 From: ja <51257127+anntnzrb@users.noreply.github.com> Date: Fri, 26 Dec 2025 12:31:51 -0500 Subject: [PATCH 049/164] feat: add shfmt formatter for shell scripts (#6204) --- packages/opencode/src/format/formatter.ts | 9 +++++++++ packages/web/src/content/docs/formatters.mdx | 1 + 2 files changed, 10 insertions(+) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 954940f8db2..90c48b05c2a 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -313,3 +313,12 @@ export const gleam: Info = { return Bun.which("gleam") !== null }, } + +export const shfmt: Info = { + name: "shfmt", + command: ["shfmt", "-w", "$FILE"], + extensions: [".sh", ".bash"], + async enabled() { + return Bun.which("shfmt") !== null + }, +} diff --git a/packages/web/src/content/docs/formatters.mdx b/packages/web/src/content/docs/formatters.mdx index fa915f116d4..2c0687b8ea5 100644 --- a/packages/web/src/content/docs/formatters.mdx +++ b/packages/web/src/content/docs/formatters.mdx @@ -30,6 +30,7 @@ OpenCode comes with several built-in formatters for popular languages and framew | ocamlformat | .ml, .mli | `ocamlformat` command available and `.ocamlformat` config file | | terraform | .tf, .tfvars | `terraform` command available | | gleam | .gleam | `gleam` command available | +| shfmt | .sh, .bash | `shfmt` command available | | oxfmt (Experimental) | .js, .jsx, .ts, .tsx | `oxfmt` dependency in `package.json` and an [experimental env variable flag](/docs/cli/#experimental) | So if your project has `prettier` in your `package.json`, OpenCode will automatically use it. From 25c68c8061e42e6318ea4356ef023947e95d16de Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Fri, 26 Dec 2025 12:32:31 -0500 Subject: [PATCH 050/164] chore: kill the dead Polaris Alpha code (#6193) --- .../opencode/src/session/prompt/polaris.txt | 107 ------------------ packages/opencode/src/session/system.ts | 2 - 2 files changed, 109 deletions(-) delete mode 100644 packages/opencode/src/session/prompt/polaris.txt diff --git a/packages/opencode/src/session/prompt/polaris.txt b/packages/opencode/src/session/prompt/polaris.txt deleted file mode 100644 index f90761890da..00000000000 --- a/packages/opencode/src/session/prompt/polaris.txt +++ /dev/null @@ -1,107 +0,0 @@ -You are OpenCode, the best coding agent on the planet. - -You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. - -IMPORTANT: Do not guess arbitrary URLs. Only provide URLs you are confident are correct and directly helpful for programming (for example, well-known official documentation). Prefer URLs provided by the user in their messages or local files. - -If the user asks for help or wants to give feedback inform them of the following: -- ctrl+p to list available actions -- To give feedback, users should report the issue at - https://github.com/sst/opencode - -When the user directly asks about OpenCode (eg. "can OpenCode do...", "does OpenCode have..."), or asks how to use a specific OpenCode feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from OpenCode docs. The list of available docs is available at https://opencode.ai/docs. - -When the user asks in second person (eg. "are you able...", "can you do..."), treat it as a request to help. Briefly confirm your capability and, when appropriate, immediately start performing the requested task or provide a concrete, useful answer instead of replying with only "yes" or "no". - -# Tone and style -- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked. -- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification. -- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session. -- Do not create new files unless necessary for achieving your goal or explicitly requested. Prefer editing an existing file when possible. This includes markdown files. - -# Professional objectivity -Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. - -# Task Management -You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools frequently for multi-step or non-trivial tasks to give the user visibility into your progress. -These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable. - -Prefer marking todos as completed soon after you finish each task, rather than delaying without reason. - -Examples: - - -user: Run the build and fix any type errors -assistant: I'm going to use the TodoWrite tool to write the following items to the todo list: -- Run the build -- Fix any type errors - -I'm now going to run the build using Bash. - -Looks like I found 10 type errors. I'm going to use the TodoWrite tool to write 10 items to the todo list. - -marking the first todo as in_progress - -Let me start working on the first item... - -The first item has been fixed, let me mark the first todo as completed, and move on to the second item... -.. -.. - -In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors. - - -user: Help me write a new feature that allows users to track their usage metrics and export them to various formats -assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the TodoWrite tool to plan this task. -Adding the following todos to the todo list: -1. Research existing metrics tracking in the codebase -2. Design the metrics collection system -3. Implement core metrics tracking functionality -4. Create export functionality for different formats - -Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that. - -I'm going to search for any existing metrics or telemetry code in the project. - -I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned... - -[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go] - - - -# Doing tasks -The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended: -- -- Use the TodoWrite tool to plan the task if required - -- Tool results and user messages may include tags. tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear. - - -# Tool usage policy -- When doing file search, prefer to use the Task tool in order to reduce context usage. -- You should proactively use the Task tool with specialized agents when the task at hand matches the agent's description. - -- When WebFetch returns a message about a redirect to a different host, you should immediately make a new WebFetch request with the redirect URL provided in the response. -- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls. -- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple Task tool calls. -- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead. -- Generally use the Task tool for broader or multi-file exploration; direct reads and searches are fine for specific, simple queries. - -user: Where are errors from the client handled? -assistant: [Uses the Task tool to find the files that handle client errors instead of using Glob or Grep directly] - - -user: What is the codebase structure? -assistant: [Uses the Task tool] - - -Prefer using the TodoWrite tool to plan and track tasks when there are multiple steps or files involved. - -# Code References - -When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location. - - -user: Where are errors from the client handled? -assistant: Clients are marked as failed in the `connectToServer` function in src/services/process.ts:712. - diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 30094388168..429e696db3b 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -10,7 +10,6 @@ import os from "os" import PROMPT_ANTHROPIC from "./prompt/anthropic.txt" import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt" -import PROMPT_POLARIS from "./prompt/polaris.txt" import PROMPT_BEAST from "./prompt/beast.txt" import PROMPT_GEMINI from "./prompt/gemini.txt" import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt" @@ -30,7 +29,6 @@ export namespace SystemPrompt { return [PROMPT_BEAST] if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI] if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC] - if (model.api.id.includes("polaris-alpha")) return [PROMPT_POLARIS] return [PROMPT_ANTHROPIC_WITHOUT_TODO] } From 1bcf8d88065cbd3183b5f44b98c6ca86cd5a7e3e Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Fri, 26 Dec 2025 11:36:31 -0600 Subject: [PATCH 051/164] fix: `opencode web` baseURL error (#6181) --- packages/app/src/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 11216643e5b..de8fcf7d124 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -38,7 +38,7 @@ const url = iife(() => { if (import.meta.env.DEV) return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` - return "http://localhost:4096" + return window.location.origin }) export function App() { From 4abaa052dbd33d15367deb1c8995eb2c6b764328 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 11:37:34 -0600 Subject: [PATCH 052/164] fix: adjust upgrade command to use gh releases page if not npm/bun/pnpm install method --- packages/opencode/src/installation/index.ts | 31 ++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 2c99e21a8f5..effe19721b4 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -166,6 +166,8 @@ export namespace Installation { export async function latest(installMethod?: Method) { const detectedMethod = installMethod || (await method()) + + // Use brew formula API for homebrew core formula if (detectedMethod === "brew") { const formula = await getBrewFormula() if (formula === "opencode") { @@ -178,19 +180,28 @@ export namespace Installation { } } - const registry = await iife(async () => { - const r = (await $`npm config get registry`.quiet().nothrow().text()).trim() - const reg = r || "https://registry.npmjs.org" - return reg.endsWith("/") ? reg.slice(0, -1) : reg - }) - const [major] = VERSION.split(".").map((x) => Number(x)) - // const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL - const channel = CHANNEL - return fetch(`${registry}/opencode-ai/${channel}`) + // Use npm registry for npm/bun/pnpm + if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") { + const registry = await iife(async () => { + const r = (await $`npm config get registry`.quiet().nothrow().text()).trim() + const reg = r || "https://registry.npmjs.org" + return reg.endsWith("/") ? reg.slice(0, -1) : reg + }) + const channel = CHANNEL + return fetch(`${registry}/opencode-ai/${channel}`) + .then((res) => { + if (!res.ok) throw new Error(res.statusText) + return res.json() + }) + .then((data: any) => data.version) + } + + // Use GitHub releases for everything else (curl, yarn, brew tap, unknown) + return fetch("https://api.github.com/repos/sst/opencode/releases/latest") .then((res) => { if (!res.ok) throw new Error(res.statusText) return res.json() }) - .then((data: any) => data.version) + .then((data: any) => data.tag_name.replace(/^v/, "")) } } From 2e10ffac6b4ffe6ff183a485d847e26641f8a1bc Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 11:43:03 -0600 Subject: [PATCH 053/164] chore: rm comments --- packages/opencode/src/installation/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index effe19721b4..975ca749bce 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -167,7 +167,6 @@ export namespace Installation { export async function latest(installMethod?: Method) { const detectedMethod = installMethod || (await method()) - // Use brew formula API for homebrew core formula if (detectedMethod === "brew") { const formula = await getBrewFormula() if (formula === "opencode") { @@ -180,7 +179,6 @@ export namespace Installation { } } - // Use npm registry for npm/bun/pnpm if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") { const registry = await iife(async () => { const r = (await $`npm config get registry`.quiet().nothrow().text()).trim() @@ -196,7 +194,6 @@ export namespace Installation { .then((data: any) => data.version) } - // Use GitHub releases for everything else (curl, yarn, brew tap, unknown) return fetch("https://api.github.com/repos/sst/opencode/releases/latest") .then((res) => { if (!res.ok) throw new Error(res.statusText) From 505068d5a6cccc732aed76580f9bec6a5dbca507 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 11:43:52 -0600 Subject: [PATCH 054/164] Revert "feat(core): optional mdns service (#6192)" This reverts commit 26e7043718fbf5fbf08eecd04ff8ed5edd82e33e. --- bun.lock | 11 ---- flake.lock | 6 +-- packages/opencode/package.json | 1 - packages/opencode/src/cli/cmd/acp.ts | 30 +++++++---- packages/opencode/src/cli/cmd/serve.ts | 25 ++++++--- packages/opencode/src/cli/cmd/tui/spawn.ts | 28 ++++++---- packages/opencode/src/cli/cmd/tui/thread.ts | 22 +++++--- packages/opencode/src/cli/cmd/tui/worker.ts | 2 +- packages/opencode/src/cli/cmd/web.ts | 31 +++++++---- packages/opencode/src/cli/network.ts | 42 --------------- packages/opencode/src/config/config.ts | 12 ----- packages/opencode/src/server/mdns.ts | 57 --------------------- packages/opencode/src/server/server.ts | 33 ++---------- packages/web/src/content/docs/cli.mdx | 18 +++---- packages/web/src/content/docs/config.mdx | 25 --------- packages/web/src/content/docs/server.mdx | 9 ++-- 16 files changed, 115 insertions(+), 237 deletions(-) delete mode 100644 packages/opencode/src/cli/network.ts delete mode 100644 packages/opencode/src/server/mdns.ts diff --git a/bun.lock b/bun.lock index 593033e34f4..d2fd6aa8de9 100644 --- a/bun.lock +++ b/bun.lock @@ -292,7 +292,6 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", - "bonjour-service": "1.3.0", "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", @@ -1082,8 +1081,6 @@ "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], - "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], - "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], @@ -2006,8 +2003,6 @@ "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], - "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], @@ -2252,8 +2247,6 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -3030,8 +3023,6 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], - "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], "mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="], @@ -3604,8 +3595,6 @@ "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], - "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], - "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], diff --git a/flake.lock b/flake.lock index 8bba6eeb3df..4ff2c1d0e11 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1766747458, - "narHash": "sha256-m63jjuo/ygo8ztkCziYh5OOIbTSXUDkKbqw3Vuqu4a4=", + "lastModified": 1766532406, + "narHash": "sha256-acLU/ag9VEoKkzOD202QASX25nG1eArXg5A0mHjKgxM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c633f572eded8c4f3c75b8010129854ed404a6ce", + "rev": "8142186f001295e5a3239f485c8a49bf2de2695a", "type": "github" }, "original": { diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 55656660e06..bf45966d86c 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -88,7 +88,6 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", - "bonjour-service": "1.3.0", "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index 2db64e3b1af..c607e5f5bb7 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -3,10 +3,8 @@ import { bootstrap } from "../bootstrap" import { cmd } from "./cmd" import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" import { ACP } from "@/acp/agent" -import { Config } from "@/config/config" import { Server } from "@/server/server" import { createOpencodeClient } from "@opencode-ai/sdk/v2" -import { withNetworkOptions, resolveNetworkOptions } from "../network" const log = Log.create({ service: "acp-command" }) @@ -21,17 +19,29 @@ export const AcpCommand = cmd({ command: "acp", describe: "start ACP (Agent Client Protocol) server", builder: (yargs) => { - return withNetworkOptions(yargs).option("cwd", { - describe: "working directory", - type: "string", - default: process.cwd(), - }) + return yargs + .option("cwd", { + describe: "working directory", + type: "string", + default: process.cwd(), + }) + .option("port", { + type: "number", + describe: "port to listen on", + default: 0, + }) + .option("hostname", { + type: "string", + describe: "hostname to listen on", + default: "127.0.0.1", + }) }, handler: async (args) => { await bootstrap(process.cwd(), async () => { - const config = await Config.get() - const opts = resolveNetworkOptions(args, config) - const server = Server.listen(opts) + const server = Server.listen({ + port: args.port, + hostname: args.hostname, + }) const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}`, diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 0fd7aa88f32..3af3316a9d3 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,16 +1,29 @@ -import { Config } from "../../config/config" import { Server } from "../../server/server" import { cmd } from "./cmd" -import { withNetworkOptions, resolveNetworkOptions } from "../network" export const ServeCommand = cmd({ command: "serve", - builder: (yargs) => withNetworkOptions(yargs), + builder: (yargs) => + yargs + .option("port", { + alias: ["p"], + type: "number", + describe: "port to listen on", + default: 0, + }) + .option("hostname", { + type: "string", + describe: "hostname to listen on", + default: "127.0.0.1", + }), describe: "starts a headless opencode server", handler: async (args) => { - const config = await Config.get() - const opts = resolveNetworkOptions(args, config) - const server = Server.listen(opts) + const hostname = args.hostname + const port = args.port + const server = Server.listen({ + port, + hostname, + }) console.log(`opencode server listening on http://${server.hostname}:${server.port}`) await new Promise(() => {}) await server.stop() diff --git a/packages/opencode/src/cli/cmd/tui/spawn.ts b/packages/opencode/src/cli/cmd/tui/spawn.ts index 7ab846428d7..fa679529890 100644 --- a/packages/opencode/src/cli/cmd/tui/spawn.ts +++ b/packages/opencode/src/cli/cmd/tui/spawn.ts @@ -1,23 +1,33 @@ import { cmd } from "@/cli/cmd/cmd" -import { Config } from "@/config/config" import { Instance } from "@/project/instance" import path from "path" import { Server } from "@/server/server" import { upgrade } from "@/cli/upgrade" -import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" export const TuiSpawnCommand = cmd({ command: "spawn [project]", builder: (yargs) => - withNetworkOptions(yargs).positional("project", { - type: "string", - describe: "path to start opencode in", - }), + yargs + .positional("project", { + type: "string", + describe: "path to start opencode in", + }) + .option("port", { + type: "number", + describe: "port to listen on", + default: 0, + }) + .option("hostname", { + type: "string", + describe: "hostname to listen on", + default: "127.0.0.1", + }), handler: async (args) => { upgrade() - const config = await Config.get() - const opts = resolveNetworkOptions(args, config) - const server = Server.listen(opts) + const server = Server.listen({ + port: args.port, + hostname: "127.0.0.1", + }) const bin = process.execPath const cmd = [] let cwd = process.cwd() diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index f75e3bd6511..3cf8937a974 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -6,8 +6,6 @@ import path from "path" import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" -import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" -import { Config } from "@/config/config" declare global { const OPENCODE_WORKER_PATH: string @@ -17,7 +15,7 @@ export const TuiThreadCommand = cmd({ command: "$0 [project]", describe: "start opencode tui", builder: (yargs) => - withNetworkOptions(yargs) + yargs .positional("project", { type: "string", describe: "path to start opencode in", @@ -38,12 +36,23 @@ export const TuiThreadCommand = cmd({ describe: "session id to continue", }) .option("prompt", { + alias: ["p"], type: "string", describe: "prompt to use", }) .option("agent", { type: "string", describe: "agent to use", + }) + .option("port", { + type: "number", + describe: "port to listen on", + default: 0, + }) + .option("hostname", { + type: "string", + describe: "hostname to listen on", + default: "127.0.0.1", }), handler: async (args) => { // Resolve relative paths against PWD to preserve behavior when using --cwd flag @@ -78,9 +87,10 @@ export const TuiThreadCommand = cmd({ process.on("unhandledRejection", (e) => { Log.Default.error(e) }) - const config = await Config.get() - const networkOpts = resolveNetworkOptions(args, config) - const server = await client.call("server", networkOpts) + const server = await client.call("server", { + port: args.port, + hostname: args.hostname, + }) const prompt = await iife(async () => { const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined if (!args.prompt) return piped diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 3ffc45ae884..76f78f3faa8 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -30,7 +30,7 @@ process.on("uncaughtException", (e) => { let server: Bun.Server export const rpc = { - async server(input: { port: number; hostname: string; mdns?: boolean }) { + async server(input: { port: number; hostname: string }) { if (server) await server.stop(true) try { server = Server.listen(input) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index adede03859c..3d3036b1b07 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -1,8 +1,6 @@ -import { Config } from "../../config/config" import { Server } from "../../server/server" import { UI } from "../ui" import { cmd } from "./cmd" -import { withNetworkOptions, resolveNetworkOptions } from "../network" import open from "open" import { networkInterfaces } from "os" @@ -30,17 +28,32 @@ function getNetworkIPs() { export const WebCommand = cmd({ command: "web", - builder: (yargs) => withNetworkOptions(yargs), + builder: (yargs) => + yargs + .option("port", { + alias: ["p"], + type: "number", + describe: "port to listen on", + default: 0, + }) + .option("hostname", { + type: "string", + describe: "hostname to listen on", + default: "127.0.0.1", + }), describe: "starts a headless opencode server", handler: async (args) => { - const config = await Config.get() - const opts = resolveNetworkOptions(args, config) - const server = Server.listen(opts) + const hostname = args.hostname + const port = args.port + const server = Server.listen({ + port, + hostname, + }) UI.empty() UI.println(UI.logo(" ")) UI.empty() - if (opts.hostname === "0.0.0.0") { + if (hostname === "0.0.0.0") { // Show localhost for local access const localhostUrl = `http://localhost:${server.port}` UI.println(UI.Style.TEXT_INFO_BOLD + " Local access: ", UI.Style.TEXT_NORMAL, localhostUrl) @@ -57,10 +70,6 @@ export const WebCommand = cmd({ } } - if (opts.mdns) { - UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, "opencode.local") - } - // Open localhost in browser open(localhostUrl.toString()).catch(() => {}) } else { diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts deleted file mode 100644 index 661688572bd..00000000000 --- a/packages/opencode/src/cli/network.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Argv, InferredOptionTypes } from "yargs" -import type { Config } from "../config/config" - -const options = { - port: { - type: "number" as const, - describe: "port to listen on", - default: 0, - }, - hostname: { - type: "string" as const, - describe: "hostname to listen on", - default: "127.0.0.1", - }, - mdns: { - type: "boolean" as const, - describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)", - default: false, - }, -} - -export type NetworkOptions = InferredOptionTypes - -export function withNetworkOptions(yargs: Argv) { - return yargs.options(options) -} - -export function resolveNetworkOptions(args: NetworkOptions, config?: Config.Info) { - const portExplicitlySet = process.argv.includes("--port") - const hostnameExplicitlySet = process.argv.includes("--hostname") - const mdnsExplicitlySet = process.argv.includes("--mdns") - - const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns) - const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port) - const hostname = hostnameExplicitlySet - ? args.hostname - : mdns && !config?.server?.hostname - ? "0.0.0.0" - : (config?.server?.hostname ?? args.hostname) - - return { hostname, port, mdns } -} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3350cf8f6a6..802c5db9296 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -587,17 +587,6 @@ export namespace Config { .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), }) - export const Server = z - .object({ - port: z.number().int().positive().optional().describe("Port to listen on"), - hostname: z.string().optional().describe("Hostname to listen on"), - mdns: z.boolean().optional().describe("Enable mDNS service discovery"), - }) - .strict() - .meta({ - ref: "ServerConfig", - }) - export const Layout = z.enum(["auto", "stretch"]).meta({ ref: "LayoutConfig", }) @@ -646,7 +635,6 @@ export namespace Config { keybinds: Keybinds.optional().describe("Custom keybind configurations"), logLevel: Log.Level.optional().describe("Log level"), tui: TUI.optional().describe("TUI specific settings"), - server: Server.optional().describe("Server configuration for opencode serve and web commands"), command: z .record(z.string(), Command) .optional() diff --git a/packages/opencode/src/server/mdns.ts b/packages/opencode/src/server/mdns.ts deleted file mode 100644 index 45e61d361ac..00000000000 --- a/packages/opencode/src/server/mdns.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Log } from "@/util/log" -import Bonjour from "bonjour-service" - -const log = Log.create({ service: "mdns" }) - -export namespace MDNS { - let bonjour: Bonjour | undefined - let currentPort: number | undefined - - export function publish(port: number, name = "opencode") { - if (currentPort === port) return - if (bonjour) unpublish() - - try { - bonjour = new Bonjour() - const service = bonjour.publish({ - name, - type: "http", - port, - txt: { path: "/" }, - }) - - service.on("up", () => { - log.info("mDNS service published", { name, port }) - }) - - service.on("error", (err) => { - log.error("mDNS service error", { error: err }) - }) - - currentPort = port - } catch (err) { - log.error("mDNS publish failed", { error: err }) - if (bonjour) { - try { - bonjour.destroy() - } catch {} - } - bonjour = undefined - currentPort = undefined - } - } - - export function unpublish() { - if (bonjour) { - try { - bonjour.unpublishAll() - bonjour.destroy() - } catch (err) { - log.error("mDNS unpublish failed", { error: err }) - } - bonjour = undefined - currentPort = undefined - log.info("mDNS service unpublished") - } - } -} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 65393e12897..c74dbbb41ef 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -45,11 +45,9 @@ import { Snapshot } from "@/snapshot" import { SessionSummary } from "@/session/summary" import { SessionStatus } from "@/session/status" import { upgradeWebSocket, websocket } from "hono/bun" -import type { BunWebSocketData } from "hono/bun" import { errors } from "./error" import { Pty } from "@/pty" import { Installation } from "@/installation" -import { MDNS } from "./mdns" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false @@ -2625,41 +2623,20 @@ export namespace Server { return result } - export function listen(opts: { port: number; hostname: string; mdns?: boolean }) { + export function listen(opts: { port: number; hostname: string }) { const args = { hostname: opts.hostname, idleTimeout: 0, fetch: App().fetch, websocket: websocket, } as const - const tryServe = (port: number) => { + if (opts.port === 0) { try { - return Bun.serve({ ...args, port }) + return Bun.serve({ ...args, port: 4096 }) } catch { - return undefined + // port 4096 not available, fall through to use port 0 } } - const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port) - if (!server) throw new Error(`Failed to start server on port ${opts.port}`) - - const shouldPublishMDNS = - opts.mdns && - server.port && - opts.hostname !== "127.0.0.1" && - opts.hostname !== "localhost" && - opts.hostname !== "::1" - if (shouldPublishMDNS) { - MDNS.publish(server.port!) - } else if (opts.mdns) { - log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") - } - - const originalStop = server.stop.bind(server) - server.stop = async (closeActiveConnections?: boolean) => { - if (shouldPublishMDNS) MDNS.unpublish() - return originalStop(closeActiveConnections) - } - - return server + return Bun.serve({ ...args, port: opts.port }) } } diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 4a826e5b3ff..e4e40ac7a4c 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -335,11 +335,10 @@ This starts an HTTP server that provides API access to opencode functionality wi #### Flags -| Flag | Description | -| ------------ | --------------------- | -| `--port` | Port to listen on | -| `--hostname` | Hostname to listen on | -| `--mdns` | Enable mDNS discovery | +| Flag | Short | Description | +| ------------ | ----- | --------------------- | +| `--port` | `-p` | Port to listen on | +| `--hostname` | | Hostname to listen on | --- @@ -429,11 +428,10 @@ This starts an HTTP server and opens a web browser to access OpenCode through a #### Flags -| Flag | Description | -| ------------ | --------------------- | -| `--port` | Port to listen on | -| `--hostname` | Hostname to listen on | -| `--mdns` | Enable mDNS discovery | +| Flag | Short | Description | +| ------------ | ----- | --------------------- | +| `--port` | `-p` | Port to listen on | +| `--hostname` | | Hostname to listen on | --- diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index d7f8031782c..ebaff36bb15 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -120,31 +120,6 @@ Available options: --- -### Server - -You can configure server settings for the `opencode serve` and `opencode web` commands through the `server` option. - -```json title="opencode.json" -{ - "$schema": "https://opencode.ai/config.json", - "server": { - "port": 4096, - "hostname": "0.0.0.0", - "mdns": true - } -} -``` - -Available options: - -- `port` - Port to listen on. -- `hostname` - Hostname to listen on. When `mdns` is enabled and no hostname is set, defaults to `0.0.0.0`. -- `mdns` - Enable mDNS service discovery. This allows other devices on the network to discover your OpenCode server. - -[Learn more about the server here](/docs/server). - ---- - ### Tools You can manage the tools an LLM can use through the `tools` option. diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx index c63917f792e..427d8f505ff 100644 --- a/packages/web/src/content/docs/server.mdx +++ b/packages/web/src/content/docs/server.mdx @@ -18,11 +18,10 @@ opencode serve [--port ] [--hostname ] #### Options -| Flag | Description | Default | -| ------------ | --------------------- | ----------- | -| `--port` | Port to listen on | `4096` | -| `--hostname` | Hostname to listen on | `127.0.0.1` | -| `--mdns` | Enable mDNS discovery | `false` | +| Flag | Short | Description | Default | +| ------------ | ----- | --------------------- | ----------- | +| `--port` | `-p` | Port to listen on | `4096` | +| `--hostname` | `-h` | Hostname to listen on | `127.0.0.1` | --- From f4fdf0eb0390db7f15ca9ce2986c831d59d234db Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 17:45:03 +0000 Subject: [PATCH 055/164] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 19 ------------------- packages/sdk/openapi.json | 24 ------------------------ 2 files changed, 43 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 6b5cfc818d0..b1d05813ce3 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1156,24 +1156,6 @@ export type KeybindsConfig = { */ export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" -/** - * Server configuration for opencode serve and web commands - */ -export type ServerConfig = { - /** - * Port to listen on - */ - port?: number - /** - * Hostname to listen on - */ - hostname?: string - /** - * Enable mDNS service discovery - */ - mdns?: boolean -} - export type AgentConfig = { model?: string temperature?: number @@ -1434,7 +1416,6 @@ export type Config = { */ diff_style?: "auto" | "stacked" } - server?: ServerConfig /** * Command configuration, see https://opencode.ai/docs/commands */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 2a2984854f8..ef6ee0ebfa0 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7692,27 +7692,6 @@ "type": "string", "enum": ["DEBUG", "INFO", "WARN", "ERROR"] }, - "ServerConfig": { - "description": "Server configuration for opencode serve and web commands", - "type": "object", - "properties": { - "port": { - "description": "Port to listen on", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 - }, - "hostname": { - "description": "Hostname to listen on", - "type": "string" - }, - "mdns": { - "description": "Enable mDNS service discovery", - "type": "boolean" - } - }, - "additionalProperties": false - }, "AgentConfig": { "type": "object", "properties": { @@ -8199,9 +8178,6 @@ } } }, - "server": { - "$ref": "#/components/schemas/ServerConfig" - }, "command": { "description": "Command configuration, see https://opencode.ai/docs/commands", "type": "object", From 9afc45102014df2aa258f55b74d48987594bc2f7 Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 26 Dec 2025 17:45:58 +0000 Subject: [PATCH 056/164] Update Nix flake.lock and hashes --- flake.lock | 6 +++--- nix/hashes.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 4ff2c1d0e11..8bba6eeb3df 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1766532406, - "narHash": "sha256-acLU/ag9VEoKkzOD202QASX25nG1eArXg5A0mHjKgxM=", + "lastModified": 1766747458, + "narHash": "sha256-m63jjuo/ygo8ztkCziYh5OOIbTSXUDkKbqw3Vuqu4a4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8142186f001295e5a3239f485c8a49bf2de2695a", + "rev": "c633f572eded8c4f3c75b8010129854ed404a6ce", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index f43b14684c3..9ef78c2321d 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-lloUZt5mLyNWAcbQrJB4wGUKvKu24WFEhOLfZD5/FMg=" + "nodeModules": "sha256-CTW7pzZ0Kq5HHF5xgEh3EnuwnqtPsDkc5ImmZjJgwA8=" } From 7a5fbdf67c67cf6cd7cc84da066bcb34871ca609 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 12:06:36 -0600 Subject: [PATCH 057/164] ci: update zed extension sync --- .github/workflows/sync-zed-extension.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-zed-extension.yml b/.github/workflows/sync-zed-extension.yml index a504582c3c8..900a16418f3 100644 --- a/.github/workflows/sync-zed-extension.yml +++ b/.github/workflows/sync-zed-extension.yml @@ -31,4 +31,4 @@ jobs: run: | ./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }} env: - GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.ZED_EXTENSIONS_PAT }} From e1c1b1340b4c0c455f7701311118cf32924e9abd Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 12:08:16 -0600 Subject: [PATCH 058/164] ci: fix var --- .github/workflows/sync-zed-extension.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-zed-extension.yml b/.github/workflows/sync-zed-extension.yml index 900a16418f3..047cf001612 100644 --- a/.github/workflows/sync-zed-extension.yml +++ b/.github/workflows/sync-zed-extension.yml @@ -31,4 +31,4 @@ jobs: run: | ./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }} env: - GITHUB_TOKEN: ${{ secrets.ZED_EXTENSIONS_PAT }} + ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }} From 053a10e5155bb6a18765aceddc70599a6359e6c1 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 12:22:56 -0600 Subject: [PATCH 059/164] ci: fix token for gh --- script/sync-zed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/sync-zed.ts b/script/sync-zed.ts index b4a417ad8b9..3ac9ee83a7e 100755 --- a/script/sync-zed.ts +++ b/script/sync-zed.ts @@ -107,7 +107,7 @@ async function main() { console.log(`📬 Creating pull request...`) const prUrl = - await $`gh pr create --repo ${UPSTREAM_REPO} --base main --head ${FORK_REPO.split("/")[0]}:${branchName} --title "Update ${EXTENSION_NAME} to v${cleanVersion}" --body "Updating OpenCode extension to v${cleanVersion}"`.text() + await $`GH_TOKEN=${token} gh pr create --repo ${UPSTREAM_REPO} --base main --head ${FORK_REPO.split("/")[0]}:${branchName} --title "Update ${EXTENSION_NAME} to v${cleanVersion}" --body "Updating OpenCode extension to v${cleanVersion}"`.text() console.log(`✅ Pull request created: ${prUrl}`) console.log(`🎉 Done!`) From 61ddd1716d86b0be060b70c5333ca32909c5e922 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 12:24:14 -0600 Subject: [PATCH 060/164] ci: re-enable sync zed --- .github/workflows/sync-zed-extension.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync-zed-extension.yml b/.github/workflows/sync-zed-extension.yml index 047cf001612..9e647b8d941 100644 --- a/.github/workflows/sync-zed-extension.yml +++ b/.github/workflows/sync-zed-extension.yml @@ -2,8 +2,8 @@ name: "sync-zed-extension" on: workflow_dispatch: - # release: - # types: [published] + release: + types: [published] jobs: zed: From 1626341a4a7ce4e390c5d45d804ac02c928ca5fc Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Fri, 26 Dec 2025 14:34:03 -0500 Subject: [PATCH 061/164] github: support issues and workflow_dispatch events (#6157) --- packages/opencode/src/cli/cmd/github.ts | 88 ++++++++++++++++-------- packages/web/src/content/docs/github.mdx | 71 ++++++++++++++++--- 2 files changed, 123 insertions(+), 36 deletions(-) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 37aed2426d1..748a9638490 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -9,7 +9,9 @@ import * as github from "@actions/github" import type { Context } from "@actions/github/lib/context" import type { IssueCommentEvent, + IssuesEvent, PullRequestReviewCommentEvent, + WorkflowDispatchEvent, WorkflowRunEvent, PullRequestEvent, } from "@octokit/webhooks-types" @@ -132,7 +134,16 @@ type IssueQueryResponse = { const AGENT_USERNAME = "opencode-agent[bot]" const AGENT_REACTION = "eyes" const WORKFLOW_FILE = ".github/workflows/opencode.yml" -const SUPPORTED_EVENTS = ["issue_comment", "pull_request_review_comment", "schedule", "pull_request"] as const + +// Event categories for routing +// USER_EVENTS: triggered by user actions, have actor/issueId, support reactions/comments +// REPO_EVENTS: triggered by automation, no actor/issueId, output to logs/PR only +const USER_EVENTS = ["issue_comment", "pull_request_review_comment", "issues", "pull_request"] as const +const REPO_EVENTS = ["schedule", "workflow_dispatch"] as const +const SUPPORTED_EVENTS = [...USER_EVENTS, ...REPO_EVENTS] as const + +type UserEvent = (typeof USER_EVENTS)[number] +type RepoEvent = (typeof REPO_EVENTS)[number] // Parses GitHub remote URLs in various formats: // - https://github.com/owner/repo.git @@ -397,27 +408,38 @@ export const GithubRunCommand = cmd({ core.setFailed(`Unsupported event type: ${context.eventName}`) process.exit(1) } + + // Determine event category for routing + // USER_EVENTS: have actor, issueId, support reactions/comments + // REPO_EVENTS: no actor/issueId, output to logs/PR only + const isUserEvent = USER_EVENTS.includes(context.eventName as UserEvent) + const isRepoEvent = REPO_EVENTS.includes(context.eventName as RepoEvent) const isCommentEvent = ["issue_comment", "pull_request_review_comment"].includes(context.eventName) + const isIssuesEvent = context.eventName === "issues" const isScheduleEvent = context.eventName === "schedule" + const isWorkflowDispatchEvent = context.eventName === "workflow_dispatch" const { providerID, modelID } = normalizeModel() const runId = normalizeRunId() const share = normalizeShare() const oidcBaseUrl = normalizeOidcBaseUrl() const { owner, repo } = context.repo - // For schedule events, payload has no issue/comment data + // For repo events (schedule, workflow_dispatch), payload has no issue/comment data const payload = context.payload as | IssueCommentEvent + | IssuesEvent | PullRequestReviewCommentEvent + | WorkflowDispatchEvent | WorkflowRunEvent | PullRequestEvent const issueEvent = isIssueCommentEvent(payload) ? payload : undefined + // workflow_dispatch has an actor (the user who triggered it), schedule does not const actor = isScheduleEvent ? undefined : context.actor - const issueId = isScheduleEvent + const issueId = isRepoEvent ? undefined - : context.eventName === "issue_comment" - ? (payload as IssueCommentEvent).issue.number + : context.eventName === "issue_comment" || context.eventName === "issues" + ? (payload as IssueCommentEvent | IssuesEvent).issue.number : (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number const runUrl = `/${owner}/${repo}/actions/runs/${runId}` const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai" @@ -462,8 +484,8 @@ export const GithubRunCommand = cmd({ if (!useGithubToken) { await configureGit(appToken) } - // Skip permission check for schedule events (no actor to check) - if (!isScheduleEvent) { + // Skip permission check and reactions for repo events (no actor to check, no issue to react to) + if (isUserEvent) { await assertPermissions() await addReaction(commentType) } @@ -480,25 +502,30 @@ export const GithubRunCommand = cmd({ })() console.log("opencode session", session.id) - // Handle 4 cases - // 1. Schedule (no issue/PR context) - // 2. Issue - // 3. Local PR - // 4. Fork PR - if (isScheduleEvent) { - // Schedule event - no issue/PR context, output goes to logs - const branch = await checkoutNewBranch("schedule") + // Handle event types: + // REPO_EVENTS (schedule, workflow_dispatch): no issue/PR context, output to logs/PR only + // USER_EVENTS on PR (pull_request, pull_request_review_comment, issue_comment on PR): work on PR branch + // USER_EVENTS on Issue (issue_comment on issue, issues): create new branch, may create PR + if (isRepoEvent) { + // Repo event - no issue/PR context, output goes to logs + if (isWorkflowDispatchEvent && actor) { + console.log(`Triggered by: ${actor}`) + } + const branchPrefix = isWorkflowDispatchEvent ? "dispatch" : "schedule" + const branch = await checkoutNewBranch(branchPrefix) const head = (await $`git rev-parse HEAD`).stdout.toString().trim() const response = await chat(userPrompt, promptFiles) const { dirty, uncommittedChanges } = await branchIsDirty(head) if (dirty) { const summary = await summarize(response) - await pushToNewBranch(summary, branch, uncommittedChanges, true) + // workflow_dispatch has an actor for co-author attribution, schedule does not + await pushToNewBranch(summary, branch, uncommittedChanges, isScheduleEvent) + const triggerType = isWorkflowDispatchEvent ? "workflow_dispatch" : "scheduled workflow" const pr = await createPR( repoData.data.default_branch, branch, summary, - `${response}\n\nTriggered by scheduled workflow${footer({ image: true })}`, + `${response}\n\nTriggered by ${triggerType}${footer({ image: true })}`, ) console.log(`Created PR #${pr}`) } else { @@ -573,7 +600,7 @@ export const GithubRunCommand = cmd({ } else if (e instanceof Error) { msg = e.message } - if (!isScheduleEvent) { + if (isUserEvent) { await createComment(`${msg}${footer()}`) await removeReaction(commentType) } @@ -628,9 +655,15 @@ export const GithubRunCommand = cmd({ } function isIssueCommentEvent( - event: IssueCommentEvent | PullRequestReviewCommentEvent | WorkflowRunEvent | PullRequestEvent, + event: + | IssueCommentEvent + | IssuesEvent + | PullRequestReviewCommentEvent + | WorkflowDispatchEvent + | WorkflowRunEvent + | PullRequestEvent, ): event is IssueCommentEvent { - return "issue" in event + return "issue" in event && "comment" in event } function getReviewCommentContext() { @@ -652,10 +685,11 @@ export const GithubRunCommand = cmd({ async function getUserPrompt() { const customPrompt = process.env["PROMPT"] - // For schedule events, PROMPT is required since there's no comment to extract from - if (isScheduleEvent) { + // For repo events and issues events, PROMPT is required since there's no comment to extract from + if (isRepoEvent || isIssuesEvent) { if (!customPrompt) { - throw new Error("PROMPT input is required for scheduled events") + const eventType = isRepoEvent ? "scheduled and workflow_dispatch" : "issues" + throw new Error(`PROMPT input is required for ${eventType} events`) } return { userPrompt: customPrompt, promptFiles: [] } } @@ -923,7 +957,7 @@ export const GithubRunCommand = cmd({ await $`git config --local ${config} "${gitConfig}"` } - async function checkoutNewBranch(type: "issue" | "schedule") { + async function checkoutNewBranch(type: "issue" | "schedule" | "dispatch") { console.log("Checking out new branch...") const branch = generateBranchName(type) await $`git checkout -b ${branch}` @@ -952,16 +986,16 @@ export const GithubRunCommand = cmd({ await $`git checkout -b ${localBranch} fork/${remoteBranch}` } - function generateBranchName(type: "issue" | "pr" | "schedule") { + function generateBranchName(type: "issue" | "pr" | "schedule" | "dispatch") { const timestamp = new Date() .toISOString() .replace(/[:-]/g, "") .replace(/\.\d{3}Z/, "") .split("T") .join("") - if (type === "schedule") { + if (type === "schedule" || type === "dispatch") { const hex = crypto.randomUUID().slice(0, 6) - return `opencode/scheduled-${hex}-${timestamp}` + return `opencode/${type}-${hex}-${timestamp}` } return `opencode/${type}${issueId}-${timestamp}` } diff --git a/packages/web/src/content/docs/github.mdx b/packages/web/src/content/docs/github.mdx index 25c3ce927a1..63c5d855b9c 100644 --- a/packages/web/src/content/docs/github.mdx +++ b/packages/web/src/content/docs/github.mdx @@ -104,12 +104,14 @@ Or you can set it up manually. OpenCode can be triggered by the following GitHub events: -| Event Type | Triggered By | Details | -| ----------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `issue_comment` | Comment on an issue or PR | Mention `/opencode` or `/oc` in your comment. OpenCode reads the issue/PR context and can create branches, open PRs, or reply with explanations. | -| `pull_request_review_comment` | Comment on specific code lines in a PR | Mention `/opencode` or `/oc` while reviewing code. OpenCode receives file path, line numbers, and diff context for precise responses. | -| `schedule` | Cron-based schedule | Run OpenCode on a schedule using the `prompt` input. Useful for automated code reviews, reports, or maintenance tasks. OpenCode can create issues or PRs as needed. | -| `pull_request` | PR opened or updated | Automatically trigger OpenCode when PRs are opened, synchronized, or reopened. Useful for automated reviews without needing to leave a comment. | +| Event Type | Triggered By | Details | +| ----------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `issue_comment` | Comment on an issue or PR | Mention `/opencode` or `/oc` in your comment. OpenCode reads context and can create branches, open PRs, or reply. | +| `pull_request_review_comment` | Comment on specific code lines in a PR | Mention `/opencode` or `/oc` while reviewing code. OpenCode receives file path, line numbers, and diff context. | +| `issues` | Issue opened or edited | Automatically trigger OpenCode when issues are created or modified. Requires `prompt` input. | +| `pull_request` | PR opened or updated | Automatically trigger OpenCode when PRs are opened, synchronized, or reopened. Useful for automated reviews. | +| `schedule` | Cron-based schedule | Run OpenCode on a schedule. Requires `prompt` input. Output goes to logs and PRs (no issue to comment on). | +| `workflow_dispatch` | Manual trigger from GitHub UI | Trigger OpenCode on demand via Actions tab. Requires `prompt` input. Output goes to logs and PRs. | ### Schedule Example @@ -145,9 +147,7 @@ jobs: If you find issues worth addressing, open an issue to track them. ``` -For scheduled events, the `prompt` input is **required** since there's no comment to extract instructions from. - -> **Note:** Scheduled workflows run without a user context to permission-check, so the workflow must grant `contents: write` and `pull-requests: write` if you expect OpenCode to create branches or PRs during a scheduled run. +For scheduled events, the `prompt` input is **required** since there's no comment to extract instructions from. Scheduled workflows run without a user context to permission-check, so the workflow must grant `contents: write` and `pull-requests: write` if you expect OpenCode to create branches or PRs. --- @@ -188,6 +188,59 @@ For `pull_request` events, if no `prompt` is provided, OpenCode defaults to revi --- +### Issues Triage Example + +Automatically triage new issues. This example filters to accounts older than 30 days to reduce spam: + +```yaml title=".github/workflows/opencode-triage.yml" +name: Issue Triage + +on: + issues: + types: [opened] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + steps: + - name: Check account age + id: check + uses: actions/github-script@v7 + with: + script: | + const user = await github.rest.users.getByUsername({ + username: context.payload.issue.user.login + }); + const created = new Date(user.data.created_at); + const days = (Date.now() - created) / (1000 * 60 * 60 * 24); + return days >= 30; + result-encoding: string + + - uses: actions/checkout@v4 + if: steps.check.outputs.result == 'true' + + - uses: sst/opencode/github@latest + if: steps.check.outputs.result == 'true' + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + model: anthropic/claude-sonnet-4-20250514 + prompt: | + Review this issue. If there's a clear fix or relevant docs: + - Provide documentation links + - Add error handling guidance for code examples + Otherwise, do not comment. +``` + +For `issues` events, the `prompt` input is **required** since there's no comment to extract instructions from. + +--- + ## Custom prompts Override the default prompt to customize OpenCode's behavior for your workflow. From 160c8ab7cc5f91be3bc74e775f81f6eaa2d74dcc Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 13:44:52 -0600 Subject: [PATCH 062/164] tweak: bash tool description to avoid unnecessary 'cd &&' usage --- packages/opencode/src/tool/bash.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index a81deb62bf2..18ee14012b2 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -1,6 +1,6 @@ Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures. -All commands run in ${directory} by default. Use the `workdir` parameter if you need to run a command in a different directory. +All commands run in ${directory} by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID using `cd && ` patterns - use `workdir` instead. IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead. @@ -11,10 +11,10 @@ Before executing the command, please follow these steps: - For example, before running "mkdir foo/bar", first use `ls foo` to check that "foo" exists and is the intended parent directory 2. Command Execution: - - Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt") + - Always quote file paths that contain spaces with double quotes (e.g., rm "path with spaces/file.txt") - Examples of proper quoting: - - cd "/Users/name/My Documents" (correct) - - cd /Users/name/My Documents (incorrect - will fail) + - mkdir "/Users/name/My Documents" (correct) + - mkdir /Users/name/My Documents (incorrect - will fail) - python "/path/with spaces/script.py" (correct) - python /path/with spaces/script.py (incorrect - will fail) - After ensuring proper quoting, execute the command. @@ -26,7 +26,7 @@ Usage notes: - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - If the output exceeds 30000 characters, output will be truncated before being returned to you. - You can use the `run_in_background` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the Bash tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter. - + - Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands: - File search: Use Glob (NOT find or ls) - Content search: Use Grep (NOT grep or rg) @@ -39,9 +39,9 @@ Usage notes: - If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m "message" && git push`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead. - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail - DO NOT use newlines to separate commands (newlines are ok in quoted strings) - - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it. + - AVOID using `cd && `. Use the `workdir` parameter to change directories instead. - pytest /foo/bar/tests + Use workdir="/foo/bar" with command: pytest tests cd /foo/bar && pytest tests @@ -53,7 +53,7 @@ Only create commits when requested by the user. If unclear, ask first. When the Git Safety Protocol: - NEVER update the git config -- NEVER run destructive/irreversible git commands (like push --force, hard reset, etc) unless the user explicitly requests them +- NEVER run destructive/irreversible git commands (like push --force, hard reset, etc) unless the user explicitly requests them - NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it - NEVER run force push to main/master, warn the user if they request it - Avoid git commit --amend. ONLY use --amend when ALL conditions are met: From 664e6bf2d06fee041ec71ac8af84b031c5333ea1 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 14:30:05 -0600 Subject: [PATCH 063/164] test: add more tests to make sure that cwd is locked for read tool --- packages/opencode/test/tool/read.test.ts | 131 +++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 47a7aee2ae6..eb860d04fcc 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -13,6 +13,137 @@ const ctx = { metadata: () => {}, } +describe("tool.read external_directory permission", () => { + test("allows reading absolute path inside project directory", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "test.txt"), "hello world") + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + external_directory: "deny", + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const read = await ReadTool.init() + const result = await read.execute({ filePath: path.join(tmp.path, "test.txt") }, ctx) + expect(result.output).toContain("hello world") + }, + }) + }) + + test("allows reading file in subdirectory inside project directory", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "subdir", "test.txt"), "nested content") + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + external_directory: "deny", + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const read = await ReadTool.init() + const result = await read.execute({ filePath: path.join(tmp.path, "subdir", "test.txt") }, ctx) + expect(result.output).toContain("nested content") + }, + }) + }) + + test("denies reading absolute path outside project directory", async () => { + await using outerTmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "secret.txt"), "secret data") + }, + }) + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + external_directory: "deny", + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const read = await ReadTool.init() + await expect(read.execute({ filePath: path.join(outerTmp.path, "secret.txt") }, ctx)).rejects.toThrow( + "not in the current working directory", + ) + }, + }) + }) + + test("denies reading relative path that traverses outside project directory", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + external_directory: "deny", + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const read = await ReadTool.init() + await expect(read.execute({ filePath: "../../../etc/passwd" }, ctx)).rejects.toThrow( + "not in the current working directory", + ) + }, + }) + }) + + test("allows reading outside project directory when external_directory is allow", async () => { + await using outerTmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "external.txt"), "external content") + }, + }) + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + permission: { + external_directory: "allow", + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const read = await ReadTool.init() + const result = await read.execute({ filePath: path.join(outerTmp.path, "external.txt") }, ctx) + expect(result.output).toContain("external content") + }, + }) + }) +}) + describe("tool.read env file blocking", () => { test.each([ [".env", true], From 1e2ef07c9789b8f156cc8ad366111149b8fba255 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 26 Dec 2025 14:31:22 -0600 Subject: [PATCH 064/164] chore: kill some unused tools --- packages/opencode/src/tool/lsp-diagnostics.ts | 26 ---------------- .../opencode/src/tool/lsp-diagnostics.txt | 1 - packages/opencode/src/tool/lsp-hover.ts | 31 ------------------- packages/opencode/src/tool/lsp-hover.txt | 1 - 4 files changed, 59 deletions(-) delete mode 100644 packages/opencode/src/tool/lsp-diagnostics.ts delete mode 100644 packages/opencode/src/tool/lsp-diagnostics.txt delete mode 100644 packages/opencode/src/tool/lsp-hover.ts delete mode 100644 packages/opencode/src/tool/lsp-hover.txt diff --git a/packages/opencode/src/tool/lsp-diagnostics.ts b/packages/opencode/src/tool/lsp-diagnostics.ts deleted file mode 100644 index 18a6868b677..00000000000 --- a/packages/opencode/src/tool/lsp-diagnostics.ts +++ /dev/null @@ -1,26 +0,0 @@ -import z from "zod" -import { Tool } from "./tool" -import path from "path" -import { LSP } from "../lsp" -import DESCRIPTION from "./lsp-diagnostics.txt" -import { Instance } from "../project/instance" - -export const LspDiagnosticTool = Tool.define("lsp_diagnostics", { - description: DESCRIPTION, - parameters: z.object({ - path: z.string().describe("The path to the file to get diagnostics."), - }), - execute: async (args) => { - const normalized = path.isAbsolute(args.path) ? args.path : path.join(Instance.directory, args.path) - await LSP.touchFile(normalized, true) - const diagnostics = await LSP.diagnostics() - const file = diagnostics[normalized] - return { - title: path.relative(Instance.worktree, normalized), - metadata: { - diagnostics, - }, - output: file?.length ? file.map(LSP.Diagnostic.pretty).join("\n") : "No errors found", - } - }, -}) diff --git a/packages/opencode/src/tool/lsp-diagnostics.txt b/packages/opencode/src/tool/lsp-diagnostics.txt deleted file mode 100644 index 88a50f6347a..00000000000 --- a/packages/opencode/src/tool/lsp-diagnostics.txt +++ /dev/null @@ -1 +0,0 @@ -do not use diff --git a/packages/opencode/src/tool/lsp-hover.ts b/packages/opencode/src/tool/lsp-hover.ts deleted file mode 100644 index 7ef856cc567..00000000000 --- a/packages/opencode/src/tool/lsp-hover.ts +++ /dev/null @@ -1,31 +0,0 @@ -import z from "zod" -import { Tool } from "./tool" -import path from "path" -import { LSP } from "../lsp" -import DESCRIPTION from "./lsp-hover.txt" -import { Instance } from "../project/instance" - -export const LspHoverTool = Tool.define("lsp_hover", { - description: DESCRIPTION, - parameters: z.object({ - file: z.string().describe("The path to the file to get diagnostics."), - line: z.number().describe("The line number to get diagnostics."), - character: z.number().describe("The character number to get diagnostics."), - }), - execute: async (args) => { - const file = path.isAbsolute(args.file) ? args.file : path.join(Instance.directory, args.file) - await LSP.touchFile(file, true) - const result = await LSP.hover({ - ...args, - file, - }) - - return { - title: path.relative(Instance.worktree, file) + ":" + args.line + ":" + args.character, - metadata: { - result, - }, - output: JSON.stringify(result, null, 2), - } - }, -}) diff --git a/packages/opencode/src/tool/lsp-hover.txt b/packages/opencode/src/tool/lsp-hover.txt deleted file mode 100644 index 88a50f6347a..00000000000 --- a/packages/opencode/src/tool/lsp-hover.txt +++ /dev/null @@ -1 +0,0 @@ -do not use From b2f45d574f26e8c4c4d48a03ef51c3a421a48b50 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 26 Dec 2025 14:24:44 -0600 Subject: [PATCH 065/164] Reapply "feat(core): optional mdns service (#6192)" This reverts commit 505068d5a6cccc732aed76580f9bec6a5dbca507. --- bun.lock | 11 ++++ packages/opencode/package.json | 1 + packages/opencode/src/cli/cmd/acp.ts | 30 ++++------- packages/opencode/src/cli/cmd/serve.ts | 25 +++------ packages/opencode/src/cli/cmd/tui/spawn.ts | 28 ++++------ packages/opencode/src/cli/cmd/tui/thread.ts | 22 +++----- packages/opencode/src/cli/cmd/tui/worker.ts | 2 +- packages/opencode/src/cli/cmd/web.ts | 31 ++++------- packages/opencode/src/cli/network.ts | 42 +++++++++++++++ packages/opencode/src/config/config.ts | 12 +++++ packages/opencode/src/server/mdns.ts | 57 +++++++++++++++++++++ packages/opencode/src/server/server.ts | 33 ++++++++++-- packages/web/src/content/docs/cli.mdx | 18 ++++--- packages/web/src/content/docs/config.mdx | 25 +++++++++ packages/web/src/content/docs/server.mdx | 9 ++-- 15 files changed, 234 insertions(+), 112 deletions(-) create mode 100644 packages/opencode/src/cli/network.ts create mode 100644 packages/opencode/src/server/mdns.ts diff --git a/bun.lock b/bun.lock index d2fd6aa8de9..593033e34f4 100644 --- a/bun.lock +++ b/bun.lock @@ -292,6 +292,7 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", + "bonjour-service": "1.3.0", "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", @@ -1081,6 +1082,8 @@ "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], @@ -2003,6 +2006,8 @@ "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], @@ -2247,6 +2252,8 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -3023,6 +3030,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], "mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="], @@ -3595,6 +3604,8 @@ "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], + "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index bf45966d86c..55656660e06 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -88,6 +88,7 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", + "bonjour-service": "1.3.0", "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index c607e5f5bb7..2db64e3b1af 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -3,8 +3,10 @@ import { bootstrap } from "../bootstrap" import { cmd } from "./cmd" import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" import { ACP } from "@/acp/agent" +import { Config } from "@/config/config" import { Server } from "@/server/server" import { createOpencodeClient } from "@opencode-ai/sdk/v2" +import { withNetworkOptions, resolveNetworkOptions } from "../network" const log = Log.create({ service: "acp-command" }) @@ -19,29 +21,17 @@ export const AcpCommand = cmd({ command: "acp", describe: "start ACP (Agent Client Protocol) server", builder: (yargs) => { - return yargs - .option("cwd", { - describe: "working directory", - type: "string", - default: process.cwd(), - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }) + return withNetworkOptions(yargs).option("cwd", { + describe: "working directory", + type: "string", + default: process.cwd(), + }) }, handler: async (args) => { await bootstrap(process.cwd(), async () => { - const server = Server.listen({ - port: args.port, - hostname: args.hostname, - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}`, diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 3af3316a9d3..0fd7aa88f32 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,29 +1,16 @@ +import { Config } from "../../config/config" import { Server } from "../../server/server" import { cmd } from "./cmd" +import { withNetworkOptions, resolveNetworkOptions } from "../network" export const ServeCommand = cmd({ command: "serve", - builder: (yargs) => - yargs - .option("port", { - alias: ["p"], - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), + builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { - const hostname = args.hostname - const port = args.port - const server = Server.listen({ - port, - hostname, - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) console.log(`opencode server listening on http://${server.hostname}:${server.port}`) await new Promise(() => {}) await server.stop() diff --git a/packages/opencode/src/cli/cmd/tui/spawn.ts b/packages/opencode/src/cli/cmd/tui/spawn.ts index fa679529890..7ab846428d7 100644 --- a/packages/opencode/src/cli/cmd/tui/spawn.ts +++ b/packages/opencode/src/cli/cmd/tui/spawn.ts @@ -1,33 +1,23 @@ import { cmd } from "@/cli/cmd/cmd" +import { Config } from "@/config/config" import { Instance } from "@/project/instance" import path from "path" import { Server } from "@/server/server" import { upgrade } from "@/cli/upgrade" +import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" export const TuiSpawnCommand = cmd({ command: "spawn [project]", builder: (yargs) => - yargs - .positional("project", { - type: "string", - describe: "path to start opencode in", - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), + withNetworkOptions(yargs).positional("project", { + type: "string", + describe: "path to start opencode in", + }), handler: async (args) => { upgrade() - const server = Server.listen({ - port: args.port, - hostname: "127.0.0.1", - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) const bin = process.execPath const cmd = [] let cwd = process.cwd() diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 3cf8937a974..f75e3bd6511 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -6,6 +6,8 @@ import path from "path" import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" +import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import { Config } from "@/config/config" declare global { const OPENCODE_WORKER_PATH: string @@ -15,7 +17,7 @@ export const TuiThreadCommand = cmd({ command: "$0 [project]", describe: "start opencode tui", builder: (yargs) => - yargs + withNetworkOptions(yargs) .positional("project", { type: "string", describe: "path to start opencode in", @@ -36,23 +38,12 @@ export const TuiThreadCommand = cmd({ describe: "session id to continue", }) .option("prompt", { - alias: ["p"], type: "string", describe: "prompt to use", }) .option("agent", { type: "string", describe: "agent to use", - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", }), handler: async (args) => { // Resolve relative paths against PWD to preserve behavior when using --cwd flag @@ -87,10 +78,9 @@ export const TuiThreadCommand = cmd({ process.on("unhandledRejection", (e) => { Log.Default.error(e) }) - const server = await client.call("server", { - port: args.port, - hostname: args.hostname, - }) + const config = await Config.get() + const networkOpts = resolveNetworkOptions(args, config) + const server = await client.call("server", networkOpts) const prompt = await iife(async () => { const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined if (!args.prompt) return piped diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 76f78f3faa8..3ffc45ae884 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -30,7 +30,7 @@ process.on("uncaughtException", (e) => { let server: Bun.Server export const rpc = { - async server(input: { port: number; hostname: string }) { + async server(input: { port: number; hostname: string; mdns?: boolean }) { if (server) await server.stop(true) try { server = Server.listen(input) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 3d3036b1b07..adede03859c 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -1,6 +1,8 @@ +import { Config } from "../../config/config" import { Server } from "../../server/server" import { UI } from "../ui" import { cmd } from "./cmd" +import { withNetworkOptions, resolveNetworkOptions } from "../network" import open from "open" import { networkInterfaces } from "os" @@ -28,32 +30,17 @@ function getNetworkIPs() { export const WebCommand = cmd({ command: "web", - builder: (yargs) => - yargs - .option("port", { - alias: ["p"], - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), + builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { - const hostname = args.hostname - const port = args.port - const server = Server.listen({ - port, - hostname, - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) UI.empty() UI.println(UI.logo(" ")) UI.empty() - if (hostname === "0.0.0.0") { + if (opts.hostname === "0.0.0.0") { // Show localhost for local access const localhostUrl = `http://localhost:${server.port}` UI.println(UI.Style.TEXT_INFO_BOLD + " Local access: ", UI.Style.TEXT_NORMAL, localhostUrl) @@ -70,6 +57,10 @@ export const WebCommand = cmd({ } } + if (opts.mdns) { + UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, "opencode.local") + } + // Open localhost in browser open(localhostUrl.toString()).catch(() => {}) } else { diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts new file mode 100644 index 00000000000..661688572bd --- /dev/null +++ b/packages/opencode/src/cli/network.ts @@ -0,0 +1,42 @@ +import type { Argv, InferredOptionTypes } from "yargs" +import type { Config } from "../config/config" + +const options = { + port: { + type: "number" as const, + describe: "port to listen on", + default: 0, + }, + hostname: { + type: "string" as const, + describe: "hostname to listen on", + default: "127.0.0.1", + }, + mdns: { + type: "boolean" as const, + describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)", + default: false, + }, +} + +export type NetworkOptions = InferredOptionTypes + +export function withNetworkOptions(yargs: Argv) { + return yargs.options(options) +} + +export function resolveNetworkOptions(args: NetworkOptions, config?: Config.Info) { + const portExplicitlySet = process.argv.includes("--port") + const hostnameExplicitlySet = process.argv.includes("--hostname") + const mdnsExplicitlySet = process.argv.includes("--mdns") + + const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns) + const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port) + const hostname = hostnameExplicitlySet + ? args.hostname + : mdns && !config?.server?.hostname + ? "0.0.0.0" + : (config?.server?.hostname ?? args.hostname) + + return { hostname, port, mdns } +} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 802c5db9296..3350cf8f6a6 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -587,6 +587,17 @@ export namespace Config { .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), }) + export const Server = z + .object({ + port: z.number().int().positive().optional().describe("Port to listen on"), + hostname: z.string().optional().describe("Hostname to listen on"), + mdns: z.boolean().optional().describe("Enable mDNS service discovery"), + }) + .strict() + .meta({ + ref: "ServerConfig", + }) + export const Layout = z.enum(["auto", "stretch"]).meta({ ref: "LayoutConfig", }) @@ -635,6 +646,7 @@ export namespace Config { keybinds: Keybinds.optional().describe("Custom keybind configurations"), logLevel: Log.Level.optional().describe("Log level"), tui: TUI.optional().describe("TUI specific settings"), + server: Server.optional().describe("Server configuration for opencode serve and web commands"), command: z .record(z.string(), Command) .optional() diff --git a/packages/opencode/src/server/mdns.ts b/packages/opencode/src/server/mdns.ts new file mode 100644 index 00000000000..45e61d361ac --- /dev/null +++ b/packages/opencode/src/server/mdns.ts @@ -0,0 +1,57 @@ +import { Log } from "@/util/log" +import Bonjour from "bonjour-service" + +const log = Log.create({ service: "mdns" }) + +export namespace MDNS { + let bonjour: Bonjour | undefined + let currentPort: number | undefined + + export function publish(port: number, name = "opencode") { + if (currentPort === port) return + if (bonjour) unpublish() + + try { + bonjour = new Bonjour() + const service = bonjour.publish({ + name, + type: "http", + port, + txt: { path: "/" }, + }) + + service.on("up", () => { + log.info("mDNS service published", { name, port }) + }) + + service.on("error", (err) => { + log.error("mDNS service error", { error: err }) + }) + + currentPort = port + } catch (err) { + log.error("mDNS publish failed", { error: err }) + if (bonjour) { + try { + bonjour.destroy() + } catch {} + } + bonjour = undefined + currentPort = undefined + } + } + + export function unpublish() { + if (bonjour) { + try { + bonjour.unpublishAll() + bonjour.destroy() + } catch (err) { + log.error("mDNS unpublish failed", { error: err }) + } + bonjour = undefined + currentPort = undefined + log.info("mDNS service unpublished") + } + } +} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index c74dbbb41ef..65393e12897 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -45,9 +45,11 @@ import { Snapshot } from "@/snapshot" import { SessionSummary } from "@/session/summary" import { SessionStatus } from "@/session/status" import { upgradeWebSocket, websocket } from "hono/bun" +import type { BunWebSocketData } from "hono/bun" import { errors } from "./error" import { Pty } from "@/pty" import { Installation } from "@/installation" +import { MDNS } from "./mdns" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false @@ -2623,20 +2625,41 @@ export namespace Server { return result } - export function listen(opts: { port: number; hostname: string }) { + export function listen(opts: { port: number; hostname: string; mdns?: boolean }) { const args = { hostname: opts.hostname, idleTimeout: 0, fetch: App().fetch, websocket: websocket, } as const - if (opts.port === 0) { + const tryServe = (port: number) => { try { - return Bun.serve({ ...args, port: 4096 }) + return Bun.serve({ ...args, port }) } catch { - // port 4096 not available, fall through to use port 0 + return undefined } } - return Bun.serve({ ...args, port: opts.port }) + const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port) + if (!server) throw new Error(`Failed to start server on port ${opts.port}`) + + const shouldPublishMDNS = + opts.mdns && + server.port && + opts.hostname !== "127.0.0.1" && + opts.hostname !== "localhost" && + opts.hostname !== "::1" + if (shouldPublishMDNS) { + MDNS.publish(server.port!) + } else if (opts.mdns) { + log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") + } + + const originalStop = server.stop.bind(server) + server.stop = async (closeActiveConnections?: boolean) => { + if (shouldPublishMDNS) MDNS.unpublish() + return originalStop(closeActiveConnections) + } + + return server } } diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index e4e40ac7a4c..4a826e5b3ff 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -335,10 +335,11 @@ This starts an HTTP server that provides API access to opencode functionality wi #### Flags -| Flag | Short | Description | -| ------------ | ----- | --------------------- | -| `--port` | `-p` | Port to listen on | -| `--hostname` | | Hostname to listen on | +| Flag | Description | +| ------------ | --------------------- | +| `--port` | Port to listen on | +| `--hostname` | Hostname to listen on | +| `--mdns` | Enable mDNS discovery | --- @@ -428,10 +429,11 @@ This starts an HTTP server and opens a web browser to access OpenCode through a #### Flags -| Flag | Short | Description | -| ------------ | ----- | --------------------- | -| `--port` | `-p` | Port to listen on | -| `--hostname` | | Hostname to listen on | +| Flag | Description | +| ------------ | --------------------- | +| `--port` | Port to listen on | +| `--hostname` | Hostname to listen on | +| `--mdns` | Enable mDNS discovery | --- diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index ebaff36bb15..d7f8031782c 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -120,6 +120,31 @@ Available options: --- +### Server + +You can configure server settings for the `opencode serve` and `opencode web` commands through the `server` option. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "server": { + "port": 4096, + "hostname": "0.0.0.0", + "mdns": true + } +} +``` + +Available options: + +- `port` - Port to listen on. +- `hostname` - Hostname to listen on. When `mdns` is enabled and no hostname is set, defaults to `0.0.0.0`. +- `mdns` - Enable mDNS service discovery. This allows other devices on the network to discover your OpenCode server. + +[Learn more about the server here](/docs/server). + +--- + ### Tools You can manage the tools an LLM can use through the `tools` option. diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx index 427d8f505ff..c63917f792e 100644 --- a/packages/web/src/content/docs/server.mdx +++ b/packages/web/src/content/docs/server.mdx @@ -18,10 +18,11 @@ opencode serve [--port ] [--hostname ] #### Options -| Flag | Short | Description | Default | -| ------------ | ----- | --------------------- | ----------- | -| `--port` | `-p` | Port to listen on | `4096` | -| `--hostname` | `-h` | Hostname to listen on | `127.0.0.1` | +| Flag | Description | Default | +| ------------ | --------------------- | ----------- | +| `--port` | Port to listen on | `4096` | +| `--hostname` | Hostname to listen on | `127.0.0.1` | +| `--mdns` | Enable mDNS discovery | `false` | --- From 390b0a79b395d6b44649c94087db2bbc8937a2f2 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 26 Dec 2025 14:47:44 -0600 Subject: [PATCH 066/164] fix(core): mdns global config --- packages/opencode/src/cli/cmd/acp.ts | 4 +--- packages/opencode/src/cli/cmd/serve.ts | 4 +--- packages/opencode/src/cli/cmd/tui/spawn.ts | 4 +--- packages/opencode/src/cli/cmd/tui/thread.ts | 6 ++---- packages/opencode/src/cli/cmd/web.ts | 4 +--- packages/opencode/src/cli/network.ts | 5 +++-- 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index 2db64e3b1af..060d0d5a156 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -3,7 +3,6 @@ import { bootstrap } from "../bootstrap" import { cmd } from "./cmd" import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" import { ACP } from "@/acp/agent" -import { Config } from "@/config/config" import { Server } from "@/server/server" import { createOpencodeClient } from "@opencode-ai/sdk/v2" import { withNetworkOptions, resolveNetworkOptions } from "../network" @@ -29,8 +28,7 @@ export const AcpCommand = cmd({ }, handler: async (args) => { await bootstrap(process.cwd(), async () => { - const config = await Config.get() - const opts = resolveNetworkOptions(args, config) + const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) const sdk = createOpencodeClient({ diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 0fd7aa88f32..657f9196c96 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,4 +1,3 @@ -import { Config } from "../../config/config" import { Server } from "../../server/server" import { cmd } from "./cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" @@ -8,8 +7,7 @@ export const ServeCommand = cmd({ builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { - const config = await Config.get() - const opts = resolveNetworkOptions(args, config) + const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) console.log(`opencode server listening on http://${server.hostname}:${server.port}`) await new Promise(() => {}) diff --git a/packages/opencode/src/cli/cmd/tui/spawn.ts b/packages/opencode/src/cli/cmd/tui/spawn.ts index 7ab846428d7..ef359e6f40e 100644 --- a/packages/opencode/src/cli/cmd/tui/spawn.ts +++ b/packages/opencode/src/cli/cmd/tui/spawn.ts @@ -1,5 +1,4 @@ import { cmd } from "@/cli/cmd/cmd" -import { Config } from "@/config/config" import { Instance } from "@/project/instance" import path from "path" import { Server } from "@/server/server" @@ -15,8 +14,7 @@ export const TuiSpawnCommand = cmd({ }), handler: async (args) => { upgrade() - const config = await Config.get() - const opts = resolveNetworkOptions(args, config) + const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) const bin = process.execPath const cmd = [] diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index f75e3bd6511..280f40fb90b 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -7,7 +7,6 @@ import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" -import { Config } from "@/config/config" declare global { const OPENCODE_WORKER_PATH: string @@ -78,9 +77,8 @@ export const TuiThreadCommand = cmd({ process.on("unhandledRejection", (e) => { Log.Default.error(e) }) - const config = await Config.get() - const networkOpts = resolveNetworkOptions(args, config) - const server = await client.call("server", networkOpts) + const opts = await resolveNetworkOptions(args) + const server = await client.call("server", opts) const prompt = await iife(async () => { const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined if (!args.prompt) return piped diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index adede03859c..fb32472d7ab 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -1,4 +1,3 @@ -import { Config } from "../../config/config" import { Server } from "../../server/server" import { UI } from "../ui" import { cmd } from "./cmd" @@ -33,8 +32,7 @@ export const WebCommand = cmd({ builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { - const config = await Config.get() - const opts = resolveNetworkOptions(args, config) + const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) UI.empty() UI.println(UI.logo(" ")) diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts index 661688572bd..397f2ba3e20 100644 --- a/packages/opencode/src/cli/network.ts +++ b/packages/opencode/src/cli/network.ts @@ -1,5 +1,5 @@ import type { Argv, InferredOptionTypes } from "yargs" -import type { Config } from "../config/config" +import { Config } from "../config/config" const options = { port: { @@ -25,7 +25,8 @@ export function withNetworkOptions(yargs: Argv) { return yargs.options(options) } -export function resolveNetworkOptions(args: NetworkOptions, config?: Config.Info) { +export async function resolveNetworkOptions(args: NetworkOptions) { + const config = await Config.global() const portExplicitlySet = process.argv.includes("--port") const hostnameExplicitlySet = process.argv.includes("--hostname") const mdnsExplicitlySet = process.argv.includes("--mdns") From da394439a19e03e06491395a3e178cb47a9537c6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Dec 2025 20:48:30 +0000 Subject: [PATCH 067/164] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 19 +++++++++++++++++++ packages/sdk/openapi.json | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index b1d05813ce3..6b5cfc818d0 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1156,6 +1156,24 @@ export type KeybindsConfig = { */ export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { + /** + * Port to listen on + */ + port?: number + /** + * Hostname to listen on + */ + hostname?: string + /** + * Enable mDNS service discovery + */ + mdns?: boolean +} + export type AgentConfig = { model?: string temperature?: number @@ -1416,6 +1434,7 @@ export type Config = { */ diff_style?: "auto" | "stacked" } + server?: ServerConfig /** * Command configuration, see https://opencode.ai/docs/commands */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index ef6ee0ebfa0..2a2984854f8 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7692,6 +7692,27 @@ "type": "string", "enum": ["DEBUG", "INFO", "WARN", "ERROR"] }, + "ServerConfig": { + "description": "Server configuration for opencode serve and web commands", + "type": "object", + "properties": { + "port": { + "description": "Port to listen on", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "hostname": { + "description": "Hostname to listen on", + "type": "string" + }, + "mdns": { + "description": "Enable mDNS service discovery", + "type": "boolean" + } + }, + "additionalProperties": false + }, "AgentConfig": { "type": "object", "properties": { @@ -8178,6 +8199,9 @@ } } }, + "server": { + "$ref": "#/components/schemas/ServerConfig" + }, "command": { "description": "Command configuration, see https://opencode.ai/docs/commands", "type": "object", From a15397cd89881c36c19b3f909ebceddb47aeca90 Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 26 Dec 2025 20:49:05 +0000 Subject: [PATCH 068/164] Update Nix flake.lock and hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 9ef78c2321d..f43b14684c3 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-CTW7pzZ0Kq5HHF5xgEh3EnuwnqtPsDkc5ImmZjJgwA8=" + "nodeModules": "sha256-lloUZt5mLyNWAcbQrJB4wGUKvKu24WFEhOLfZD5/FMg=" } From 52b99622ad0c01d11a5f8a5826ed955f6aa13dca Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 26 Dec 2025 17:32:37 -0500 Subject: [PATCH 069/164] zen: add context for login errors --- .../console/app/src/routes/auth/callback.ts | 54 +++++++++++-------- packages/console/function/src/auth.ts | 6 ++- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/console/app/src/routes/auth/callback.ts b/packages/console/app/src/routes/auth/callback.ts index a793b85962a..2f8781e9882 100644 --- a/packages/console/app/src/routes/auth/callback.ts +++ b/packages/console/app/src/routes/auth/callback.ts @@ -5,28 +5,36 @@ import { useAuthSession } from "~/context/auth.session" export async function GET(input: APIEvent) { const url = new URL(input.request.url) - const code = url.searchParams.get("code") - if (!code) throw new Error("No code found") - const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`) - if (result.err) { - throw new Error(result.err.message) - } - const decoded = AuthClient.decode(result.tokens.access, {} as any) - if (decoded.err) throw new Error(decoded.err.message) - const session = await useAuthSession() - const id = decoded.subject.properties.accountID - await session.update((value) => { - return { - ...value, - account: { - ...value.account, - [id]: { - id, - email: decoded.subject.properties.email, + try { + const code = url.searchParams.get("code") + if (!code) throw new Error("No code found") + const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`) + if (result.err) throw new Error(result.err.message) + const decoded = AuthClient.decode(result.tokens.access, {} as any) + if (decoded.err) throw new Error(decoded.err.message) + const session = await useAuthSession() + const id = decoded.subject.properties.accountID + await session.update((value) => { + return { + ...value, + account: { + ...value.account, + [id]: { + id, + email: decoded.subject.properties.email, + }, }, - }, - current: id, - } - }) - return redirect("/auth") + current: id, + } + }) + return redirect("/auth") + } catch (e: any) { + return new Response( + JSON.stringify({ + error: e.message, + cause: Object.fromEntries(url.searchParams.entries()), + }), + { status: 500 }, + ) + } } diff --git a/packages/console/function/src/auth.ts b/packages/console/function/src/auth.ts index 742e0d567ce..082564b21ce 100644 --- a/packages/console/function/src/auth.ts +++ b/packages/console/function/src/auth.ts @@ -123,7 +123,11 @@ export default { }, }).then((x) => x.json())) as any subject = user.id.toString() - email = emails.find((x: any) => x.primary && x.verified)?.email + + const primaryEmail = emails.find((x: any) => x.primary) + if (!primaryEmail) throw new Error("No primary email found for GitHub user") + if (!primaryEmail.verified) throw new Error("Primary email for GitHub user not verified") + email = primaryEmail.email } else if (response.provider === "google") { if (!response.id.email_verified) throw new Error("Google email not verified") subject = response.id.sub as string From ed06de5e307c386f6aaf6294ea614deb64ddad30 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 26 Dec 2025 19:31:42 -0500 Subject: [PATCH 070/164] core: add configurable compaction settings to allow users to disable auto-compaction and pruning via config instead of flags --- packages/opencode/src/config/config.ts | 14 ++++++++++++++ packages/opencode/src/session/compaction.ts | 10 ++++++---- packages/opencode/src/session/prompt.ts | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3350cf8f6a6..c94a34be0e6 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -141,6 +141,14 @@ export namespace Config { if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({}) + // Apply flag overrides for compaction settings + if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) { + result.compaction = { ...result.compaction, auto: false } + } + if (Flag.OPENCODE_DISABLE_PRUNE) { + result.compaction = { ...result.compaction, prune: false } + } + return { config: result, directories, @@ -791,6 +799,12 @@ export namespace Config { url: z.string().optional().describe("Enterprise URL"), }) .optional(), + compaction: z + .object({ + auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"), + prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"), + }) + .optional(), experimental: z .object({ hook: z diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 339ba2f42a1..b3d17794b95 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -7,13 +7,13 @@ import { Provider } from "../provider/provider" import { MessageV2 } from "./message-v2" import z from "zod" import { SessionPrompt } from "./prompt" -import { Flag } from "../flag/flag" import { Token } from "../util/token" import { Log } from "../util/log" import { SessionProcessor } from "./processor" import { fn } from "@/util/fn" import { Agent } from "@/agent/agent" import { Plugin } from "@/plugin" +import { Config } from "@/config/config" export namespace SessionCompaction { const log = Log.create({ service: "session.compaction" }) @@ -27,8 +27,9 @@ export namespace SessionCompaction { ), } - export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) { - if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false + export async function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) { + const config = await Config.get() + if ((config.compaction?.auto ?? true) === false) return false const context = input.model.limit.context if (context === 0) return false const count = input.tokens.input + input.tokens.cache.read + input.tokens.output @@ -46,7 +47,8 @@ export namespace SessionCompaction { // calls. then erases output of previous tool calls. idea is to throw away old // tool calls that are no longer relevant. export async function prune(input: { sessionID: string }) { - if (Flag.OPENCODE_DISABLE_PRUNE) return + const config = await Config.get() + if ((config.compaction?.prune ?? true) === false) return log.info("pruning") const msgs = await Session.messages({ sessionID: input.sessionID }) let total = 0 diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index fabe3fa5128..19dc90b3bcb 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -459,7 +459,7 @@ export namespace SessionPrompt { if ( lastFinished && lastFinished.summary !== true && - SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model }) + (await SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })) ) { await SessionCompaction.create({ sessionID, From f8fb08b3b42007d6b1ab995e6c55d1996050de69 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 27 Dec 2025 00:32:34 +0000 Subject: [PATCH 071/164] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 10 ++++++++++ packages/sdk/openapi.json | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 6b5cfc818d0..90b2154e18a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1586,6 +1586,16 @@ export type Config = { */ url?: string } + compaction?: { + /** + * Enable automatic compaction when context is full (default: true) + */ + auto?: boolean + /** + * Enable pruning of old tool outputs (default: true) + */ + prune?: boolean + } experimental?: { hook?: { file_edited?: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 2a2984854f8..c3658a90c50 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8566,6 +8566,19 @@ } } }, + "compaction": { + "type": "object", + "properties": { + "auto": { + "description": "Enable automatic compaction when context is full (default: true)", + "type": "boolean" + }, + "prune": { + "description": "Enable pruning of old tool outputs (default: true)", + "type": "boolean" + } + } + }, "experimental": { "type": "object", "properties": { From 2cdc88d295f3cdac432ea81657d2f5f9da2bde45 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 26 Dec 2025 19:44:31 -0500 Subject: [PATCH 072/164] core: add compaction config tests to verify auto and prune settings work correctly --- packages/opencode/test/config/config.test.ts | 94 ++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 6f43cab6174..8871fd50bab 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -533,3 +533,97 @@ test("deduplicates duplicate plugins from global and local configs", async () => }, }) }) + +test("compaction config defaults to true when not specified", 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, + fn: async () => { + const config = await Config.get() + // When not specified, compaction should be undefined (defaults handled in usage) + expect(config.compaction).toBeUndefined() + }, + }) +}) + +test("compaction config can disable auto compaction", 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", + compaction: { + auto: false, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.compaction?.auto).toBe(false) + expect(config.compaction?.prune).toBeUndefined() + }, + }) +}) + +test("compaction config can disable prune", 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", + compaction: { + prune: false, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.compaction?.prune).toBe(false) + expect(config.compaction?.auto).toBeUndefined() + }, + }) +}) + +test("compaction config can disable both auto and prune", 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", + compaction: { + auto: false, + prune: false, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.compaction?.auto).toBe(false) + expect(config.compaction?.prune).toBe(false) + }, + }) +}) From 2b054bec9582b6a6ba421d5ea40576878f8e59e8 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 26 Dec 2025 19:48:56 -0500 Subject: [PATCH 073/164] core: fix compaction config checks to properly respect user settings --- packages/opencode/src/session/compaction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index b3d17794b95..42bab2eb975 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -29,7 +29,7 @@ export namespace SessionCompaction { export async function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) { const config = await Config.get() - if ((config.compaction?.auto ?? true) === false) return false + if (config.compaction?.auto === false) return false const context = input.model.limit.context if (context === 0) return false const count = input.tokens.input + input.tokens.cache.read + input.tokens.output @@ -48,7 +48,7 @@ export namespace SessionCompaction { // tool calls that are no longer relevant. export async function prune(input: { sessionID: string }) { const config = await Config.get() - if ((config.compaction?.prune ?? true) === false) return + if (config.compaction?.prune === false) return log.info("pruning") const msgs = await Session.messages({ sessionID: input.sessionID }) let total = 0 From 4385fa4dd79955cdb1d7086365ee1a238ebf9748 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 26 Dec 2025 20:47:13 -0600 Subject: [PATCH 074/164] fix(desktop): prompt input fixes, directory and branch in status bar --- packages/app/src/components/prompt-input.tsx | 161 +++++++++++++++++-- packages/app/src/components/status-bar.tsx | 26 ++- packages/app/src/context/global-sync.tsx | 8 + 3 files changed, 176 insertions(+), 19 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 03fa02fe35d..2407fe97a9c 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -82,6 +82,37 @@ export const PromptInput: Component = (props) => { const command = useCommand() let editorRef!: HTMLDivElement let fileInputRef!: HTMLInputElement + let scrollRef!: HTMLDivElement + + const scrollCursorIntoView = () => { + const container = scrollRef + const selection = window.getSelection() + if (!container || !selection || selection.rangeCount === 0) return + + const range = selection.getRangeAt(0) + if (!editorRef.contains(range.startContainer)) return + + const rect = range.getBoundingClientRect() + if (!rect.height) return + + const containerRect = container.getBoundingClientRect() + const top = rect.top - containerRect.top + container.scrollTop + const bottom = rect.bottom - containerRect.top + container.scrollTop + const padding = 12 + + if (top < container.scrollTop + padding) { + container.scrollTop = Math.max(0, top - padding) + return + } + + if (bottom > container.scrollTop + container.clientHeight - padding) { + container.scrollTop = bottom - container.clientHeight + padding + } + } + + const queueScroll = () => { + requestAnimationFrame(scrollCursorIntoView) + } const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) @@ -153,6 +184,7 @@ export const PromptInput: Component = (props) => { editorRef.focus() setCursorPosition(editorRef, length) setStore("applyingHistory", false) + queueScroll() }) } @@ -357,9 +389,23 @@ export const PromptInput: Component = (props) => { (currentParts) => { const domParts = parseFromDOM() const normalized = Array.from(editorRef.childNodes).every((node) => { - if (node.nodeType === Node.TEXT_NODE) return true + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent ?? "" + if (!text.includes("\u200B")) return true + if (text !== "\u200B") return false + + const prev = node.previousSibling + const next = node.nextSibling + const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR" + const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR" + if (!prevIsBr && !nextIsBr) return false + if (nextIsBr && !prevIsBr && prev) return false + return true + } if (node.nodeType !== Node.ELEMENT_NODE) return false - return (node as HTMLElement).dataset.type === "file" + const el = node as HTMLElement + if (el.dataset.type === "file") return true + return el.tagName === "BR" }) if (normalized && isPromptEqual(currentParts, domParts)) return @@ -372,7 +418,7 @@ export const PromptInput: Component = (props) => { editorRef.innerHTML = "" currentParts.forEach((part) => { if (part.type === "text") { - editorRef.appendChild(document.createTextNode(part.content)) + editorRef.appendChild(createTextFragment(part.content)) } else if (part.type === "file") { const pill = document.createElement("span") pill.textContent = part.content @@ -398,7 +444,7 @@ export const PromptInput: Component = (props) => { let buffer = "" const flushText = () => { - const content = buffer.replace(/\r\n?/g, "\n") + const content = buffer.replace(/\r\n?/g, "\n").replace(/\u200B/g, "") buffer = "" if (!content) return parts.push({ type: "text", content, start: position, end: position + content.length }) @@ -472,6 +518,7 @@ export const PromptInput: Component = (props) => { if (prompt.dirty()) { prompt.set(DEFAULT_PROMPT, 0) } + queueScroll() return } @@ -500,6 +547,7 @@ export const PromptInput: Component = (props) => { } prompt.set(rawParts, cursorPosition) + queueScroll() } const addPart = (part: ContentPart) => { @@ -529,9 +577,10 @@ export const PromptInput: Component = (props) => { const nodes = Array.from(editorRef.childNodes) for (const node of nodes) { - const length = node.textContent?.length ?? 0 + const length = getNodeLength(node) const isText = node.nodeType === Node.TEXT_NODE const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file" + const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR" if (isText && remaining <= length) { if (edge === "start") range.setStart(node, remaining) @@ -539,7 +588,7 @@ export const PromptInput: Component = (props) => { return } - if (isFile && remaining <= length) { + if ((isFile || isBreak) && remaining <= length) { if (edge === "start" && remaining === 0) range.setStartBefore(node) if (edge === "start" && remaining > 0) range.setStartAfter(node) if (edge === "end" && remaining === 0) range.setEndBefore(node) @@ -565,11 +614,25 @@ export const PromptInput: Component = (props) => { selection.removeAllRanges() selection.addRange(range) } else if (part.type === "text") { - const textNode = document.createTextNode(part.content) const range = selection.getRangeAt(0) + const fragment = createTextFragment(part.content) + const last = fragment.lastChild range.deleteContents() - range.insertNode(textNode) - range.setStartAfter(textNode) + range.insertNode(fragment) + if (last) { + if (last.nodeType === Node.TEXT_NODE) { + const text = last.textContent ?? "" + if (text === "\u200B") { + range.setStart(last, 0) + } + if (text !== "\u200B") { + range.setStart(last, text.length) + } + } + if (last.nodeType !== Node.TEXT_NODE) { + range.setStartAfter(last) + } + } range.collapse(true) selection.removeAllRanges() selection.addRange(range) @@ -646,6 +709,24 @@ export const PromptInput: Component = (props) => { } const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Backspace") { + const selection = window.getSelection() + if (selection && selection.isCollapsed) { + const node = selection.anchorNode + const offset = selection.anchorOffset + if (node && node.nodeType === Node.TEXT_NODE) { + const text = node.textContent ?? "" + if (/^\u200B+$/.test(text) && offset > 0) { + const range = document.createRange() + range.setStart(node, 0) + range.collapse(true) + selection.removeAllRanges() + selection.addRange(range) + } + } + } + } + if (event.key === "!" && store.mode === "normal") { const cursorPosition = getCursorPosition(editorRef) if (cursorPosition === 0) { @@ -686,7 +767,10 @@ export const PromptInput: Component = (props) => { const cursorPosition = getCursorPosition(editorRef) const textLength = promptLength(prompt.current()) - const textContent = editorRef.textContent ?? "" + const textContent = prompt + .current() + .map((part) => ("content" in part ? part.content : "")) + .join("") const isEmpty = textContent.trim() === "" || textLength <= 1 const hasNewlines = textContent.includes("\n") const inHistory = store.historyIndex >= 0 @@ -978,7 +1062,7 @@ export const PromptInput: Component = (props) => {
-
+
(scrollRef = el)}>
{ @@ -1119,23 +1203,56 @@ export const PromptInput: Component = (props) => { ) } +function createTextFragment(content: string): DocumentFragment { + const fragment = document.createDocumentFragment() + const segments = content.split("\n") + segments.forEach((segment, index) => { + if (segment) { + fragment.appendChild(document.createTextNode(segment)) + } else if (segments.length > 1) { + fragment.appendChild(document.createTextNode("\u200B")) + } + if (index < segments.length - 1) { + fragment.appendChild(document.createElement("br")) + } + }) + return fragment +} + +function getNodeLength(node: Node): number { + if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1 + return (node.textContent ?? "").replace(/\u200B/g, "").length +} + +function getTextLength(node: Node): number { + if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? "").replace(/\u200B/g, "").length + if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1 + let length = 0 + for (const child of Array.from(node.childNodes)) { + length += getTextLength(child) + } + return length +} + function getCursorPosition(parent: HTMLElement): number { const selection = window.getSelection() if (!selection || selection.rangeCount === 0) return 0 const range = selection.getRangeAt(0) + if (!parent.contains(range.startContainer)) return 0 const preCaretRange = range.cloneRange() preCaretRange.selectNodeContents(parent) preCaretRange.setEnd(range.startContainer, range.startOffset) - return preCaretRange.toString().length + return getTextLength(preCaretRange.cloneContents()) } function setCursorPosition(parent: HTMLElement, position: number) { let remaining = position let node = parent.firstChild while (node) { - const length = node.textContent ? node.textContent.length : 0 + const length = getNodeLength(node) const isText = node.nodeType === Node.TEXT_NODE const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file" + const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR" if (isText && remaining <= length) { const range = document.createRange() @@ -1147,10 +1264,24 @@ function setCursorPosition(parent: HTMLElement, position: number) { return } - if (isFile && remaining <= length) { + if ((isFile || isBreak) && remaining <= length) { const range = document.createRange() const selection = window.getSelection() - range.setStartAfter(node) + if (remaining === 0) { + range.setStartBefore(node) + } + if (remaining > 0 && isFile) { + range.setStartAfter(node) + } + if (remaining > 0 && isBreak) { + const next = node.nextSibling + if (next && next.nodeType === Node.TEXT_NODE) { + range.setStart(next, 0) + } + if (!next || next.nodeType !== Node.TEXT_NODE) { + range.setStartAfter(node) + } + } range.collapse(true) selection?.removeAllRanges() selection?.addRange(range) diff --git a/packages/app/src/components/status-bar.tsx b/packages/app/src/components/status-bar.tsx index e0e25c60b8b..d8a88503f20 100644 --- a/packages/app/src/components/status-bar.tsx +++ b/packages/app/src/components/status-bar.tsx @@ -1,13 +1,31 @@ -import { Show, type ParentProps } from "solid-js" +import { createMemo, Show, type ParentProps } from "solid-js" import { usePlatform } from "@/context/platform" +import { useSync } from "@/context/sync" +import { useGlobalSync } from "@/context/global-sync" export function StatusBar(props: ParentProps) { const platform = usePlatform() + const sync = useSync() + const globalSync = useGlobalSync() + + const directoryDisplay = createMemo(() => { + const directory = sync.data.path.directory || "" + const home = globalSync.data.path.home || "" + const short = home && directory.startsWith(home) ? directory.replace(home, "~") : directory + const branch = sync.data.vcs?.branch + return branch ? `${short}:${branch}` : short + }) + return (
- - v{platform.version} - +
+ + v{platform.version} + + + {directoryDisplay()} + +
{props.children}
) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 7a9dc8dc425..c51901eb254 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -14,6 +14,7 @@ import { type Command, type McpStatus, type LspStatus, + type VcsInfo, createOpencodeClient, } from "@opencode-ai/sdk/v2/client" import { createStore, produce, reconcile } from "solid-js/store" @@ -47,6 +48,7 @@ type State = { [name: string]: McpStatus } lsp: LspStatus[] + vcs: VcsInfo | undefined limit: number message: { [sessionID: string]: Message[] @@ -93,6 +95,7 @@ function createGlobalSync() { todo: {}, mcp: {}, lsp: [], + vcs: undefined, limit: 5, message: {}, part: {}, @@ -159,6 +162,7 @@ function createGlobalSync() { config: () => sdk.config.get().then((x) => setStore("config", x.data!)), mcp: () => sdk.mcp.status().then((x) => setStore("mcp", x.data ?? {})), lsp: () => sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])), + vcs: () => sdk.vcs.get().then((x) => setStore("vcs", x.data)), } await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e)))) .then(() => setStore("ready", true)) @@ -305,6 +309,10 @@ function createGlobalSync() { } break } + case "vcs.branch.updated": { + setStore("vcs", { branch: event.properties.branch }) + break + } } }) From 1bcc72c477556088a0a717413dba7acaf58fe204 Mon Sep 17 00:00:00 2001 From: ja <51257127+anntnzrb@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:12:35 -0500 Subject: [PATCH 075/164] feat: add ability to disable spinner animation (#6084) --- .../cli/cmd/tui/component/dialog-session-list.tsx | 10 ++++++++-- .../src/cli/cmd/tui/component/prompt/index.tsx | 9 +++++++-- .../src/cli/cmd/tui/routes/session/index.tsx | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 1217bb54ae0..cb7b5d282ee 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -2,12 +2,13 @@ import { useDialog } from "@tui/ui/dialog" import { DialogSelect } from "@tui/ui/dialog-select" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" -import { createEffect, createMemo, createSignal, onMount } from "solid-js" +import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js" import { Locale } from "@/util/locale" import { Keybind } from "@/util/keybind" import { useTheme } from "../context/theme" import { useSDK } from "../context/sdk" import { DialogSessionRename } from "./dialog-session-rename" +import { useKV } from "../context/kv" import "opentui-spinner/solid" export function DialogSessionList() { @@ -16,6 +17,7 @@ export function DialogSessionList() { const { theme } = useTheme() const route = useRoute() const sdk = useSDK() + const kv = useKV() const [toDelete, setToDelete] = createSignal() @@ -45,7 +47,11 @@ export function DialogSessionList() { value: x.id, category, footer: Locale.time(x.time.updated), - gutter: isWorking ? : undefined, + gutter: isWorking ? ( + [⋯]}> + + + ) : undefined, } }) .slice(0, 150) 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 9494b81cb10..f819746d53c 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -29,6 +29,7 @@ import { useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderConnect } from "../dialog-provider" import { DialogAlert } from "../../ui/dialog-alert" import { useToast } from "../../ui/toast" +import { useKV } from "../../context/kv" export type PromptProps = { sessionID?: string @@ -124,6 +125,7 @@ export function Prompt(props: PromptProps) { const command = useCommandDialog() const renderer = useRenderer() const { theme, syntax } = useTheme() + const kv = useKV() function promptModelWarning() { toast.show({ @@ -996,8 +998,11 @@ export function Prompt(props: PromptProps) { justifyContent={status().type === "retry" ? "space-between" : "flex-start"} > - {/* @ts-ignore // SpinnerOptions doesn't support marginLeft */} - + + [⋯]}> + + + {(() => { const retry = createMemo(() => { 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 818b96da43b..177c43a463a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -128,6 +128,7 @@ export function Session() { const [showScrollbar, setShowScrollbar] = createSignal(kv.get("scrollbar_visible", false)) const [userMessageMarkdown, setUserMessageMarkdown] = createSignal(kv.get("user_message_markdown", true)) const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word") + const [animationsEnabled, setAnimationsEnabled] = createSignal(kv.get("animations_enabled", true)) const wide = createMemo(() => dimensions().width > 120) const sidebarVisible = createMemo(() => { @@ -584,6 +585,19 @@ export function Session() { dialog.clear() }, }, + { + title: animationsEnabled() ? "Disable animations" : "Enable animations", + value: "session.toggle.animations", + category: "Session", + onSelect: (dialog) => { + setAnimationsEnabled((prev) => { + const next = !prev + kv.set("animations_enabled", next) + return next + }) + dialog.clear() + }, + }, { title: "Page up", value: "session.page.up", From bfb9787361af01cf2eb8c6dbb2dd7dd08e91496f Mon Sep 17 00:00:00 2001 From: Christopher Ochsenreither Date: Fri, 26 Dec 2025 21:57:59 -0700 Subject: [PATCH 076/164] fix: compact command after revert now properly cleans up revert state (#6235) --- packages/opencode/src/server/server.ts | 2 + .../test/session/revert-compact.test.ts | 285 ++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 packages/opencode/test/session/revert-compact.test.ts diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 65393e12897..b15fb6196ca 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1084,6 +1084,8 @@ export namespace Server { async (c) => { const sessionID = c.req.valid("param").sessionID const body = c.req.valid("json") + const session = await Session.get(sessionID) + await SessionRevert.cleanup(session) const msgs = await Session.messages({ sessionID }) let currentAgent = await Agent.defaultAgent() for (let i = msgs.length - 1; i >= 0; i--) { diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts new file mode 100644 index 00000000000..de2b14573f4 --- /dev/null +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -0,0 +1,285 @@ +import { describe, expect, test, beforeEach, afterEach } from "bun:test" +import path from "path" +import { Session } from "../../src/session" +import { SessionRevert } from "../../src/session/revert" +import { SessionCompaction } from "../../src/session/compaction" +import { MessageV2 } from "../../src/session/message-v2" +import { Log } from "../../src/util/log" +import { Instance } from "../../src/project/instance" +import { Identifier } from "../../src/id/id" +import { tmpdir } from "../fixture/fixture" + +const projectRoot = path.join(__dirname, "../..") +Log.init({ print: false }) + +describe("revert + compact workflow", () => { + test("should properly handle compact command after revert", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + // Create a session + const session = await Session.create({}) + const sessionID = session.id + + // Create a user message + const userMsg1 = await Session.updateMessage({ + id: Identifier.ascending("message"), + role: "user", + sessionID, + agent: "default", + model: { + providerID: "openai", + modelID: "gpt-4", + }, + time: { + created: Date.now(), + }, + }) + + // Add a text part to the user message + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: userMsg1.id, + sessionID, + type: "text", + text: "Hello, please help me", + }) + + // Create an assistant response message + const assistantMsg1: MessageV2.Assistant = { + id: Identifier.ascending("message"), + role: "assistant", + sessionID, + mode: "default", + agent: "default", + path: { + cwd: tmp.path, + root: tmp.path, + }, + cost: 0, + tokens: { + output: 0, + input: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + modelID: "gpt-4", + providerID: "openai", + parentID: userMsg1.id, + time: { + created: Date.now(), + }, + finish: "end_turn", + } + await Session.updateMessage(assistantMsg1) + + // Add a text part to the assistant message + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: assistantMsg1.id, + sessionID, + type: "text", + text: "Sure, I'll help you!", + }) + + // Create another user message + const userMsg2 = await Session.updateMessage({ + id: Identifier.ascending("message"), + role: "user", + sessionID, + agent: "default", + model: { + providerID: "openai", + modelID: "gpt-4", + }, + time: { + created: Date.now(), + }, + }) + + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: userMsg2.id, + sessionID, + type: "text", + text: "What's the capital of France?", + }) + + // Create another assistant response + const assistantMsg2: MessageV2.Assistant = { + id: Identifier.ascending("message"), + role: "assistant", + sessionID, + mode: "default", + agent: "default", + path: { + cwd: tmp.path, + root: tmp.path, + }, + cost: 0, + tokens: { + output: 0, + input: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + modelID: "gpt-4", + providerID: "openai", + parentID: userMsg2.id, + time: { + created: Date.now(), + }, + finish: "end_turn", + } + await Session.updateMessage(assistantMsg2) + + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: assistantMsg2.id, + sessionID, + type: "text", + text: "The capital of France is Paris.", + }) + + // Verify messages before revert + let messages = await Session.messages({ sessionID }) + expect(messages.length).toBe(4) // 2 user + 2 assistant messages + const messageIds = messages.map((m) => m.info.id) + expect(messageIds).toContain(userMsg1.id) + expect(messageIds).toContain(userMsg2.id) + expect(messageIds).toContain(assistantMsg1.id) + expect(messageIds).toContain(assistantMsg2.id) + + // Revert the last user message (userMsg2) + await SessionRevert.revert({ + sessionID, + messageID: userMsg2.id, + }) + + // Check that revert state is set + let sessionInfo = await Session.get(sessionID) + expect(sessionInfo.revert).toBeDefined() + const revertMessageID = sessionInfo.revert?.messageID + expect(revertMessageID).toBeDefined() + + // Messages should still be in the list (not removed yet, just marked for revert) + messages = await Session.messages({ sessionID }) + expect(messages.length).toBe(4) + + // Now clean up the revert state (this is what the compact endpoint should do) + await SessionRevert.cleanup(sessionInfo) + + // After cleanup, the reverted messages (those after the revert point) should be removed + messages = await Session.messages({ sessionID }) + const remainingIds = messages.map((m) => m.info.id) + // The revert point is somewhere in the message chain, so we should have fewer messages + expect(messages.length).toBeLessThan(4) + // userMsg2 and assistantMsg2 should be removed (they come after the revert point) + expect(remainingIds).not.toContain(userMsg2.id) + expect(remainingIds).not.toContain(assistantMsg2.id) + + // Revert state should be cleared + sessionInfo = await Session.get(sessionID) + expect(sessionInfo.revert).toBeUndefined() + + // Clean up + await Session.remove(sessionID) + }, + }) + }) + + test("should properly clean up revert state before creating compaction message", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + // Create a session + const session = await Session.create({}) + const sessionID = session.id + + // Create initial messages + const userMsg = await Session.updateMessage({ + id: Identifier.ascending("message"), + role: "user", + sessionID, + agent: "default", + model: { + providerID: "openai", + modelID: "gpt-4", + }, + time: { + created: Date.now(), + }, + }) + + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: userMsg.id, + sessionID, + type: "text", + text: "Hello", + }) + + const assistantMsg: MessageV2.Assistant = { + id: Identifier.ascending("message"), + role: "assistant", + sessionID, + mode: "default", + agent: "default", + path: { + cwd: tmp.path, + root: tmp.path, + }, + cost: 0, + tokens: { + output: 0, + input: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + modelID: "gpt-4", + providerID: "openai", + parentID: userMsg.id, + time: { + created: Date.now(), + }, + finish: "end_turn", + } + await Session.updateMessage(assistantMsg) + + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: assistantMsg.id, + sessionID, + type: "text", + text: "Hi there!", + }) + + // Revert the user message + await SessionRevert.revert({ + sessionID, + messageID: userMsg.id, + }) + + // Check that revert state is set + let sessionInfo = await Session.get(sessionID) + expect(sessionInfo.revert).toBeDefined() + + // Simulate what the compact endpoint does: cleanup revert before creating compaction + await SessionRevert.cleanup(sessionInfo) + + // Verify revert state is cleared + sessionInfo = await Session.get(sessionID) + expect(sessionInfo.revert).toBeUndefined() + + // Verify messages are properly cleaned up + const messages = await Session.messages({ sessionID }) + expect(messages.length).toBe(0) // All messages should be reverted + + // Clean up + await Session.remove(sessionID) + }, + }) + }) +}) From 3c02d5d338698d382b02fbbf7eaa07a9b0f1ebc6 Mon Sep 17 00:00:00 2001 From: rari404 <138394996+edlsh@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:20:07 -0500 Subject: [PATCH 077/164] feat: add path traversal protection to File.read and File.list (#5985) --- packages/opencode/src/file/index.ts | 15 +++ .../opencode/test/file/path-traversal.test.ts | 115 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 packages/opencode/test/file/path-traversal.test.ts diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 148ab45cb07..9462ec57369 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -7,6 +7,7 @@ import path from "path" import fs from "fs" import ignore from "ignore" import { Log } from "../util/log" +import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Ripgrep } from "./ripgrep" import fuzzysort from "fuzzysort" @@ -235,6 +236,13 @@ export namespace File { using _ = log.time("read", { file }) const project = Instance.project const full = path.join(Instance.directory, file) + + // TODO: Filesystem.contains is lexical only - symlinks inside the project can escape. + // TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization. + if (!Filesystem.contains(Instance.directory, full)) { + throw new Error(`Access denied: path escapes project directory`) + } + const bunFile = Bun.file(full) if (!(await bunFile.exists())) { @@ -288,6 +296,13 @@ export namespace File { ignored = ig.ignores.bind(ig) } const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory + + // TODO: Filesystem.contains is lexical only - symlinks inside the project can escape. + // TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization. + if (!Filesystem.contains(Instance.directory, resolved)) { + throw new Error(`Access denied: path escapes project directory`) + } + const nodes: Node[] = [] for (const entry of await fs.promises .readdir(resolved, { diff --git a/packages/opencode/test/file/path-traversal.test.ts b/packages/opencode/test/file/path-traversal.test.ts new file mode 100644 index 00000000000..c20c76a2e7f --- /dev/null +++ b/packages/opencode/test/file/path-traversal.test.ts @@ -0,0 +1,115 @@ +import { test, expect, describe } from "bun:test" +import path from "path" +import { Filesystem } from "../../src/util/filesystem" +import { File } from "../../src/file" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" + +describe("Filesystem.contains", () => { + test("allows paths within project", () => { + expect(Filesystem.contains("/project", "/project/src")).toBe(true) + expect(Filesystem.contains("/project", "/project/src/file.ts")).toBe(true) + expect(Filesystem.contains("/project", "/project")).toBe(true) + }) + + test("blocks ../ traversal", () => { + expect(Filesystem.contains("/project", "/project/../etc")).toBe(false) + expect(Filesystem.contains("/project", "/project/src/../../etc")).toBe(false) + expect(Filesystem.contains("/project", "/etc/passwd")).toBe(false) + }) + + test("blocks absolute paths outside project", () => { + expect(Filesystem.contains("/project", "/etc/passwd")).toBe(false) + expect(Filesystem.contains("/project", "/tmp/file")).toBe(false) + expect(Filesystem.contains("/home/user/project", "/home/user/other")).toBe(false) + }) + + test("handles prefix collision edge cases", () => { + expect(Filesystem.contains("/project", "/project-other/file")).toBe(false) + expect(Filesystem.contains("/project", "/projectfile")).toBe(false) + }) +}) + +/* + * Integration tests for File.read() and File.list() path traversal protection. + * + * These tests verify the HTTP API code path is protected. The HTTP endpoints + * in server.ts (GET /file/content, GET /file) call File.read()/File.list() + * directly - they do NOT go through ReadTool or the agent permission layer. + * + * This is a SEPARATE code path from ReadTool, which has its own checks. + */ +describe("File.read path traversal protection", () => { + test("rejects ../ traversal attempting to read /etc/passwd", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "allowed.txt"), "allowed content") + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await expect(File.read("../../../etc/passwd")).rejects.toThrow("Access denied: path escapes project directory") + }, + }) + }) + + test("rejects deeply nested traversal", async () => { + await using tmp = await tmpdir() + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await expect(File.read("src/nested/../../../../../../../etc/passwd")).rejects.toThrow( + "Access denied: path escapes project directory", + ) + }, + }) + }) + + test("allows valid paths within project", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "valid.txt"), "valid content") + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await File.read("valid.txt") + expect(result.content).toBe("valid content") + }, + }) + }) +}) + +describe("File.list path traversal protection", () => { + test("rejects ../ traversal attempting to list /etc", async () => { + await using tmp = await tmpdir() + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await expect(File.list("../../../etc")).rejects.toThrow("Access denied: path escapes project directory") + }, + }) + }) + + test("allows valid subdirectory listing", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "subdir", "file.txt"), "content") + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await File.list("subdir") + expect(Array.isArray(result)).toBe(true) + }, + }) + }) +}) From e6b9988fa4913d25fe2f6bf5b0f3b7218f8c8e7e Mon Sep 17 00:00:00 2001 From: Didier Durand <2927957+didier-durand@users.noreply.github.com> Date: Sat, 27 Dec 2025 07:46:06 +0100 Subject: [PATCH 078/164] doc: fix typos in various files (#6238) --- .github/workflows/review.yml | 2 +- packages/opencode/src/agent/prompt/title.txt | 2 +- packages/opencode/src/session/prompt/copilot-gpt-5.txt | 2 +- packages/opencode/src/session/prompt/qwen.txt | 2 +- packages/opencode/src/tool/bash.txt | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index c0e3a5deb15..44bfeb33661 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -64,7 +64,7 @@ jobs: Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage. - When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts) + When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simplest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts) Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block. If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors. diff --git a/packages/opencode/src/agent/prompt/title.txt b/packages/opencode/src/agent/prompt/title.txt index f67aaa95bac..7e927b797ce 100644 --- a/packages/opencode/src/agent/prompt/title.txt +++ b/packages/opencode/src/agent/prompt/title.txt @@ -22,7 +22,7 @@ Your output must be: - The title should NEVER include "summarizing" or "generating" when generating a title - DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT - Always output something meaningful, even if the input is minimal. -- If the user message is short or conversational (e.g. "hello", "lol", "whats up", "hey"): +- If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"): → create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.) diff --git a/packages/opencode/src/session/prompt/copilot-gpt-5.txt b/packages/opencode/src/session/prompt/copilot-gpt-5.txt index 81594301944..f8e3e6b8c98 100644 --- a/packages/opencode/src/session/prompt/copilot-gpt-5.txt +++ b/packages/opencode/src/session/prompt/copilot-gpt-5.txt @@ -129,7 +129,7 @@ Tools can be disabled by the user. You may see tools used previously in the conv Use proper Markdown formatting in your answers. When referring to a filename or symbol in the user's workspace, wrap it in backticks. When sharing setup or run steps for the user to execute, render commands in fenced code blocks with an appropriate language tag (`bash`, `sh`, `powershell`, `python`, etc.). Keep one command per line; avoid prose-only representations of commands. -Keep responses conversational and fun—use a brief, friendly preamble that acknowledges the goal and states what you're about to do next. Avoid literal scaffold labels like "Plan:", "Task receipt:", or "Actions:"; instead, use short paragraphs and, when helpful, concise bullet lists. Do not start with filler acknowledgements (e.g., "Sounds good", "Great", "Okay, I will…"). For multi-step tasks, maintain a lightweight checklist implicitly and weave progress into your narration. +Keep responses conversational and fun—use a brief, friendly preamble that acknowledges the goal and states what you're about to do next. Avoid literal scaffold labels like "Plan:", "Task receipt:", or "Actions:"; instead, use short paragraphs and, when helpful, concise bullet lists. Do not start with filler acknowledgements (e.g., "Sounds good", "Great", "Okay, I will…"). For multistep tasks, maintain a lightweight checklist implicitly and weave progress into your narration. For section headers in your response, use level-2 Markdown headings (`##`) for top-level sections and level-3 (`###`) for subsections. Choose titles dynamically to match the task and content. Do not hard-code fixed section names; create only the sections that make sense and only when they have non-empty content. Keep headings short and descriptive (e.g., "actions taken", "files changed", "how to run", "performance", "notes"), and order them naturally (actions > artifacts > how to run > performance > notes) when applicable. You may add a tasteful emoji to a heading when it improves scannability; keep it minimal and professional. Headings must start at the beginning of the line with `## ` or `### `, have a blank line before and after, and must not be inside lists, block quotes, or code fences. When listing files created/edited, include a one-line purpose for each file when helpful. In performance sections, base any metrics on actual runs from this session; note the hardware/OS context and mark estimates clearly—never fabricate numbers. In "Try it" sections, keep commands copyable; comments starting with `#` are okay, but put each command on its own line. If platform-specific acceleration applies, include an optional speed-up fenced block with commands. Close with a concise completion summary describing what changed and how it was verified (build/tests/linters), plus any follow-ups. diff --git a/packages/opencode/src/session/prompt/qwen.txt b/packages/opencode/src/session/prompt/qwen.txt index a34fdb01a05..d88d9d063ba 100644 --- a/packages/opencode/src/session/prompt/qwen.txt +++ b/packages/opencode/src/session/prompt/qwen.txt @@ -84,7 +84,7 @@ The user will primarily request you perform software engineering tasks. This inc - Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially. - Implement the solution using all tools available to you - Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach. -- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time. +- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (e.g. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time. NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. - Tool results and user messages may include tags. tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result. diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index 18ee14012b2..c31263c04eb 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -22,7 +22,7 @@ Before executing the command, please follow these steps: Usage notes: - The command argument is required. - - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes). + - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will time out after 120000ms (2 minutes). - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - If the output exceeds 30000 characters, output will be truncated before being returned to you. - You can use the `run_in_background` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the Bash tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter. @@ -70,7 +70,7 @@ Git Safety Protocol: - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style. 2. Analyze all staged changes (both previously staged and newly added) and draft a commit message: - Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.). - - Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files + - Do not commit files that likely contain secrets (.env, credentials.json, etc.). Warn the user if they specifically request to commit those files - Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what" - Ensure it accurately reflects the changes and their purpose 3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands: From 4667d57e3c351a168789edfea0bbf440220adfc5 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 27 Dec 2025 00:50:59 -0600 Subject: [PATCH 079/164] ci: stale issues --- .github/workflows/stale-issues.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/stale-issues.yml diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 00000000000..d41e8e60c50 --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,29 @@ +name: "Auto-close stale issues" + +on: + schedule: + - cron: "30 1 * * *" # Daily at 1:30 AM + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/stale@v10 + with: + days-before-stale: 90 + days-before-close: 7 + stale-issue-label: "stale" + close-issue-message: | + [automated] Closing due to 90+ days of inactivity. + + Feel free to reopen if you still need this! + stale-issue-message: | + [automated] This issue has had no activity for 90 days. + + It will be closed in 7 days if there's no new activity. + remove-stale-when-updated: true + exempt-issue-labels: "pinned,security,feature-request,on-hold" + start-date: "2025-12-27" From 685f3ea324cf5d0401f7b0895a78560149bf8a4b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 27 Dec 2025 12:04:27 +0000 Subject: [PATCH 080/164] ignore: update download stats 2025-12-27 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 77a9d296cfc..41c93525478 100644 --- a/STATS.md +++ b/STATS.md @@ -182,3 +182,4 @@ | 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) | | 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) | | 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) | +| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) | From c523ca412747d66e0236865a4fa2481f7d50f64e Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sat, 27 Dec 2025 05:19:03 -0600 Subject: [PATCH 081/164] wip(desktop): handle more errors --- packages/app/src/components/header.tsx | 4 ++ packages/app/src/components/prompt-input.tsx | 60 ++++++++++++-------- packages/app/src/components/terminal.tsx | 32 ++++++----- packages/app/src/context/terminal.tsx | 60 +++++++++++++------- packages/desktop/vite.config.ts | 3 + 5 files changed, 100 insertions(+), 59 deletions(-) diff --git a/packages/app/src/components/header.tsx b/packages/app/src/components/header.tsx index 3eae0e05d41..74c49f07ac6 100644 --- a/packages/app/src/components/header.tsx +++ b/packages/app/src/components/header.tsx @@ -188,6 +188,10 @@ export function Header(props: { shareURL = await globalSDK.client.session .share({ sessionID: session.id, directory: currentDirectory() }) .then((r) => r.data?.share?.url) + .catch((e) => { + console.error("Failed to share session", e) + return undefined + }) } return shareURL }, diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 2407fe97a9c..f1bb9132a59 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -643,9 +643,11 @@ export const PromptInput: Component = (props) => { } const abort = () => - sdk.client.session.abort({ - sessionID: params.id!, - }) + sdk.client.session + .abort({ + sessionID: params.id!, + }) + .catch(() => {}) const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => { const text = prompt @@ -883,12 +885,16 @@ export const PromptInput: Component = (props) => { const agent = local.agent.current()!.name if (isShellMode) { - sdk.client.session.shell({ - sessionID: existing.id, - agent, - model, - command: text, - }) + sdk.client.session + .shell({ + sessionID: existing.id, + agent, + model, + command: text, + }) + .catch((e) => { + console.error("Failed to send shell command", e) + }) return } @@ -897,13 +903,17 @@ export const PromptInput: Component = (props) => { const commandName = cmdName.slice(1) const customCommand = sync.data.command.find((c) => c.name === commandName) if (customCommand) { - sdk.client.session.command({ - sessionID: existing.id, - command: commandName, - arguments: args.join(" "), - agent, - model: `${model.providerID}/${model.modelID}`, - }) + sdk.client.session + .command({ + sessionID: existing.id, + command: commandName, + arguments: args.join(" "), + agent, + model: `${model.providerID}/${model.modelID}`, + }) + .catch((e) => { + console.error("Failed to send command", e) + }) return } } @@ -929,13 +939,17 @@ export const PromptInput: Component = (props) => { model, }) - sdk.client.session.prompt({ - sessionID: existing.id, - agent, - model, - messageID, - parts: requestParts, - }) + sdk.client.session + .prompt({ + sessionID: existing.id, + agent, + model, + messageID, + parts: requestParts, + }) + .catch((e) => { + console.error("Failed to send prompt", e) + }) } return ( diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index c05ddfbf635..abf03fc0640 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -82,13 +82,15 @@ export const Terminal = (props: TerminalProps) => { window.addEventListener("resize", handleResize) term.onResize(async (size) => { if (ws && ws.readyState === WebSocket.OPEN) { - await sdk.client.pty.update({ - ptyID: local.pty.id, - size: { - cols: size.cols, - rows: size.rows, - }, - }) + await sdk.client.pty + .update({ + ptyID: local.pty.id, + size: { + cols: size.cols, + rows: size.rows, + }, + }) + .catch(() => {}) } }) term.onData((data) => { @@ -106,13 +108,15 @@ export const Terminal = (props: TerminalProps) => { // }) ws.addEventListener("open", () => { console.log("WebSocket connected") - sdk.client.pty.update({ - ptyID: local.pty.id, - size: { - cols: term.cols, - rows: term.rows, - }, - }) + sdk.client.pty + .update({ + ptyID: local.pty.id, + size: { + cols: term.cols, + rows: term.rows, + }, + }) + .catch(() => {}) }) ws.addEventListener("message", (event) => { term.write(event.data) diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 6f7c11dea8c..e9a07077cef 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -36,35 +36,49 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont all: createMemo(() => Object.values(store.all)), active: createMemo(() => store.active), new() { - sdk.client.pty.create({ title: `Terminal ${store.all.length + 1}` }).then((pty) => { - const id = pty.data?.id - if (!id) return - setStore("all", [ - ...store.all, - { - id, - title: pty.data?.title ?? "Terminal", - }, - ]) - setStore("active", id) - }) + sdk.client.pty + .create({ title: `Terminal ${store.all.length + 1}` }) + .then((pty) => { + const id = pty.data?.id + if (!id) return + setStore("all", [ + ...store.all, + { + id, + title: pty.data?.title ?? "Terminal", + }, + ]) + setStore("active", id) + }) + .catch((e) => { + console.error("Failed to create terminal", e) + }) }, update(pty: Partial & { id: string }) { setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x))) - sdk.client.pty.update({ - ptyID: pty.id, - title: pty.title, - size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, - }) + sdk.client.pty + .update({ + ptyID: pty.id, + title: pty.title, + size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, + }) + .catch((e) => { + console.error("Failed to update terminal", e) + }) }, async clone(id: string) { const index = store.all.findIndex((x) => x.id === id) const pty = store.all[index] if (!pty) return - const clone = await sdk.client.pty.create({ - title: pty.title, - }) - if (!clone.data) return + const clone = await sdk.client.pty + .create({ + title: pty.title, + }) + .catch((e) => { + console.error("Failed to clone terminal", e) + return undefined + }) + if (!clone?.data) return setStore("all", index, { ...pty, ...clone.data, @@ -88,7 +102,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont setStore("active", previous?.id) } }) - await sdk.client.pty.remove({ ptyID: id }) + await sdk.client.pty.remove({ ptyID: id }).catch((e) => { + console.error("Failed to close terminal", e) + }) }, move(id: string, to: number) { const index = store.all.findIndex((f) => f.id === id) diff --git a/packages/desktop/vite.config.ts b/packages/desktop/vite.config.ts index 123a2028c91..6d4f62dc2cb 100644 --- a/packages/desktop/vite.config.ts +++ b/packages/desktop/vite.config.ts @@ -10,6 +10,9 @@ export default defineConfig({ // // 1. prevent Vite from obscuring rust errors clearScreen: false, + build: { + sourcemap: true, + }, // 2. tauri expects a fixed port, fail if that port is not available server: { port: 1420, From 21eba5f987482b4e2e75ab1c564815bd7b0613f4 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sat, 27 Dec 2025 05:16:39 -0600 Subject: [PATCH 082/164] feat(desktop): permissions --- packages/app/src/context/global-sync.tsx | 61 ++++++ packages/app/src/context/local.tsx | 25 ++- packages/app/src/pages/directory-layout.tsx | 11 +- packages/app/src/pages/layout.tsx | 50 ++++- packages/opencode/src/permission/index.ts | 11 + packages/opencode/src/server/server.ts | 22 ++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 20 ++ packages/sdk/js/src/v2/gen/types.gen.ts | 18 ++ packages/sdk/openapi.json | 37 ++++ packages/ui/src/components/basic-tool.tsx | 11 +- packages/ui/src/components/message-part.css | 95 ++++++++ packages/ui/src/components/message-part.tsx | 226 ++++++++++++++++---- packages/ui/src/components/session-turn.css | 8 + packages/ui/src/components/session-turn.tsx | 23 ++ packages/ui/src/components/toast.tsx | 10 +- packages/ui/src/context/data.tsx | 14 +- packages/ui/src/context/dialog.tsx | 4 - 17 files changed, 586 insertions(+), 60 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index c51901eb254..50c8a9d1c87 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -15,6 +15,7 @@ import { type McpStatus, type LspStatus, type VcsInfo, + type Permission, createOpencodeClient, } from "@opencode-ai/sdk/v2/client" import { createStore, produce, reconcile } from "solid-js/store" @@ -44,6 +45,9 @@ type State = { todo: { [sessionID: string]: Todo[] } + permission: { + [sessionID: string]: Permission[] + } mcp: { [name: string]: McpStatus } @@ -78,6 +82,7 @@ function createGlobalSync() { }) const children: Record>> = {} + const permissionListeners: Set<(info: { directory: string; permission: Permission }) => void> = new Set() function child(directory: string) { if (!directory) console.error("No directory provided") if (!children[directory]) { @@ -93,6 +98,7 @@ function createGlobalSync() { session_status: {}, session_diff: {}, todo: {}, + permission: {}, mcp: {}, lsp: [], vcs: undefined, @@ -163,6 +169,15 @@ function createGlobalSync() { mcp: () => sdk.mcp.status().then((x) => setStore("mcp", x.data ?? {})), lsp: () => sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])), vcs: () => sdk.vcs.get().then((x) => setStore("vcs", x.data)), + permission: () => + sdk.permission.list().then((x) => { + const grouped: Record = {} + for (const perm of x.data ?? []) { + grouped[perm.sessionID] = grouped[perm.sessionID] ?? [] + grouped[perm.sessionID]!.push(perm) + } + setStore("permission", grouped) + }), } await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e)))) .then(() => setStore("ready", true)) @@ -313,6 +328,46 @@ function createGlobalSync() { setStore("vcs", { branch: event.properties.branch }) break } + case "permission.updated": { + const permissions = store.permission[event.properties.sessionID] + const isNew = !permissions || !permissions.find((p) => p.id === event.properties.id) + if (!permissions) { + setStore("permission", event.properties.sessionID, [event.properties]) + } else { + const result = Binary.search(permissions, event.properties.id, (p) => p.id) + setStore( + "permission", + event.properties.sessionID, + produce((draft) => { + if (result.found) { + draft[result.index] = event.properties + return + } + draft.push(event.properties) + }), + ) + } + if (isNew) { + for (const listener of permissionListeners) { + listener({ directory, permission: event.properties }) + } + } + break + } + case "permission.replied": { + const permissions = store.permission[event.properties.sessionID] + if (!permissions) break + const result = Binary.search(permissions, event.properties.permissionID, (p) => p.id) + if (!result.found) break + setStore( + "permission", + event.properties.sessionID, + produce((draft) => { + draft.splice(result.index, 1) + }), + ) + break + } } }) @@ -384,6 +439,12 @@ function createGlobalSync() { project: { loadSessions, }, + permission: { + onUpdated(listener: (info: { directory: string; permission: Permission }) => void) { + permissionListeners.add(listener) + return () => permissionListeners.delete(listener) + }, + }, } } diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 600a0e4b160..49217b82be8 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -377,17 +377,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } const list = async (path: string) => { - return sdk.client.file.list({ path: path + "/" }).then((x) => { - setStore( - "node", - produce((draft) => { - x.data!.forEach((node) => { - if (node.path in draft) return - draft[node.path] = node - }) - }), - ) - }) + return sdk.client.file + .list({ path: path + "/" }) + .then((x) => { + setStore( + "node", + produce((draft) => { + x.data!.forEach((node) => { + if (node.path in draft) return + draft[node.path] = node + }) + }), + ) + }) + .catch(() => {}) } const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!) diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index c909a373d56..04f90bdcbf6 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,6 +1,6 @@ import { createMemo, Show, type ParentProps } from "solid-js" import { useParams } from "@solidjs/router" -import { SDKProvider } from "@/context/sdk" +import { SDKProvider, useSDK } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" import { LocalProvider } from "@/context/local" import { base64Decode } from "@opencode-ai/util/encode" @@ -18,8 +18,15 @@ export default function Layout(props: ParentProps) { {iife(() => { const sync = useSync() + const sdk = useSDK() return ( - + { + sdk.client.permission.respond(input) + }} + > {props.children} ) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 5efba6d994b..538a3b8409d 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -117,6 +117,39 @@ export default function Layout(props: ParentProps) { } }) + onMount(() => { + const unsub = globalSync.permission.onUpdated(({ directory, permission }) => { + const currentDir = params.dir ? base64Decode(params.dir) : undefined + const currentSession = params.id + if (directory === currentDir && permission.sessionID === currentSession) return + const [store] = globalSync.child(directory) + const session = store.session.find((s) => s.id === permission.sessionID) + if (directory === currentDir && session?.parentID === currentSession) return + const sessionTitle = session?.title ?? "New session" + const projectName = getFilename(directory) + showToast({ + persistent: true, + icon: "checklist", + title: "Permission required", + description: `${sessionTitle} in ${projectName} needs permission`, + actions: [ + { + label: "Go to session", + onClick: () => { + navigate(`/${base64Encode(directory)}/session/${permission.sessionID}`) + }, + dismissAfter: true, + }, + { + label: "Dismiss", + onClick: "dismiss", + }, + ], + }) + }) + onCleanup(unsub) + }) + function sortSessions(a: Session, b: Session) { const now = Date.now() const oneMinuteAgo = now - 60 * 1000 @@ -454,8 +487,20 @@ export default function Layout(props: ParentProps) { const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated)) const notifications = createMemo(() => notification.session.unseen(props.session.id)) const hasError = createMemo(() => notifications().some((n) => n.type === "error")) + const hasPermissions = createMemo(() => { + const store = globalSync.child(props.project.worktree)[0] + const permissions = store.permission?.[props.session.id] ?? [] + if (permissions.length > 0) return true + const childSessions = store.session.filter((s) => s.parentID === props.session.id) + for (const child of childSessions) { + const childPermissions = store.permission?.[child.id] ?? [] + if (childPermissions.length > 0) return true + } + return false + }) const isWorking = createMemo(() => { if (props.session.id === params.id) return false + if (hasPermissions()) return false const status = globalSync.child(props.project.worktree)[0].session_status[props.session.id] return status?.type === "busy" || status?.type === "retry" }) @@ -486,6 +531,9 @@ export default function Layout(props: ParentProps) { + +
+
@@ -587,7 +635,7 @@ export default function Layout(props: ParentProps) { closeProject(props.project.worktree)}> - Close Project + Close project diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index f3a8852ae4f..cbfeb6a9b9a 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -86,6 +86,17 @@ export namespace Permission { return state().pending } + export function list() { + const { pending } = state() + const result: Info[] = [] + for (const items of Object.values(pending)) { + for (const item of Object.values(items)) { + result.push(item.info) + } + } + return result.sort((a, b) => a.id.localeCompare(b.id)) + } + export async function ask(input: { type: Info["type"] title: Info["title"] diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index b15fb6196ca..e25d9ded473 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1532,6 +1532,28 @@ export namespace Server { return c.json(true) }, ) + .get( + "/permission", + describeRoute({ + summary: "List pending permissions", + description: "Get all pending permission requests across all sessions.", + operationId: "permission.list", + responses: { + 200: { + description: "List of pending permissions", + content: { + "application/json": { + schema: resolver(Permission.Info.array()), + }, + }, + }, + }, + }), + async (c) => { + const permissions = Permission.list() + return c.json(permissions) + }, + ) .get( "/command", describeRoute({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 97bc92b8669..797896ace9a 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -54,6 +54,7 @@ import type { PartUpdateErrors, PartUpdateResponses, PathGetResponses, + PermissionListResponses, PermissionRespondErrors, PermissionRespondResponses, ProjectCurrentResponses, @@ -1618,6 +1619,25 @@ export class Permission extends HeyApiClient { }, }) } + + /** + * List pending permissions + * + * Get all pending permission requests across all sessions. + */ + public list( + parameters?: { + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }]) + return (options?.client ?? this.client).get({ + url: "/https/github.com/permission", + ...options, + ...params, + }) + } } export class Command extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 90b2154e18a..5c4cc69423d 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3356,6 +3356,24 @@ export type PermissionRespondResponses = { export type PermissionRespondResponse = PermissionRespondResponses[keyof PermissionRespondResponses] +export type PermissionListData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/https/github.com/permission" +} + +export type PermissionListResponses = { + /** + * List of pending permissions + */ + 200: Array +} + +export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] + export type CommandListData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index c3658a90c50..3903566b91e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -2879,6 +2879,43 @@ ] } }, + "/permission": { + "get": { + "operationId": "permission.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List pending permissions", + "description": "Get all pending permission requests across all sessions.", + "responses": { + "200": { + "description": "List of pending permissions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Permission" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.list({\n ...\n})" + } + ] + } + }, "/command": { "get": { "operationId": "command.list", diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index 28320eeb3e9..67720955dcb 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -1,4 +1,4 @@ -import { For, Match, Show, Switch, type JSX } from "solid-js" +import { createEffect, createSignal, For, Match, Show, Switch, type JSX } from "solid-js" import { Collapsible } from "./collapsible" import { Icon, IconProps } from "./icon" @@ -24,11 +24,18 @@ export interface BasicToolProps { children?: JSX.Element hideDetails?: boolean defaultOpen?: boolean + forceOpen?: boolean } export function BasicTool(props: BasicToolProps) { + const [open, setOpen] = createSignal(props.defaultOpen ?? false) + + createEffect(() => { + if (props.forceOpen) setOpen(true) + }) + return ( - +
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 6daf1a8b513..a8a9e6a31ed 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -361,3 +361,98 @@ overflow: hidden; } } + +[data-component="tool-part-wrapper"] { + width: 100%; + + &[data-permission="true"] { + position: sticky; + top: var(--sticky-header-height, 80px); + bottom: 0px; + z-index: 10; + border-radius: 6px; + border: none; + box-shadow: var(--shadow-xs-border-base); + background-color: var(--surface-raised-base); + overflow: visible; + + &::before { + content: ""; + position: absolute; + inset: -1.5px; + border-radius: 7.5px; + border: 1.5px solid transparent; + background: + linear-gradient(var(--background-base) 0 0) padding-box, + conic-gradient( + from var(--border-angle), + transparent 0deg, + transparent 270deg, + var(--border-warning-strong, var(--border-warning-selected)) 300deg, + var(--border-warning-base) 360deg + ) + border-box; + animation: chase-border 1.5s linear infinite; + pointer-events: none; + z-index: -1; + } + + & > *:first-child { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + overflow: hidden; + } + + & > *:last-child { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + overflow: hidden; + } + + [data-component="collapsible"] { + border: none; + } + + [data-component="card"] { + border: none; + } + } +} + +@property --border-angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} + +@keyframes chase-border { + from { + --border-angle: 0deg; + } + to { + --border-angle: 360deg; + } +} + +[data-component="permission-prompt"] { + display: flex; + flex-direction: column; + padding: 8px 12px; + background-color: var(--surface-raised-strong); + border-radius: 0 0 6px 6px; + + [data-slot="permission-message"] { + display: none; + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + } + + [data-slot="permission-actions"] { + display: flex; + align-items: center; + gap: 8px; + justify-content: flex-end; + } +} diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 1424041e8c0..0a1518b796e 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -1,4 +1,4 @@ -import { Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js" +import { Component, createEffect, createMemo, createSignal, For, Match, Show, Switch, type JSX } from "solid-js" import { Dynamic } from "solid-js/web" import { AssistantMessage, @@ -16,6 +16,7 @@ import { useDiffComponent } from "../context/diff" import { useCodeComponent } from "../context/code" import { BasicTool } from "./basic-tool" import { GenericTool } from "./basic-tool" +import { Button } from "./button" import { Card } from "./card" import { Icon } from "./icon" import { Checkbox } from "./checkbox" @@ -188,11 +189,6 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo { } } -function getToolPartInfo(part: ToolPart): ToolInfo { - const input = part.state.input || {} - return getToolInfo(part.tool, input) -} - export function registerPartComponent(type: string, component: PartComponent) { PART_MAPPING[type] = component } @@ -334,6 +330,7 @@ export interface ToolProps { status?: string hideDetails?: boolean defaultOpen?: boolean + forceOpen?: boolean } export type ToolComponent = Component @@ -361,11 +358,35 @@ export const ToolRegistry = { } PART_MAPPING["tool"] = function ToolPartDisplay(props) { + const data = useData() const part = props.part as ToolPart + + const permission = createMemo(() => { + const sessionID = props.message.sessionID + const permissions = data.store.permission?.[sessionID] ?? [] + return permissions.find((p) => p.callID === part.callID) + }) + + const [forceOpen, setForceOpen] = createSignal(false) + createEffect(() => { + if (permission()) setForceOpen(true) + }) + + const respond = (response: "once" | "always" | "reject") => { + const perm = permission() + if (!perm || !data.respondToPermission) return + data.respondToPermission({ + sessionID: perm.sessionID, + permissionID: perm.id, + response, + }) + } + const component = createMemo(() => { const render = ToolRegistry.render(part.tool) ?? GenericTool - const metadata = part.state.status === "pending" ? {} : (part.state.metadata ?? {}) - const input = part.state.status === "completed" ? part.state.input : {} + // @ts-expect-error + const metadata = part.state?.metadata ?? {} + const input = part.state?.input ?? {} return ( @@ -399,9 +420,11 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { input={input} tool={part.tool} metadata={metadata} - output={part.state.status === "completed" ? part.state.output : undefined} + // @ts-expect-error + output={part.state.output} status={part.state.status} hideDetails={props.hideDetails} + forceOpen={forceOpen()} defaultOpen={props.defaultOpen} /> @@ -409,7 +432,29 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { ) }) - return {component()} + return ( +
+ {component()} + + {(perm) => ( +
+
{perm().title}
+
+ + + +
+
+ )} +
+
+ ) } PART_MAPPING["text"] = function TextPartDisplay(props) { @@ -564,6 +609,7 @@ ToolRegistry.register({ ToolRegistry.register({ name: "task", render(props) { + const data = useData() const summary = () => (props.metadata.summary ?? []) as { id: string; tool: string; state: { status: string; title?: string } }[] @@ -571,35 +617,141 @@ ToolRegistry.register({ working: () => true, }) + const childSessionId = () => props.metadata.sessionId as string | undefined + + const childPermission = createMemo(() => { + const sessionId = childSessionId() + if (!sessionId) return undefined + const permissions = data.store.permission?.[sessionId] ?? [] + return permissions.toSorted((a, b) => a.id.localeCompare(b.id))[0] + }) + + const childToolPart = createMemo(() => { + const perm = childPermission() + if (!perm) return undefined + const sessionId = childSessionId() + if (!sessionId) return undefined + // Find the tool part that matches the permission's callID + const messages = data.store.message[sessionId] ?? [] + for (const msg of messages) { + const parts = data.store.part[msg.id] ?? [] + for (const part of parts) { + if (part.type === "tool" && (part as ToolPart).callID === perm.callID) { + return { part: part as ToolPart, message: msg } + } + } + } + return undefined + }) + + const respond = (response: "once" | "always" | "reject") => { + const perm = childPermission() + if (!perm || !data.respondToPermission) return + data.respondToPermission({ + sessionID: perm.sessionID, + permissionID: perm.id, + response, + }) + } + + const renderChildToolPart = () => { + const toolData = childToolPart() + if (!toolData) return null + const { part } = toolData + const render = ToolRegistry.render(part.tool) ?? GenericTool + // @ts-expect-error + const metadata = part.state?.metadata ?? {} + const input = part.state?.input ?? {} + return ( + + ) + } + return ( - -
-
- - {(item) => { - const info = getToolInfo(item.tool) - return ( -
- - {info.title} - - {item.state.title} - +
+ + + {(perm) => ( + <> + + } + > + {renderChildToolPart()} + +
+
{perm().title}
+
+ + +
- ) +
+ + )} +
+ + -
-
- + > +
+
+ + {(item) => { + const info = getToolInfo(item.tool) + return ( +
+ + {info.title} + + {item.state.title} + +
+ ) + }} +
+
+
+ + + +
) }, }) @@ -618,7 +770,7 @@ ToolRegistry.register({ >
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 63c77e5ac78..1748feab963 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -357,4 +357,12 @@ margin-top: 0; } } + + [data-slot="session-turn-permission-parts"] { + width: 100%; + min-width: 0; + display: flex; + flex-direction: column; + gap: 12px; + } } diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index a0368b0d492..ce4845a71c0 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -151,6 +151,22 @@ export function SessionTurn( return false }) + const permissionParts = createMemo(() => { + const result: { part: ToolPart; message: AssistantMessage }[] = [] + const permissions = data.store.permission?.[props.sessionID] ?? [] + if (!permissions.length) return result + + for (const m of assistantMessages()) { + const msgParts = data.store.part[m.id] ?? [] + for (const p of msgParts) { + if (p?.type === "tool" && permissions.some((perm) => perm.callID === (p as ToolPart).callID)) { + result.push({ part: p as ToolPart, message: m }) + } + } + } + return result + }) + const shellModePart = createMemo(() => { const p = parts() if (!p.every((part) => part?.type === "text" && part?.synthetic)) return @@ -469,6 +485,13 @@ export function SessionTurn(
+ 0}> +
+ + {({ part, message }) => } + +
+
{/* Summary */}
diff --git a/packages/ui/src/components/toast.tsx b/packages/ui/src/components/toast.tsx index c1a29cd04dc..7e90e9f2f32 100644 --- a/packages/ui/src/components/toast.tsx +++ b/packages/ui/src/components/toast.tsx @@ -92,6 +92,7 @@ export type ToastVariant = "default" | "success" | "error" | "loading" export interface ToastAction { label: string onClick: "dismiss" | (() => void) + dismissAfter?: boolean } export interface ToastOptions { @@ -128,7 +129,14 @@ export function showToast(options: ToastOptions | string) { {opts.actions!.map((action) => ( diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index f532534188c..3292ba579f0 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -1,4 +1,4 @@ -import type { Message, Session, Part, FileDiff, SessionStatus } from "@opencode-ai/sdk/v2" +import type { Message, Session, Part, FileDiff, SessionStatus, Permission } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" @@ -13,6 +13,9 @@ type Data = { session_diff_preload?: { [sessionID: string]: PreloadMultiFileDiffResult[] } + permission?: { + [sessionID: string]: Permission[] + } message: { [sessionID: string]: Message[] } @@ -21,9 +24,15 @@ type Data = { } } +export type PermissionRespondFn = (input: { + sessionID: string + permissionID: string + response: "once" | "always" | "reject" +}) => void + export const { use: useData, provider: DataProvider } = createSimpleContext({ name: "Data", - init: (props: { data: Data; directory: string }) => { + init: (props: { data: Data; directory: string; onPermissionRespond?: PermissionRespondFn }) => { return { get store() { return props.data @@ -31,6 +40,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ get directory() { return props.directory }, + respondToPermission: props.onPermissionRespond, } }, }) diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx index 56be9ee4789..8e1a6aad8e5 100644 --- a/packages/ui/src/context/dialog.tsx +++ b/packages/ui/src/context/dialog.tsx @@ -33,10 +33,6 @@ function init() { }, close() { active()?.onClose?.() - if (!active()?.onClose) { - const promptInput = document.querySelector("[data-component=prompt-input]") as HTMLElement - promptInput?.focus() - } setActive(undefined) }, show(element: DialogElement, owner: Owner, onClose?: () => void) { From 7aecb43e846eea4c383a29bf6a224db909ac474a Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 27 Dec 2025 20:51:09 +0000 Subject: [PATCH 083/164] release: v1.0.204 --- 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 | 16 +++++++------- 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, 46 insertions(+), 46 deletions(-) diff --git a/bun.lock b/bun.lock index 593033e34f4..c2827775623 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -98,7 +98,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -125,7 +125,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -149,7 +149,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -173,7 +173,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@opencode-ai/app": "workspace:*", "@solid-primitives/storage": "catalog:", @@ -200,7 +200,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -229,7 +229,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -245,7 +245,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.203", + "version": "1.0.204", "bin": { "opencode": "./bin/opencode", }, @@ -347,7 +347,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -367,7 +367,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.203", + "version": "1.0.204", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -378,7 +378,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -391,7 +391,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -426,7 +426,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.203", + "version": "1.0.204", "dependencies": { "zod": "catalog:", }, @@ -437,7 +437,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.203", + "version": "1.0.204", "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 4fc9678e70e..7937753aea1 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.0.203", + "version": "1.0.204", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index a12dc87f24d..4474366b880 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.203", + "version": "1.0.204", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 4f6d2717fb7..f74d28b2e32 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.0.203", + "version": "1.0.204", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 572a86ddd5e..57b004fb709 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.203", + "version": "1.0.204", "$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 1b2869dd9ec..f2c7c7302f9 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.203", + "version": "1.0.204", "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 4bdb5ce3886..23aa11091fb 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.0.203", + "version": "1.0.204", "type": "module", "scripts": { "typecheck": "tsgo -b", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index a89e5df7ef7..e4a7f45beae 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.0.203", + "version": "1.0.204", "private": true, "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index e21818e4629..3e01e835339 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.0.203" +version = "1.0.204" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-linux-arm64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-linux-x64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.203/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 160e78b35fd..44c6ef110ef 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.203", + "version": "1.0.204", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 55656660e06..67c8443cbb7 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.203", + "version": "1.0.204", "name": "opencode", "type": "module", "private": true, @@ -52,22 +52,22 @@ "@ai-sdk/amazon-bedrock": "3.0.57", "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/azure": "2.0.73", + "@ai-sdk/cerebras": "1.0.33", + "@ai-sdk/cohere": "2.0.21", + "@ai-sdk/deepinfra": "1.0.30", + "@ai-sdk/gateway": "2.0.23", "@ai-sdk/google": "2.0.44", "@ai-sdk/google-vertex": "3.0.81", + "@ai-sdk/groq": "2.0.33", "@ai-sdk/mcp": "0.0.8", "@ai-sdk/mistral": "2.0.26", "@ai-sdk/openai": "2.0.71", "@ai-sdk/openai-compatible": "1.0.27", + "@ai-sdk/perplexity": "2.0.22", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", - "@ai-sdk/xai": "2.0.42", - "@ai-sdk/cerebras": "1.0.33", - "@ai-sdk/cohere": "2.0.21", - "@ai-sdk/deepinfra": "1.0.30", - "@ai-sdk/gateway": "2.0.23", - "@ai-sdk/groq": "2.0.33", - "@ai-sdk/perplexity": "2.0.22", "@ai-sdk/togetherai": "1.0.30", + "@ai-sdk/xai": "2.0.42", "@clack/prompts": "1.0.0-alpha.1", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 63abdf3e471..4d82f2a5fd8 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.0.203", + "version": "1.0.204", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -24,4 +24,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 292e8163acd..ac6f8480269 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.0.203", + "version": "1.0.204", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index 4c2f8eb7356..98cb0d7e7d6 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.203", + "version": "1.0.204", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 0e7da54bdcb..bb6adb0fb76 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.203", + "version": "1.0.204", "type": "module", "exports": { "./*": "./src/components/*.tsx", diff --git a/packages/util/package.json b/packages/util/package.json index c5df6f176bc..f558fdc01c0 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.203", + "version": "1.0.204", "private": true, "type": "module", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 2fb471239b7..866eaab394a 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.203", + "version": "1.0.204", "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 1b4cf99f985..5d15e76c2eb 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.0.203", + "version": "1.0.204", "publisher": "sst-dev", "repository": { "type": "git", From 7617f594412acd3545bce6f6dd07f86d36b25560 Mon Sep 17 00:00:00 2001 From: ewired <37567272+ewired@users.noreply.github.com> Date: Sat, 27 Dec 2025 19:53:17 -0500 Subject: [PATCH 084/164] Allow line numbers and ranges in autocomplete (#4238) --- .../cmd/tui/component/prompt/autocomplete.tsx | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index cef083ad734..a5823289505 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -12,6 +12,38 @@ import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "@/util/locale" import type { PromptInfo } from "./history" +function removeLineRange(input: string) { + const hashIndex = input.lastIndexOf("#") + return hashIndex !== -1 ? input.substring(0, hashIndex) : input +} + +function extractLineRange(input: string) { + const hashIndex = input.lastIndexOf("#") + if (hashIndex === -1) { + return { baseQuery: input } + } + + const baseName = input.substring(0, hashIndex) + const linePart = input.substring(hashIndex + 1) + const lineMatch = linePart.match(/^(\d+)(?:-(\d*))?$/) + + if (!lineMatch) { + return { baseQuery: baseName } + } + + const startLine = Number(lineMatch[1]) + const endLine = lineMatch[2] && startLine < Number(lineMatch[2]) ? Number(lineMatch[2]) : undefined + + return { + lineRange: { + baseName, + startLine, + endLine, + }, + baseQuery: baseName, + } +} + export type AutocompleteRef = { onInput: (value: string) => void onKeyDown: (e: KeyEvent) => void @@ -142,9 +174,11 @@ export function Autocomplete(props: { async (query) => { if (!store.visible || store.visible === "/") return [] + const { lineRange, baseQuery } = extractLineRange(query ?? "") + // Get files from SDK const result = await sdk.client.find.files({ - query: query ?? "", + query: baseQuery, }) const options: AutocompleteOption[] = [] @@ -153,15 +187,27 @@ export function Autocomplete(props: { if (!result.error && result.data) { const width = props.anchor().width - 4 options.push( - ...result.data.map( - (item): AutocompleteOption => ({ - display: Locale.truncateMiddle(item, width), + ...result.data.map((item): AutocompleteOption => { + let url = `file://${process.cwd()}/${item}` + let filename = item + if (lineRange && !item.endsWith("/")) { + filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}` + const urlObj = new URL(url) + urlObj.searchParams.set("start", String(lineRange.startLine)) + if (lineRange.endLine !== undefined) { + urlObj.searchParams.set("end", String(lineRange.endLine)) + } + url = urlObj.toString() + } + + return { + display: Locale.truncateMiddle(filename, width), onSelect: () => { - insertPart(item, { + insertPart(filename, { type: "file", mime: "text/plain", - filename: item, - url: `file://${process.cwd()}/${item}`, + filename, + url, source: { type: "file", text: { @@ -173,8 +219,8 @@ export function Autocomplete(props: { }, }) }, - }), - ), + } + }), ) } @@ -383,8 +429,8 @@ export function Autocomplete(props: { return prev } - const result = fuzzysort.go(currentFilter, mixed, { - keys: [(obj) => obj.display.trimEnd(), "description", (obj) => obj.aliases?.join(" ") ?? ""], + const result = fuzzysort.go(removeLineRange(currentFilter), mixed, { + keys: [(obj) => removeLineRange(obj.display.trimEnd()), "description", (obj) => obj.aliases?.join(" ") ?? ""], limit: 10, scoreFn: (objResults) => { const displayResult = objResults[0] From 613813ac12fa17cfae9b3b26d7b0d2105e273a63 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 28 Dec 2025 00:53:48 +0000 Subject: [PATCH 085/164] 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 4d82f2a5fd8..63174acec23 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,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 ac6f8480269..7933647718b 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 9d485dd307ebced056729ae56903064c66cff85c Mon Sep 17 00:00:00 2001 From: Ivan Pantic Date: Sun, 28 Dec 2025 01:54:27 +0100 Subject: [PATCH 086/164] docs: add opencode-notificator to ecosystem plugins list (#6269) Co-authored-by: Ivan Pantic Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/web/src/content/docs/ecosystem.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx index 9c772b99305..de0d5fd3769 100644 --- a/packages/web/src/content/docs/ecosystem.mdx +++ b/packages/web/src/content/docs/ecosystem.mdx @@ -32,6 +32,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw | [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs | | [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers | | [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible | +| [opencode-notificator](https://github.com/panta/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions | | [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context | --- From de28fafb471cca4a79be2b9e0b8767ec852ea5ab Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 27 Dec 2025 19:07:25 -0600 Subject: [PATCH 087/164] fix: search all recent models instead of only top 5 in TUI /models command --- .../src/cli/cmd/tui/component/dialog-model.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index fc0559cd686..bc90dbb5c6e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -37,11 +37,9 @@ export function DialogModel(props: { providerID?: string }) { const recents = local.model.recent() const recentList = showExtra() - ? recents - .filter( - (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID), - ) - .slice(0, 5) + ? recents.filter( + (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID), + ) : [] const favoriteOptions = favorites.flatMap((item) => { @@ -182,7 +180,10 @@ export function DialogModel(props: { providerID?: string }) { // Apply fuzzy filtering to each section separately, maintaining section order if (q) { const filteredFavorites = fuzzysort.go(q, favoriteOptions, { keys: ["title"] }).map((x) => x.obj) - const filteredRecents = fuzzysort.go(q, recentOptions, { keys: ["title"] }).map((x) => x.obj) + const filteredRecents = fuzzysort + .go(q, recentOptions, { keys: ["title"] }) + .map((x) => x.obj) + .slice(0, 5) const filteredProviders = fuzzysort.go(q, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj) const filteredPopular = fuzzysort.go(q, popularProviders, { keys: ["title"] }).map((x) => x.obj) return [...filteredFavorites, ...filteredRecents, ...filteredProviders, ...filteredPopular] From 7a94d7a2c5a12c9fbda987b8c63dddd5df3f1393 Mon Sep 17 00:00:00 2001 From: processtrader <232431073+processtrader@users.noreply.github.com> Date: Sun, 28 Dec 2025 02:10:23 +0100 Subject: [PATCH 088/164] fix: stats command to correctly handle `--days 0` for current day statistics (#6259) Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- packages/opencode/src/cli/cmd/stats.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index f41b23ee971..94f1b549f40 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -82,12 +82,21 @@ async function getAllSessions(): Promise { return sessions } -async function aggregateSessionStats(days?: number, projectFilter?: string): Promise { +export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise { const sessions = await getAllSessions() - const DAYS_IN_SECOND = 24 * 60 * 60 * 1000 - const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0 + const MS_IN_DAY = 24 * 60 * 60 * 1000 + + const cutoffTime = (() => { + if (days === undefined) return 0 + if (days === 0) { + const now = new Date() + now.setHours(0, 0, 0, 0) + return now.getTime() + } + return Date.now() - days * MS_IN_DAY + })() - let filteredSessions = days ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions + let filteredSessions = cutoffTime > 0 ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions if (projectFilter !== undefined) { if (projectFilter === "") { @@ -198,7 +207,7 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro } } - const actualDays = Math.max(1, Math.ceil((latestTime - earliestTime) / DAYS_IN_SECOND)) + const actualDays = Math.max(1, Math.ceil((latestTime - earliestTime) / MS_IN_DAY)) stats.dateRange = { earliest: earliestTime, latest: latestTime, From 8a2f4ddf70813aa37c69810d5c5a96a1388dd6fa Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Sun, 28 Dec 2025 01:10:51 +0000 Subject: [PATCH 089/164] chore: update `INVALID_DIRS` to include plural 'skills' directory (#6255) --- 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 c94a34be0e6..807cd46fd26 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -155,7 +155,7 @@ export namespace Config { } }) - const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools"].join(",")}}/`) + const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools", "skills"].join(",")}}/`) async function assertValid(dir: string) { const invalid = await Array.fromAsync( INVALID_DIRS.scan({ From 2fe7a7f2d32e8f61d1e88e2bd2966b66ee9a9cd3 Mon Sep 17 00:00:00 2001 From: Nindaleth Date: Sun, 28 Dec 2025 02:11:30 +0100 Subject: [PATCH 090/164] docs: document attach command (#6254) Co-authored-by: Black_Fox --- packages/web/src/content/docs/cli.mdx | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 4a826e5b3ff..35ef993b8ec 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -57,6 +57,33 @@ opencode agent [command] --- +### attach + +Attach a terminal to an already running OpenCode backend server started via `serve` or `web` commands. + +```bash +opencode attach [url] +``` + +This allows using the TUI with a remote OpenCode backend. For example: + +```bash +# Start the backend server for web/mobile access +opencode web --port 4096 --hostname 0.0.0.0 + +# In another terminal, attach the TUI to the running backend +opencode attach http://10.20.30.40:4096 +``` + +#### Flags + +| Flag | Short | Description | +| ------------ | ----- | --------------------------------- | +| `--dir` | | Working directory to start TUI in | +| `--session` | `-s` | Session ID to continue | + +--- + #### create Create a new agent with custom configuration. @@ -325,7 +352,7 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript" ### serve -Start a headless opencode server for API access. Check out the [server docs](/docs/server) for the full HTTP interface. +Start a headless OpenCode server for API access. Check out the [server docs](/docs/server) for the full HTTP interface. ```bash opencode serve From 2c0d9a46cbd394b5f50e12e6a4a03928307607c2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 28 Dec 2025 01:12:02 +0000 Subject: [PATCH 091/164] chore: generate --- packages/web/src/content/docs/cli.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 35ef993b8ec..1553dc80ee9 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -77,10 +77,10 @@ opencode attach http://10.20.30.40:4096 #### Flags -| Flag | Short | Description | -| ------------ | ----- | --------------------------------- | -| `--dir` | | Working directory to start TUI in | -| `--session` | `-s` | Session ID to continue | +| Flag | Short | Description | +| ----------- | ----- | --------------------------------- | +| `--dir` | | Working directory to start TUI in | +| `--session` | `-s` | Session ID to continue | --- From e35d97f9d7005a4227eb56cc008cffb230161eda Mon Sep 17 00:00:00 2001 From: scarf Date: Sun, 28 Dec 2025 10:14:56 +0900 Subject: [PATCH 092/164] feat: add bash shell completions (#6239) --- packages/opencode/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 638ee7347db..03ccf76042f 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -77,6 +77,7 @@ const cli = yargs(hideBin(process.argv)) }) }) .usage("\n" + UI.logo()) + .completion("completion", "generate shell completion script") .command(AcpCommand) .command(McpCommand) .command(TuiThreadCommand) From 7ea0d37ee3b01be8788a95db5b6f08690d01465c Mon Sep 17 00:00:00 2001 From: rektide Date: Sun, 28 Dec 2025 01:32:33 +0000 Subject: [PATCH 093/164] Thinking & tool call visibility settings for `/copy` and `/export` (#6243) Co-authored-by: Aiden Cline --- .../src/cli/cmd/tui/routes/session/index.tsx | 51 ++++-- .../cli/cmd/tui/ui/dialog-export-options.tsx | 148 ++++++++++++++++++ 2 files changed, 187 insertions(+), 12 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx 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 177c43a463a..d5298518700 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -52,7 +52,6 @@ import { DialogMessage } from "./dialog-message" import type { PromptInfo } from "../../component/prompt/history" import { iife } from "@/util/iife" import { DialogConfirm } from "@tui/ui/dialog-confirm" -import { DialogPrompt } from "@tui/ui/dialog-prompt" import { DialogTimeline } from "./dialog-timeline" import { DialogForkFromTimeline } from "./dialog-fork-from-timeline" import { DialogSessionRename } from "../../component/dialog-session-rename" @@ -67,7 +66,7 @@ import stripAnsi from "strip-ansi" import { Footer } from "./footer.tsx" import { usePromptRef } from "../../context/prompt" import { Filesystem } from "@/util/filesystem" -import { DialogSubagent } from "./dialog-subagent.tsx" +import { DialogExportOptions } from "../../ui/dialog-export-options" addDefaultParsers(parsers.parsers) @@ -784,8 +783,22 @@ export function Session() { for (const part of parts) { if (part.type === "text" && !part.synthetic) { transcript += `${part.text}\n\n` + } else if (part.type === "reasoning") { + if (showThinking()) { + transcript += `_Thinking:_\n\n${part.text}\n\n` + } } else if (part.type === "tool") { - transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n` + transcript += `\`\`\`\nTool: ${part.tool}\n` + if (showDetails() && part.state.input) { + transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\`` + } + if (showDetails() && part.state.status === "completed" && part.state.output) { + transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\`` + } + if (showDetails() && part.state.status === "error" && part.state.error) { + transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\`` + } + transcript += `\n\`\`\`\n\n` } } @@ -812,6 +825,14 @@ export function Session() { const sessionData = session() const sessionMessages = messages() + const defaultFilename = `session-${sessionData.id.slice(0, 8)}.md` + + const options = await DialogExportOptions.show(dialog, defaultFilename, showThinking(), showDetails()) + + if (options === null) return + + const { filename: customFilename, thinking: includeThinking, toolDetails: includeToolDetails } = options + let transcript = `# ${sessionData.title}\n\n` transcript += `**Session ID:** ${sessionData.id}\n` transcript += `**Created:** ${new Date(sessionData.time.created).toLocaleString()}\n` @@ -826,22 +847,28 @@ export function Session() { for (const part of parts) { if (part.type === "text" && !part.synthetic) { transcript += `${part.text}\n\n` + } else if (part.type === "reasoning") { + if (includeThinking) { + transcript += `_Thinking:_\n\n${part.text}\n\n` + } } else if (part.type === "tool") { - transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n` + transcript += `\`\`\`\nTool: ${part.tool}\n` + if (includeToolDetails && part.state.input) { + transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\`` + } + if (includeToolDetails && part.state.status === "completed" && part.state.output) { + transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\`` + } + if (includeToolDetails && part.state.status === "error" && part.state.error) { + transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\`` + } + transcript += `\n\`\`\`\n\n` } } transcript += `---\n\n` } - // Prompt for optional filename - const customFilename = await DialogPrompt.show(dialog, "Export filename", { - value: `session-${sessionData.id.slice(0, 8)}.md`, - }) - - // Cancel if user pressed escape - if (customFilename === null) return - // Save to file in current working directory const exportDir = process.cwd() const filename = customFilename.trim() diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx new file mode 100644 index 00000000000..874a236ee4c --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx @@ -0,0 +1,148 @@ +import { TextareaRenderable, TextAttributes } from "@opentui/core" +import { useTheme } from "../context/theme" +import { useDialog, type DialogContext } from "./dialog" +import { createStore } from "solid-js/store" +import { onMount, Show, type JSX } from "solid-js" +import { useKeyboard } from "@opentui/solid" + +export type DialogExportOptionsProps = { + defaultFilename: string + defaultThinking: boolean + defaultToolDetails: boolean + onConfirm?: (options: { filename: string; thinking: boolean; toolDetails: boolean }) => void + onCancel?: () => void +} + +export function DialogExportOptions(props: DialogExportOptionsProps) { + const dialog = useDialog() + const { theme } = useTheme() + let textarea: TextareaRenderable + const [store, setStore] = createStore({ + thinking: props.defaultThinking, + toolDetails: props.defaultToolDetails, + active: "filename" as "filename" | "thinking" | "toolDetails", + }) + + useKeyboard((evt) => { + if (evt.name === "return") { + props.onConfirm?.({ + filename: textarea.plainText, + thinking: store.thinking, + toolDetails: store.toolDetails, + }) + } + if (evt.name === "tab") { + const order: Array<"filename" | "thinking" | "toolDetails"> = ["filename", "thinking", "toolDetails"] + const currentIndex = order.indexOf(store.active) + const nextIndex = (currentIndex + 1) % order.length + setStore("active", order[nextIndex]) + evt.preventDefault() + } + if (evt.name === "space") { + if (store.active === "thinking") setStore("thinking", !store.thinking) + if (store.active === "toolDetails") setStore("toolDetails", !store.toolDetails) + evt.preventDefault() + } + }) + + onMount(() => { + dialog.setSize("medium") + setTimeout(() => { + textarea.focus() + }, 1) + textarea.gotoLineEnd() + }) + + return ( + + + + Export Options + + esc + + + + Filename: + +