From 3ab41d548f0c2352d6fd351e16d8151bc4c25f00 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 20:35:02 -0500 Subject: [PATCH 01/13] ci: skip force push when beta branch is unchanged --- script/beta.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/script/beta.ts b/script/beta.ts index 53329e4dce..4355c5879c 100755 --- a/script/beta.ts +++ b/script/beta.ts @@ -124,7 +124,18 @@ async function main() { throw new Error(`${failed.length} PR(s) failed to merge`) } - console.log("\nForce pushing beta branch...") + console.log("\nChecking if beta branch has changes...") + await $`git fetch origin beta` + + const localTree = await $`git rev-parse beta^{tree}`.text() + const remoteTree = await $`git rev-parse origin/beta^{tree}`.text() + + if (localTree.trim() === remoteTree.trim()) { + console.log("Beta branch has identical contents, no push needed") + return + } + + console.log("Force pushing beta branch...") await $`git push origin beta --force --no-verify` console.log("Successfully synced beta branch") From 6c9b2c37a5e12f0e79ce5a18af33073c9e646cc1 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 20:39:58 -0500 Subject: [PATCH 02/13] core: allow starting new sessions after errors by fixing stuck session status --- packages/opencode/src/session/processor.ts | 1 + packages/opencode/src/session/prompt.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index ada6f83145..24b4a4f9fb 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -368,6 +368,7 @@ export namespace SessionProcessor { sessionID: input.assistantMessage.sessionID, error: input.assistantMessage.error, }) + SessionStatus.set(input.sessionID, { type: "idle" }) } if (snapshot) { const patch = await Snapshot.patch(snapshot) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index c6d040f2fa..98dce97ba9 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -62,7 +62,7 @@ export namespace SessionPrompt { abort: AbortController callbacks: { resolve(input: MessageV2.WithParts): void - reject(): void + reject(reason?: any): void }[] } > = {} @@ -72,7 +72,7 @@ export namespace SessionPrompt { for (const item of Object.values(current)) { item.abort.abort() for (const callback of item.callbacks) { - callback.reject() + callback.reject(new DOMException("Aborted", "AbortError")) } } }, @@ -251,10 +251,13 @@ export namespace SessionPrompt { log.info("cancel", { sessionID }) const s = state() const match = s[sessionID] - if (!match) return + if (!match) { + SessionStatus.set(sessionID, { type: "idle" }) + return + } match.abort.abort() for (const item of match.callbacks) { - item.reject() + item.reject(new DOMException("Aborted", "AbortError")) } delete s[sessionID] SessionStatus.set(sessionID, { type: "idle" }) From 83d0e48e3854a4a9f8c28ec17398a8554c988d39 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 20:52:17 -0500 Subject: [PATCH 03/13] tui: fix task status to show current tool state from message store --- AGENTS.md | 1 + .../src/cli/cmd/tui/routes/session/index.tsx | 28 ++++++++++++++----- packages/opencode/src/tool/task.ts | 1 - 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8cfe768da3..3df71f6419 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,7 @@ - Prefer single word variable names where possible - Use Bun APIs when possible, like `Bun.file()` - Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity +- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream ### Avoid let statements 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 ada84e487b..209469bad8 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1799,9 +1799,19 @@ function Task(props: ToolProps) { const keybind = useKeybind() const { navigate } = useRoute() const local = useLocal() + const sync = useSync() - const current = createMemo(() => props.metadata.summary?.findLast((x) => x.state.status !== "pending")) - const color = createMemo(() => local.agent.color(props.input.subagent_type ?? "unknown")) + const tools = createMemo(() => { + const sessionID = props.metadata.sessionId + const msgs = sync.data.message[sessionID ?? ""] ?? [] + return msgs.flatMap((msg) => + (sync.data.part[msg.id] ?? []) + .filter((part): part is ToolPart => part.type === "tool") + .map((part) => ({ tool: part.tool, state: part.state })), + ) + }) + + const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending")) return ( @@ -1817,13 +1827,17 @@ function Task(props: ToolProps) { > - {props.input.description} ({props.metadata.summary?.length ?? 0} toolcalls) + {props.input.description} ({tools().length} toolcalls) - - └ {Locale.titlecase(current()!.tool)}{" "} - {current()!.state.status === "completed" ? current()!.state.title : ""} - + {(item) => { + const title = item().state.status === "completed" ? (item().state as any).title : "" + return ( + + └ {Locale.titlecase(item().tool)} {title} + + ) + }} diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index ad4268b7b0..ac28d9f322 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -130,7 +130,6 @@ export const TaskTool = Tool.define("task", async (ctx) => { ctx.metadata({ title: params.description, metadata: { - summary: Object.values(parts).sort((a, b) => a.id.localeCompare(b.id)), sessionId: session.id, model, }, From 0f3630d936f8bf8e53edc8050c37696f3a098d10 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 21:01:57 -0500 Subject: [PATCH 04/13] ci: skip unicode filename test due to inconsistent behavior causing CI failures --- packages/opencode/test/snapshot/snapshot.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index de58f4f85e..ef6271ed5d 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -292,7 +292,7 @@ test("unicode filenames", async () => { }) }) -test("unicode filenames modification and restore", async () => { +test.skip("unicode filenames modification and restore", async () => { await using tmp = await bootstrap() await Instance.provide({ directory: tmp.path, From d1f884033f2325ffd344da53b93d89da2ca296ca Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 21:02:26 -0500 Subject: [PATCH 05/13] ignore: switch commit model to kimi-k2.5 for improved command execution reliability --- .opencode/command/commit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index 8260029195..d8a420b173 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -1,6 +1,6 @@ --- description: git commit and push -model: opencode/glm-4.7 +model: opencode/kimi-k2.5 subtask: true --- From 826664b559c3d42f60c8f48fa5dddce997cc0cf0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 21:16:13 -0500 Subject: [PATCH 06/13] ci: restrict nix-hashes workflow to dev branch pushes only Remove pull_request trigger and limit push trigger to dev branch to prevent unnecessary workflow runs on feature branches and PRs. The workflow will now only execute when dependency files change on the dev branch. --- .github/workflows/nix-hashes.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/nix-hashes.yml b/.github/workflows/nix-hashes.yml index cc16d81844..5446f9212f 100644 --- a/.github/workflows/nix-hashes.yml +++ b/.github/workflows/nix-hashes.yml @@ -6,13 +6,7 @@ permissions: on: workflow_dispatch: push: - paths: - - "bun.lock" - - "package.json" - - "packages/*/package.json" - - "flake.lock" - - ".github/workflows/nix-hashes.yml" - pull_request: + branches: [dev] paths: - "bun.lock" - "package.json" From 8fbba8de73461b82dfa72c870b8686c9e177ead5 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 1 Feb 2026 21:39:24 -0500 Subject: [PATCH 07/13] ci: Fix Pulumi version conflict in deploy workflow Added a workaround to fix Pulumi version conflict in the deployment workflow. --- .github/workflows/deploy.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 25466a63e0..c08d7edf3b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,6 +21,15 @@ jobs: with: node-version: "24" + # Workaround for Pulumi version conflict: + # GitHub runners have Pulumi 3.212.0+ pre-installed, which removed the -root flag + # from pulumi-language-nodejs (see https://github.com/pulumi/pulumi/pull/21065). + # SST 3.17.x uses Pulumi SDK 3.210.0 which still passes -root, causing a conflict. + # Removing the system language plugin forces SST to use its bundled compatible version. + # TODO: Remove when sst supports Pulumi >3.210.0 + - name: Fix Pulumi version conflict + run: sudo rm -f /usr/local/bin/pulumi-language-nodejs + - run: bun sst deploy --stage=${{ github.ref_name }} env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} From eade8ee07ba36ab3ee2e409135172c5503ae1c11 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 22:28:00 -0500 Subject: [PATCH 08/13] docs: Restructure AGENTS.md style guide with organized sections and code examples --- AGENTS.md | 96 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3df71f6419..eeec0c3418 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,9 @@ ## Style Guide +### General Principles + - Keep things in one function unless composable or reusable -- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context - Avoid `try`/`catch` where possible - Avoid using the `any` type - Prefer single word variable names where possible @@ -14,70 +15,97 @@ - Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity - Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream -### Avoid let statements +### Naming -We don't like `let` statements, especially combined with if/else statements. -Prefer `const`. - -Good: +Prefer single word names for variables and functions. Only use multiple words if necessary. ```ts -const foo = condition ? 1 : 2 +// Good +const foo = 1 +function journal(dir: string) {} + +// Bad +const fooBar = 1 +function prepareJournal(dir: string) {} ``` -Bad: +Reduce total variable count by inlining when a value is only used once. ```ts -let foo +// Good +const journal = await Bun.file(path.join(dir, "journal.json")).json() +// Bad +const journalPath = path.join(dir, "journal.json") +const journal = await Bun.file(journalPath).json() +``` + +### Destructuring + +Avoid unnecessary destructuring. Use dot notation to preserve context. + +```ts +// Good +obj.a +obj.b + +// Bad +const { a, b } = obj +``` + +### Variables + +Prefer `const` over `let`. Use ternaries or early returns instead of reassignment. + +```ts +// Good +const foo = condition ? 1 : 2 + +// Bad +let foo if (condition) foo = 1 else foo = 2 ``` -### Avoid else statements +### Control Flow -Prefer early returns or using an `iife` to avoid else statements. - -Good: +Avoid `else` statements. Prefer early returns. ```ts +// Good function foo() { if (condition) return 1 return 2 } -``` -Bad: - -```ts +// Bad function foo() { if (condition) return 1 else return 2 } ``` -### Prefer single word naming +### Schema Definitions (Drizzle) -Try your best to find a single word name for your variables, functions, etc. -Only use multiple words if you cannot. - -Good: +Use snake_case for field names so column names don't need to be redefined as strings. ```ts -const foo = 1 -const bar = 2 -const baz = 3 -``` +// Good +const table = sqliteTable("session", { + id: text().primaryKey(), + project_id: text().notNull(), + created_at: integer().notNull(), +}) -Bad: - -```ts -const fooBar = 1 -const barBaz = 2 -const bazFoo = 3 +// Bad +const table = sqliteTable("session", { + id: text("id").primaryKey(), + projectID: text("project_id").notNull(), + createdAt: integer("created_at").notNull(), +}) ``` ## Testing -You MUST avoid using `mocks` as much as possible. -Tests MUST test actual implementation, do not duplicate logic into a test. +- Avoid mocks as much as possible +- Test actual implementation, do not duplicate logic into tests From c5dc075a8866e72ba710ade780044505d96d359e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 22:29:45 -0500 Subject: [PATCH 09/13] Revert "fix(plugin): correct exports to point to dist instead of src" This reverts commit 7417e6eb38e96c5e3904a4503740a305475d154a. --- packages/plugin/package.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 160ce6a826..5a4afbae43 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -9,14 +9,8 @@ "build": "tsc" }, "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./tool": { - "types": "./dist/tool.d.ts", - "import": "./dist/tool.js" - } + ".": "./src/index.ts", + "./tool": "./src/tool.ts" }, "files": [ "dist" From a4d31b6f950117b6ad241a0d191759d37d3302f4 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 1 Feb 2026 22:32:45 -0500 Subject: [PATCH 10/13] ci: enable typecheck on push to dev branch to catch type errors immediately after merge --- .github/workflows/typecheck.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 011e23f5f6..b247d24b40 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -1,6 +1,8 @@ name: typecheck on: + push: + branches: [dev] pull_request: branches: [dev] workflow_dispatch: From 8e985e0a75ca5f2cb859434fe82dee7ea81cb59f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 2 Feb 2026 04:48:29 +0100 Subject: [PATCH 11/13] Use opentui OSC52 clipboard (#11718) Co-authored-by: opencode-agent[bot] --- bun.lock | 20 +++++++------- nix/hashes.json | 8 +++--- packages/opencode/package.json | 4 +-- packages/opencode/src/cli/cmd/tui/app.tsx | 1 + .../src/cli/cmd/tui/util/clipboard.ts | 26 ++++++++----------- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/bun.lock b/bun.lock index 0857283634..ace2735991 100644 --- a/bun.lock +++ b/bun.lock @@ -298,8 +298,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.75", - "@opentui/solid": "0.1.75", + "@opentui/core": "0.1.76", + "@opentui/solid": "0.1.76", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -1227,21 +1227,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.75", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.75", "@opentui/core-darwin-x64": "0.1.75", "@opentui/core-linux-arm64": "0.1.75", "@opentui/core-linux-x64": "0.1.75", "@opentui/core-win32-arm64": "0.1.75", "@opentui/core-win32-x64": "0.1.75", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA=="], + "@opentui/core": ["@opentui/core@0.1.76", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.76", "@opentui/core-darwin-x64": "0.1.76", "@opentui/core-linux-arm64": "0.1.76", "@opentui/core-linux-x64": "0.1.76", "@opentui/core-win32-arm64": "0.1.76", "@opentui/core-win32-x64": "0.1.76", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Y4f4KH6Mbj0J6+MorcvtHSeT+Lbs3YDPEQcTRTWsPOqWz3A0F5/+OPtZKho1EtLWQqJflCWdf/JQj5A3We3qRg=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.75", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGaGZjkFpqcXJk6321JzhRl66pM2VxBlI470L8W4DQUW4S6iDT1R9L7awSzGB4Cn9toUl7DTV8BemaXZYXV4SA=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.76", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aRYNOPRKL6URovSPhRvXtBV7SqdmR7s6hmEBSdXiYo39AozTcvKviF8gJWXQATcKDEcOtRir6TsASzDq5Coheg=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.75", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPlvqQI0whZ76amHydpJs5kN+QeWAIcFbI8RAtlAo9baj2EbxTDC+JGwgb9Fnt0/YQx831humbtaNDhV2Jt1bw=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.76", "", { "os": "darwin", "cpu": "x64" }, "sha512-KFaRvVQ0Wr1PgaexUkF3KYt41pYmxGJW3otENeE6WDa/nXe2AElibPFRjqSEH54YrY5Q84SDI77/wGP4LZ/Wyg=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.75", "", { "os": "linux", "cpu": "arm64" }, "sha512-nVxIQ4Hqf84uBergDpWiVzU6pzpjy6tqBHRQpySxZ2flkJ/U6/aMEizVrQ1jcgIdxZtvqWDETZhzxhG0yDx+cw=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.76", "", { "os": "linux", "cpu": "arm64" }, "sha512-s7v+GDwavfieZg8xZV4V07fXFrHfFq4UZ2JpYFDUgNs9vFp+++WUjh3pfbfE+2ldbhcG2iOtuiV9aG1tVCbTEg=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.75", "", { "os": "linux", "cpu": "x64" }, "sha512-1CnApef4kxA+ORyLfbuCLgZfEjp4wr3HjFnt7FAfOb73kIZH82cb7JYixeqRyy9eOcKfKqxLmBYy3o8IDkc4Rg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.76", "", { "os": "linux", "cpu": "x64" }, "sha512-ugwuHpmvdKRHXKVsrC3zRYY6bg2JxVCzAQ1NOiWRLq3N3N4ha6BHAkHMCeHgR/ZI4R8MSRB6vtJRVI1F9VHxjA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.75", "", { "os": "win32", "cpu": "arm64" }, "sha512-j0UB95nmkYGNzmOrs6GqaddO1S90R0YC6IhbKnbKBdjchFPNVLz9JpexAs6MBDXPZwdKAywMxtwG2h3aTJtxng=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.76", "", { "os": "win32", "cpu": "arm64" }, "sha512-wjpRWrerPItb5E1fP4SAcNMxQp1yEukbgvP4Azip836/ixxbghL6y0P57Ya/rv7QYLrkNZXoQ+tr9oXhPH5BVA=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.75", "", { "os": "win32", "cpu": "x64" }, "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.76", "", { "os": "win32", "cpu": "x64" }, "sha512-2YjtZJdd3iO+SY9NKocE4/Pm9VolzAthUOXjpK4Pv5pnR9hBpPvX7FFSXJTfASj7y2j1tATWrlQLocZCFP/oMA=="], - "@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="], + "@opentui/solid": ["@opentui/solid@0.1.76", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.76", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-PiD62FGoPoVLFpY4g08i4UYlx4sGR2OmHUPj6CuZZwy2UJD4fKn1RYV+kAPHfUW4qN/88I1k/w/Dniz1WvXrAQ=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/nix/hashes.json b/nix/hashes.json index 6fe8f61d3d..9843699345 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-06Otz3loT4vn0578VDxUqVudtzQvV7oM3EIzjZnsejo=", - "aarch64-linux": "sha256-88Qai5RkSenCZkakOg52b6xU2ok+h/Ns4/5L3+55sFY=", - "aarch64-darwin": "sha256-x8dgCF0CJBWi2dZLDHMGdlTqys1X755ok0PM6x0HAGo=", - "x86_64-darwin": "sha256-FkLDqorfIfOw+tB7SW5vgyhOIoI0IV9lqPW1iEmvUiI=" + "x86_64-linux": "sha256-aRFzPzgu32XgNSk8S2z4glTlgHqEmOLZHlBQSIYIMvY=", + "aarch64-linux": "sha256-aCZLkmRrCa0bli0jgsaLcC5GlZdjQPbb6xD6Fc03eX8=", + "aarch64-darwin": "sha256-oZOOR6k8MmabNVDQNY5ywR06rRycdnXZL+gUucKSQ+g=", + "x86_64-darwin": "sha256-LXIcLnjn+1eTFWIsQ9W0U2orGm59P/L470O0KFFkRHg=" } } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 4afb724300..45074b96c9 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -82,8 +82,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.75", - "@opentui/solid": "0.1.75", + "@opentui/core": "0.1.76", + "@opentui/solid": "0.1.76", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 713def2e5a..2e1ffa4f00 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -186,6 +186,7 @@ function App() { const route = useRoute() const dimensions = useTerminalDimensions() const renderer = useRenderer() + Clipboard.setRenderer(renderer) renderer.disableStdoutInterception() const dialog = useDialog() const local = useLocal() diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 0e287fbc41..5c27a26cd0 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -1,24 +1,12 @@ import { $ } from "bun" +import type { CliRenderer } from "@opentui/core" import { platform, release } from "os" import clipboardy from "clipboardy" import { lazy } from "../../../../util/lazy.js" import { tmpdir } from "os" import path from "path" -/** - * Writes text to clipboard via OSC 52 escape sequence. - * This allows clipboard operations to work over SSH by having - * the terminal emulator handle the clipboard locally. - */ -function writeOsc52(text: string): void { - if (!process.stdout.isTTY) return - const base64 = Buffer.from(text).toString("base64") - const osc52 = `\x1b]52;c;${base64}\x07` - // tmux and screen require DCS passthrough wrapping - const passthrough = process.env["TMUX"] || process.env["STY"] - const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 - process.stdout.write(sequence) -} +const rendererRef = { current: undefined as CliRenderer | undefined } export namespace Clipboard { export interface Content { @@ -26,6 +14,10 @@ export namespace Clipboard { mime: string } + export function setRenderer(renderer: CliRenderer | undefined): void { + rendererRef.current = renderer + } + export async function read(): Promise { const os = platform() @@ -154,7 +146,11 @@ export namespace Clipboard { }) export async function copy(text: string): Promise { - writeOsc52(text) + const renderer = rendererRef.current + if (renderer) { + const copied = renderer.copyToClipboardOSC52(text) + if (copied) return + } await getCopyMethod()(text) } } From 0dc80df6fd1f4c89cebf9ed20cd9d9291f996236 Mon Sep 17 00:00:00 2001 From: Dax Date: Sun, 1 Feb 2026 22:51:55 -0500 Subject: [PATCH 12/13] Add spinner animation for Task tool (#11725) --- .../cmd/tui/component/dialog-session-list.tsx | 10 ++------ .../src/cli/cmd/tui/component/spinner.tsx | 24 ++++++++++++++++++ .../src/cli/cmd/tui/routes/session/index.tsx | 25 ++++++++++++++++--- 3 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/spinner.tsx 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 85c174c1dc..775969bfcb 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 @@ -10,7 +10,7 @@ import { useSDK } from "../context/sdk" import { DialogSessionRename } from "./dialog-session-rename" import { useKV } from "../context/kv" import { createDebouncedSignal } from "../util/signal" -import "opentui-spinner/solid" +import { Spinner } from "./spinner" export function DialogSessionList() { const dialog = useDialog() @@ -32,8 +32,6 @@ export function DialogSessionList() { const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) - const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] - const sessions = createMemo(() => searchResults() ?? sync.data.session) const options = createMemo(() => { @@ -56,11 +54,7 @@ export function DialogSessionList() { value: x.id, category, footer: Locale.time(x.time.updated), - gutter: isWorking ? ( - [⋯]}> - - - ) : undefined, + gutter: isWorking ? : undefined, } }) }) diff --git a/packages/opencode/src/cli/cmd/tui/component/spinner.tsx b/packages/opencode/src/cli/cmd/tui/component/spinner.tsx new file mode 100644 index 0000000000..8dc5455504 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/spinner.tsx @@ -0,0 +1,24 @@ +import { Show } from "solid-js" +import { useTheme } from "../context/theme" +import { useKV } from "../context/kv" +import type { JSX } from "@opentui/solid" +import type { RGBA } from "@opentui/core" +import "opentui-spinner/solid" + +const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + +export function Spinner(props: { children?: JSX.Element; color?: RGBA }) { + const { theme } = useTheme() + const kv = useKV() + const color = () => props.color ?? theme.textMuted + return ( + ⋯ {props.children}}> + + + + {props.children} + + + + ) +} 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 209469bad8..8316d112c9 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -16,6 +16,7 @@ import path from "path" import { useRoute, useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" import { SplitBorder } from "@tui/component/border" +import { Spinner } from "@tui/component/spinner" import { useTheme } from "@tui/context/theme" import { BoxRenderable, @@ -1559,7 +1560,13 @@ function InlineTool(props: { ) } -function BlockTool(props: { title: string; children: JSX.Element; onClick?: () => void; part?: ToolPart }) { +function BlockTool(props: { + title: string + children: JSX.Element + onClick?: () => void + part?: ToolPart + spinner?: boolean +}) { const { theme } = useTheme() const renderer = useRenderer() const [hover, setHover] = createSignal(false) @@ -1582,9 +1589,16 @@ function BlockTool(props: { title: string; children: JSX.Element; onClick?: () = props.onClick?.() }} > - - {props.title} - + + {props.title} + + } + > + {props.title.replace(/^# /, "")} + {props.children} {error()} @@ -1813,6 +1827,8 @@ function Task(props: ToolProps) { const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending")) + const isRunning = createMemo(() => props.part.state.status === "running") + return ( @@ -1824,6 +1840,7 @@ function Task(props: ToolProps) { : undefined } part={props.part} + spinner={isRunning()} > From c69474846f079317c5d6a422c577793c5ba65b8d Mon Sep 17 00:00:00 2001 From: Dax Date: Sun, 1 Feb 2026 23:37:07 -0500 Subject: [PATCH 13/13] Simplify directory tree output for prompts (#11731) --- packages/opencode/src/file/ripgrep.ts | 112 +++++++----------------- packages/opencode/src/session/system.ts | 6 +- 2 files changed, 37 insertions(+), 81 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 463a9fb362..c1e5113bf8 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -275,100 +275,56 @@ export namespace Ripgrep { log.info("tree", input) const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd, signal: input.signal })) interface Node { - path: string[] - children: Node[] + name: string + children: Map } - function getPath(node: Node, parts: string[], create: boolean) { - if (parts.length === 0) return node - let current = node - for (const part of parts) { - let existing = current.children.find((x) => x.path.at(-1) === part) - if (!existing) { - if (!create) return - existing = { - path: current.path.concat(part), - children: [], - } - current.children.push(existing) - } - current = existing - } - return current + function dir(node: Node, name: string) { + const existing = node.children.get(name) + if (existing) return existing + const next = { name, children: new Map() } + node.children.set(name, next) + return next } - const root: Node = { - path: [], - children: [], - } + const root: Node = { name: "", children: new Map() } for (const file of files) { if (file.includes(".opencode")) continue const parts = file.split(path.sep) - getPath(root, parts, true) - } - - function sort(node: Node) { - node.children.sort((a, b) => { - if (!a.children.length && b.children.length) return 1 - if (!b.children.length && a.children.length) return -1 - return a.path.at(-1)!.localeCompare(b.path.at(-1)!) - }) - for (const child of node.children) { - sort(child) + if (parts.length < 2) continue + let node = root + for (const part of parts.slice(0, -1)) { + node = dir(node, part) } } - sort(root) - let current = [root] - const result: Node = { - path: [], - children: [], - } - - let processed = 0 - const limit = input.limit ?? 50 - while (current.length > 0) { - const next = [] - for (const node of current) { - if (node.children.length) next.push(...node.children) - } - const max = Math.max(...current.map((x) => x.children.length)) - for (let i = 0; i < max && processed < limit; i++) { - for (const node of current) { - const child = node.children[i] - if (!child) continue - getPath(result, child.path, true) - processed++ - if (processed >= limit) break - } - } - if (processed >= limit) { - for (const node of [...current, ...next]) { - const compare = getPath(result, node.path, false) - if (!compare) continue - if (compare?.children.length !== node.children.length) { - const diff = node.children.length - compare.children.length - compare.children.push({ - path: compare.path.concat(`[${diff} truncated]`), - children: [], - }) - } - } - break - } - current = next + function count(node: Node): number { + let total = 0 + for (const child of node.children.values()) { + total += 1 + count(child) + } + return total } + const total = count(root) + const limit = input.limit ?? total const lines: string[] = [] + const queue: { node: Node; path: string }[] = [] + for (const child of Array.from(root.children.values()).sort((a, b) => a.name.localeCompare(b.name))) { + queue.push({ node: child, path: child.name }) + } - function render(node: Node, depth: number) { - const indent = "\t".repeat(depth) - lines.push(indent + node.path.at(-1) + (node.children.length ? "/" : "")) - for (const child of node.children) { - render(child, depth + 1) + let used = 0 + for (let i = 0; i < queue.length && used < limit; i++) { + const { node, path } = queue[i] + lines.push(path) + used++ + for (const child of Array.from(node.children.values()).sort((a, b) => a.name.localeCompare(b.name))) { + queue.push({ node: child, path: `${path}/${child.name}` }) } } - result.children.map((x) => render(x, 0)) + + if (total > used) lines.push(`[${total - used} truncated]`) return lines.join("\n") } diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index d34a086fe4..b02267bf8a 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -36,16 +36,16 @@ export namespace SystemPrompt { ` Platform: ${process.platform}`, ` Today's date: ${new Date().toDateString()}`, ``, - ``, + ``, ` ${ project.vcs === "git" && false ? await Ripgrep.tree({ cwd: Instance.directory, - limit: 200, + limit: 50, }) : "" }`, - ``, + ``, ].join("\n"), ] }