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 01/19] 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 02/19] 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 03/19] 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 04/19] 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 05/19] 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 06/19] 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 07/19] 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 08/19] 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 09/19] 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 10/19] 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 11/19] 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 12/19] 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 13/19] 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 14/19] 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 15/19] 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 16/19] 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 2c648a4c3188069d08fb39c0d3bd06629a434b49 Mon Sep 17 00:00:00 2001 From: shuv Date: Fri, 26 Dec 2025 15:02:56 -0800 Subject: [PATCH 17/19] sync: record last synced tag v1.0.203 --- .github/last-synced-tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/last-synced-tag b/.github/last-synced-tag index ae3553b8ac8..ce81bd0b334 100644 --- a/.github/last-synced-tag +++ b/.github/last-synced-tag @@ -1 +1 @@ -v1.0.201 +v1.0.203 From cc07bb5eaf6cf910a347c39e9910d5efc63c4441 Mon Sep 17 00:00:00 2001 From: shuv Date: Fri, 26 Dec 2025 15:49:30 -0800 Subject: [PATCH 18/19] chore: remove completed planning documents --- ...N-202-web-app-askquestion-ui-2025-12-24.md | 359 ------------ ...orktree-project-id-collision-2025-12-22.md | 530 ------------------ 2 files changed, 889 deletions(-) delete mode 100644 CONTEXT/PLAN-202-web-app-askquestion-ui-2025-12-24.md delete mode 100644 CONTEXT/PLAN-5638-fix-worktree-project-id-collision-2025-12-22.md diff --git a/CONTEXT/PLAN-202-web-app-askquestion-ui-2025-12-24.md b/CONTEXT/PLAN-202-web-app-askquestion-ui-2025-12-24.md deleted file mode 100644 index c260525c69e..00000000000 --- a/CONTEXT/PLAN-202-web-app-askquestion-ui-2025-12-24.md +++ /dev/null @@ -1,359 +0,0 @@ -## Goal and Scope -Create a web app askquestion wizard UI that matches TUI behavior so sessions no longer hang. This plan covers UI, state detection, endpoint wiring in the web app; no implementation is performed here. - -## Source Context and Decisions -### Issue Summary (GitHub #202) -- Problem: Web app has no askquestion UI, so askquestion tool calls hang; resuming a web-app-started session in the TUI shows questions but cannot submit answers. -- Root cause: TUI has complete askquestion handling (detection, UI, submit), web app has none. -- Acceptance criteria: Wizard UI, keyboard/tab navigation, option selection or custom input, submit/cancel behavior, resumed sessions work in both TUI and web app. - -### Key Decisions and Rationale -- Mirror TUI detection logic for pending askquestion tool parts to ensure consistent behavior across web app and TUI. This avoids inconsistent state detection and aligns with existing tool metadata behavior. -- Reuse the askquestion endpoints (`/askquestion/respond`, `/askquestion/cancel`) to keep a single server-side behavior path; avoids inventing new API shape. -- Build a dedicated `AskQuestionWizard` component in the web app (SolidJS), modeled after TUI features (wizard tabs, single/multi-select, text input, keyboard shortcuts) to ensure feature parity. -- Use sync-based detection only (via `message.part.updated` events that update tool metadata). The web app's event architecture differs from TUI's bus-based system; the sync context already handles part updates which contain the tool metadata needed for detection. -- Render the wizard inline in the session page (replacing the prompt input area when active), not as a modal overlay. This matches the TUI behavior where the dialog appears in place of the prompt. - -## Internal Code References -- TUI detection logic: `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx:398-427` -- TUI dialog implementation: `packages/opencode/src/cli/cmd/tui/ui/dialog-askquestion.tsx` -- AskQuestion types (source of truth): `packages/opencode/src/askquestion/index.ts:4-35` -- Server endpoints: `packages/opencode/src/server/server.ts:1585-1653` -- Web app session page (integration point): `packages/app/src/pages/session.tsx` -- Web app sync context: `packages/app/src/context/sync.tsx` -- Web app global sync (event handling): `packages/app/src/context/global-sync.tsx:154-295` -- New component (to add): `packages/app/src/components/askquestion-wizard.tsx` - -### Existing UI Components to Reuse -- `packages/ui/src/components/tabs.tsx` - Kobalte-based tabs for wizard navigation -- `packages/ui/src/components/checkbox.tsx` - For multi-select options -- `packages/ui/src/components/button.tsx` - For submit/cancel actions -- `packages/ui/src/components/text-field.tsx` - For custom text input -- `packages/ui/src/context/dialog.tsx` - Dialog context (for reference, but wizard renders inline) - -## External References (for UI patterns and APIs) -- https://raw.githubusercontent.com/solidjs-use/solidjs-use/main/packages/core/src/useStepper/index.md -- https://raw.githubusercontent.com/chakra-ui/zag/main/website/data/snippets/solid/tabs/usage.mdx -- https://raw.githubusercontent.com/chakra-ui/zag/main/website/data/snippets/solid/steps/usage.mdx - -## Functional Requirements Mapping -| Requirement | Plan Coverage | Notes | -| --- | --- | --- | -| Web app wizard UI when askquestion invoked | Inline component + session wiring | Matches TUI behavior and issue guidance | -| Tab/arrow navigation between questions | Keyboard and tab UI logic | Mirror TUI controls and shortcuts | -| Select options or enter custom responses | Single-select, multi-select, text input | Use type-safe question model | -| Submit answers continues conversation | POST `/askquestion/respond` | Include `callID`, `sessionID`, `answers` array | -| Cancel dismisses and signals cancellation | POST `/askquestion/cancel` | Include `callID`, `sessionID` | -| Resume sessions in TUI or web app | Sync-based detection of pending asks | Scan message parts for `status: "waiting"` | - -## Technical Specifications - -### API Endpoints -- `POST /askquestion/respond` - - Payload: `{ callID: string, sessionID: string, answers: Answer[] }` - - Purpose: Submit answers and continue tool execution - - Error: Returns error if no pending askquestion found with the given callID -- `POST /askquestion/cancel` - - Payload: `{ callID: string, sessionID: string }` - - Purpose: Cancel tool execution and dismiss UI - - Error: Returns error if no pending askquestion found with the given callID - -### Data Models and Types - -Since `AskQuestion` types are defined in `packages/opencode/src/askquestion/index.ts` and not exported through the SDK, define local types in the web app component: - -```typescript -// Types to define in packages/app/src/components/askquestion-wizard.tsx - -interface AskQuestionOption { - value: string - label: string - description?: string -} - -interface AskQuestionQuestion { - id: string - label: string // Short tab label, e.g. "UI Framework" - question: string // Full question text - options: AskQuestionOption[] // 2-8 options - multiSelect?: boolean -} - -interface AskQuestionAnswer { - questionId: string - values: string[] // Selected option value(s) - customText?: string // Custom text if user typed their own response -} - -interface PendingAskQuestion { - callID: string - messageID: string - questions: AskQuestionQuestion[] -} -``` - -### Detection Logic - -The detection logic must match TUI implementation at `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx:398-427`: - -```typescript -// Detection pattern for pending askquestion -const pendingAskQuestion = createMemo(() => { - const sessionMessages = sync.data.message[sessionID] ?? [] - - // Search backwards for the most recent pending question - for (const message of [...sessionMessages].reverse()) { - const parts = sync.data.part[message.id] ?? [] - - for (const part of [...parts].reverse()) { - if (part.type !== "tool") continue - if (part.tool !== "askquestion") continue - if (part.state.status !== "running") continue - - const metadata = part.state.metadata as { - status?: string - questions?: AskQuestionQuestion[] - } | undefined - - if (metadata?.status !== "waiting") continue - - return { - callID: part.callID, - messageID: part.messageID, - questions: metadata.questions ?? [], - } - } - } - - return null -}) -``` - -### Integration Points -- Session page (web app): detect pending askquestion and render wizard inline -- Sync context: already handles `message.part.updated` events which update tool metadata -- Prompt input area: conditionally replaced by wizard when askquestion is pending - -## Implementation Plan - -### Milestone 1: Define Types and Review TUI Parity -- [x] Define local TypeScript types for Question, Option, Answer in web app -- [x] Review TUI askquestion UX behavior at `packages/opencode/src/cli/cmd/tui/ui/dialog-askquestion.tsx` -- [x] Document keyboard shortcuts: 1-8 for quick select, Space to toggle, Enter to confirm/advance, Escape to cancel, Tab/Arrow for navigation -- [x] Confirm existing UI components to reuse: `Tabs`, `Checkbox`, `Button`, `TextField` - -### Milestone 2: Add AskQuestionWizard Component -- [x] Create `packages/app/src/components/askquestion-wizard.tsx` in SolidJS -- [x] Define component props: `questions`, `onSubmit`, `onCancel` -- [x] Implement internal state using `createStore`: - - `activeTab: number` - current question index - - `questionStates: Array<{ selectedOption: number, selectedValues: string[], customText?: string }>` - - `isTypingCustom: boolean` - whether custom input is focused -- [x] Render tab bar showing question labels with completion indicators (filled/empty circle) -- [x] Render current question with options list -- [x] Implement single-select: click/Enter selects and auto-advances -- [x] Implement multi-select: Space toggles, Enter confirms and advances -- [x] Add "Type something..." option at end of options list for custom input -- [x] Implement keyboard navigation: - - Up/Down or Ctrl+P/N: navigate options - - Left/Right or Tab/Shift+Tab: navigate questions - - 1-8: quick select option by number - - Space: toggle selection (multi-select) or select (single-select) - - Enter: confirm and advance or submit if last question - - Escape: cancel wizard - - Ctrl+Enter: submit all answers -- [x] Add footer with navigation hints -- [x] Apply styling consistent with web app design tokens - -### Milestone 3: Detect Pending AskQuestion in Session Page -- [x] Add `pendingAskQuestion` memoized signal in `packages/app/src/pages/session.tsx` -- [x] Scan synced message parts for tool parts where: - - `part.type === "tool"` - - `part.tool === "askquestion"` - - `part.state.status === "running"` - - `part.state.metadata.status === "waiting"` -- [x] Extract `callID`, `messageID`, and `questions` from matching part -- [x] Search backwards through messages to find most recent pending question -- [x] Detection works for both live sessions and resumed sessions - -### Milestone 4: Wire Session Page Integration and API Calls -- [x] Import `AskQuestionWizard` in session page -- [x] Conditionally render wizard instead of `PromptInput` when `pendingAskQuestion()` is truthy -- [x] Use `` or `/` pattern (matches TUI at line 1426-1464) -- [x] Implement `onSubmit` handler: - ```typescript - async (answers: AskQuestionAnswer[]) => { - await fetch(`${sdk.url}/askquestion/respond`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - callID: pendingAskQuestion().callID, - sessionID: params.id, - answers, - }), - }).catch(() => { - showToast({ title: "Failed to submit answers", variant: "error" }) - }) - } - ``` -- [x] Implement `onCancel` handler: - ```typescript - async () => { - await fetch(`${sdk.url}/askquestion/cancel`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - callID: pendingAskQuestion().callID, - sessionID: params.id, - }), - }).catch(() => { - showToast({ title: "Failed to cancel", variant: "error" }) - }) - } - ``` -- [x] After submit/cancel, the tool metadata will update via sync, causing `pendingAskQuestion()` to return null and hiding the wizard - -### Milestone 5: Accessibility, UX, and Edge Cases -- [x] Verify tab/arrow navigation and focus management across questions -- [x] Ensure multi-select toggles do not override previous selections -- [x] Handle empty questions list gracefully (don't render wizard) -- [x] Handle missing metadata gracefully (don't render wizard) -- [x] Prevent existing session page keyboard handlers from triggering while wizard is active - - Session page has handlers at `packages/app/src/pages/session.tsx:438-455` - - Check `document.activeElement` or add `data-prevent-autofocus` attribute -- [x] Handle API errors gracefully: - - Show toast on submit/cancel failure - - Handle case where pending request no longer exists (server restarted, session aborted) -- [x] Ensure wizard doesn't render for completed askquestion tool parts - -### Milestone 6: Testing and Validation -- [ ] Test single question with single-select options -- [ ] Test multiple questions with mixed single/multi-select -- [ ] Test custom text input for responses -- [ ] Test cancel mid-wizard -- [ ] Test submit with all questions answered -- [ ] Test resumed session with pending askquestion (web app reload) -- [ ] Test session started in TUI, resumed in web app -- [ ] Test session started in web app, resumed in TUI -- [ ] Test session abort while askquestion is pending - -## Implementation Order and Dependencies -1. Define local types and review TUI implementation (Milestone 1) -2. Build `AskQuestionWizard` component with internal state (Milestone 2) -3. Implement pending detection in session page (Milestone 3) -4. Wire session page integration and API calls (Milestone 4) -5. Address accessibility, keyboard conflicts, and edge cases (Milestone 5) -6. Test all scenarios (Milestone 6) - -## Validation Criteria -- [ ] Triggering askquestion in web app shows wizard UI immediately (replaces prompt input) -- [ ] Users can navigate between questions via tabs, arrows, and Tab key -- [ ] Users can select options via click, Enter, Space, or number keys -- [ ] Users can enter custom text responses -- [ ] Submit sends correct payload format to `/askquestion/respond` and resumes LLM response -- [ ] Cancel sends correct payload to `/askquestion/cancel` and closes wizard -- [ ] Resumed sessions with pending askquestion show the wizard in both web app and TUI -- [ ] No regression in non-askquestion session rendering -- [ ] Keyboard shortcuts in wizard don't conflict with session page shortcuts - -## Risks and Mitigations -- Risk: Web app and TUI detection logic diverge over time - - Mitigation: Document exact detection conditions; consider extracting shared detection logic in future -- Risk: Keyboard shortcuts conflict with session page shortcuts - - Mitigation: Check if wizard is active before handling session-level keyboard events; use `data-prevent-autofocus` pattern -- Risk: Server restarts or session aborts while askquestion is pending - - Mitigation: Handle API errors gracefully; show toast and allow retry or dismiss -- Risk: Type definitions in web app diverge from server schema - - Mitigation: Document that types must match `packages/opencode/src/askquestion/index.ts`; consider extracting to shared package in future -- Risk: Sync data updates race with wizard state - - Mitigation: Derive pending state from sync data (reactive), don't cache separately - -## Resolved Questions - -### Does the web app already have a dialog component or tab system to reuse? -Yes. Use these existing components: -- `packages/ui/src/components/tabs.tsx` - Kobalte-based Tabs with List, Trigger, Content -- `packages/ui/src/components/checkbox.tsx` - For multi-select option checkboxes -- `packages/ui/src/components/button.tsx` - For submit/cancel buttons -- `packages/ui/src/components/text-field.tsx` - For custom text input - -Note: The wizard should render **inline** in the session page (replacing the prompt input), not as a modal overlay via the dialog system. - -### Should the web app explicitly store pending askquestion state in sync context? -No. Derive the pending state from existing sync data using a memoized computation. The sync context already stores `message` and `part` data which contains the tool metadata. Adding separate askquestion state would require keeping it synchronized and could lead to inconsistencies. - -### Are there existing keyboard shortcut handlers in web app that must be preserved? -Yes. The session page has keyboard handlers at `packages/app/src/pages/session.tsx:438-455` that auto-focus the prompt input when typing. When the wizard is active, these handlers should be bypassed. Check `document.activeElement` or use the existing `data-prevent-autofocus` pattern. - -## Appendix: Correct Payload Examples - -### Submit Response Payload -```json -{ - "callID": "call_abc123", - "sessionID": "session_xyz789", - "answers": [ - { - "questionId": "ui_framework", - "values": ["react"], - "customText": null - }, - { - "questionId": "styling", - "values": ["tailwind", "css-modules"], - "customText": null - }, - { - "questionId": "other_requirements", - "values": [], - "customText": "I also need SSR support" - } - ] -} -``` - -### Cancel Payload -```json -{ - "callID": "call_abc123", - "sessionID": "session_xyz789" -} -``` - -### Tool Metadata Structure (from askquestion tool) -When askquestion is waiting for user input: -```json -{ - "title": "Asking 3 questions", - "metadata": { - "status": "waiting", - "questions": [ - { - "id": "ui_framework", - "label": "UI Framework", - "question": "Which UI framework would you like to use?", - "options": [ - { "value": "react", "label": "React", "description": "Popular component library" }, - { "value": "vue", "label": "Vue", "description": "Progressive framework" }, - { "value": "svelte", "label": "Svelte", "description": "Compile-time framework" } - ], - "multiSelect": false - } - ] - } -} -``` - -When askquestion is completed: -```json -{ - "title": "Asked 3 questions", - "metadata": { - "status": "completed", - "questions": ["UI Framework", "Styling", "Other Requirements"], - "answers": [ - { "questionId": "ui_framework", "values": ["react"] } - ] - } -} -``` diff --git a/CONTEXT/PLAN-5638-fix-worktree-project-id-collision-2025-12-22.md b/CONTEXT/PLAN-5638-fix-worktree-project-id-collision-2025-12-22.md deleted file mode 100644 index c01f9b8eb83..00000000000 --- a/CONTEXT/PLAN-5638-fix-worktree-project-id-collision-2025-12-22.md +++ /dev/null @@ -1,530 +0,0 @@ -# Plan: Fix Desktop App Worktree Project ID Collision - -**Issue:** [sst/opencode#5638](https://github.com/sst/opencode/issues/5638) -**Related PR:** [sst/opencode#5647](https://github.com/sst/opencode/pull/5647) -**Date:** 2025-12-22 -**Status:** Implemented -**Reviewed:** 2025-12-22 - -## Review Notes - -This plan has been reviewed against the current codebase. Key findings incorporated: - -- Cache write location must use absolute paths (fixed in Task 1.2) -- Race condition protection added to migration (Task 2.1) -- Path normalization for cross-platform hash stability (Task 1.5) -- Decision made on old project entry handling (Task 2.3 - Option B) -- Test cleanup for worktrees specified (Task 4.2) -- Existing bug fix noted: current code doesn't await cache writes (Task 1.6) - -## Problem Statement - -When opening multiple git worktrees from the same repository in the desktop app, the second worktree replaces the first one's project data. This happens because project IDs are derived solely from the root commit hash (`git rev-list --max-parents=0 --all`), which is identical across all worktrees of the same repository. - -### Root Cause - -The project ID generation in `packages/opencode/src/project/project.ts:55-73` uses only the git root commit hash as the unique identifier. Since all worktrees from the same repository share the same commit history (and thus the same root commit), they all receive the same project ID, causing data collision in storage and snapshots. - -### Impact - -- Users cannot have multiple worktrees from the same repository open simultaneously in the desktop app -- Opening a second worktree overwrites session data from the first -- This affects any workflow involving git worktrees (feature branches, parallel development, etc.) - -## Solution Overview - -Implement a differentiated project ID scheme: - -- **Main worktree:** Uses root commit hash only (backwards compatible) -- **Linked worktrees:** Uses `{rootCommit}-{worktreeHash}` format for unique IDs - -### Key Design Decisions - -1. **Backwards Compatibility:** Main worktrees retain the existing ID format to preserve existing session data for the common case -2. **Windows-safe ID format:** Use `-` as a separator instead of `|` because project IDs are used as filesystem paths in storage/snapshots and `|` is invalid on Windows -3. **Worktree Hash Caching:** Store the worktree hash in `.git/worktrees/{name}/opencode-worktree` to ensure ID stability if the worktree path changes -4. **Session Migration:** Migrate sessions from old format to new format when users upgrade, using session directory matching instead of project worktree metadata -5. **Fsmonitor Disable:** Disable git fsmonitor in snapshot repos to prevent hangs with linked worktrees - -## Technical Specifications - -### Project ID Format - -| Worktree Type | ID Format | Example | -| --------------- | ----------------------------- | -------------------------- | -| Main worktree | `{rootCommit}` | `a1b2c3d4e5f6...` | -| Linked worktree | `{rootCommit}-{worktreeHash}` | `a1b2c3d4e5f6...-7f8a9b2c` | - -### Cache File Locations - -| File | Location | Purpose | -| ------------------- | ------------------------------------------------------------------- | ------------------------------------- | -| Root commit cache | `.git/opencode` (main) or `.git/worktrees/{name}/opencode` (linked) | Cache expensive root commit lookup | -| Worktree hash cache | `.git/worktrees/{name}/opencode-worktree` | Ensure stable ID for linked worktrees | - -### Worktree Detection - -A linked worktree is detected by checking `git rev-parse --git-dir` and verifying that the normalized path includes `path.join(".git", "worktrees")`. This avoids path separator issues on Windows. - -**Important:** The `git rev-parse --git-dir` command returns a relative path (`.git`) for main worktrees but an absolute path for linked worktrees. Always resolve to absolute path using `path.resolve(worktree, gitDirRaw)` before further processing. - -### Path Normalization for Hashing - -To ensure cross-platform hash stability (e.g., WSL + Windows accessing same worktree), normalize path separators before hashing: - -```typescript -const normalizedPath = worktree.replace(/\\/g, "/") -const worktreeHash = Bun.hash(normalizedPath).toString(16) -``` - -### Project ID Format Documentation - -Add inline documentation in code: - -```typescript -// Project ID formats: -// - Main worktree: "{rootCommit}" (e.g., "a1b2c3d4...") -// - Linked worktree: "{rootCommit}-{pathHash}" (e.g., "a1b2c3d4...-7f8a9b2c") -// The separator is "-" (not "|") because project IDs are used in filesystem paths -``` - -### Upgrade Detection - -Do not rely on cached root commit for linked worktrees. Instead, after computing `rootCommit`, check for legacy storage under the old ID and migrate sessions whose `session.directory` matches the current `worktree`. - -## Files to Modify - -### Primary Changes - -| File | Changes | -| ------------------------------------------------ | ------------------------------------------------------- | -| `packages/opencode/src/project/project.ts` | Project ID generation, caching, and migration logic | -| `packages/opencode/src/snapshot/index.ts` | Disable fsmonitor for worktree compatibility | -| `packages/opencode/test/project/project.test.ts` | New tests for worktree ID differentiation and migration | - -### Reference Files (read-only) - -| File | Purpose | -| ------------------------------------------ | -------------------------------------------------- | -| `packages/opencode/src/storage/storage.ts` | Storage key structure, filesystem path constraints | -| `packages/opencode/src/session/index.ts` | Session data structure (directory fields) | - -## Implementation Tasks - -### Phase 1: Core Project ID Changes - -- [x] **1.1 Resolve worktree path early** - - Move `git rev-parse --show-toplevel` to execute before ID generation - - Normalize path for stable hashing (`path.resolve` already used) - - File: `packages/opencode/src/project/project.ts:54-87` - -- [x] **1.2 Implement gitDir resolution (with error handling)** - - Add `git rev-parse --git-dir` to get actual git directory - - Handles linked worktrees where `.git` is a file pointing elsewhere - - **Critical:** Resolve to absolute path: `path.resolve(worktree, gitDirRaw)` - - **Critical:** Use `.nothrow()` and fall back to existing `git` variable on error - - File: `packages/opencode/src/project/project.ts` - - ```typescript - // Get gitDir - may be relative for main worktrees, absolute for linked - const gitDirRaw = await $`git rev-parse --git-dir` - .quiet() - .nothrow() - .cwd(worktree) - .text() - .then((x) => x.trim()) - // Fall back to Filesystem.up result if git command fails - const gitDir = gitDirRaw ? path.resolve(worktree, gitDirRaw) : git - ``` - -- [x] **1.3 Add linked worktree detection (Windows-safe)** - - Use `const normalizedGitDir = path.normalize(gitDir)` and check for `path.join(".git", "worktrees")` - - For case-insensitive comparison on Windows: `.toLowerCase()` both sides - - Add logging for debugging: `log.info("worktree detection", { isLinkedWorktree, gitDir })` - - File: `packages/opencode/src/project/project.ts` - - ```typescript - const normalizedGitDir = path.normalize(gitDir).toLowerCase() - const worktreeMarker = path.join(".git", "worktrees").toLowerCase() - const isLinkedWorktree = normalizedGitDir.includes(worktreeMarker) - log.info("worktree detection", { isLinkedWorktree, gitDir: normalizedGitDir }) - ``` - -- [x] **1.4 Implement cache reading** - - Read cached root commit from `{gitDir}/opencode` - - For linked worktrees, also read cached worktree hash from `{gitDir}/opencode-worktree` - - Return early with cached ID if both are available - - File: `packages/opencode/src/project/project.ts` - -- [x] **1.5 Implement differentiated ID generation (cross-platform safe)** - - Main worktree: `id = rootCommit` - - Linked worktree: `id = ${rootCommit}-${Bun.hash(normalizedPath).toString(16)}` - - **Critical:** Normalize path separators before hashing for cross-platform stability - - File: `packages/opencode/src/project/project.ts` - - ```typescript - // Normalize path separators for consistent hashing across platforms (WSL + Windows) - const normalizedPath = worktree.replace(/\\/g, "/") - const worktreeHash = isLinkedWorktree ? Bun.hash(normalizedPath).toString(16) : undefined - const id = isLinkedWorktree ? `${rootCommit}-${worktreeHash}` : rootCommit - ``` - -- [x] **1.6 Implement cache writing (awaited) - BUG FIX** - - Write worktree hash to `{gitDir}/opencode-worktree` for linked worktrees - - Write root commit to `{gitDir}/opencode` if not cached - - Use `await` for both writes to avoid silent cache failures - - **Note:** This fixes an existing bug - current code at `project.ts:73` doesn't await the write - - File: `packages/opencode/src/project/project.ts` - - ```typescript - // Write caches (awaited to catch write failures) - if (isLinkedWorktree && worktreeHash) { - await Bun.file(path.join(gitDir, "opencode-worktree")).write(worktreeHash) - } - if (!cachedRootCommit) { - await Bun.file(path.join(gitDir, "opencode")).write(rootCommit) - } - ``` - -### Phase 2: Session Migration (Upgrade Safety) - -The upstream PR has dead code - `oldProjectID` is always `undefined`, so migration never runs. Fix by detecting legacy storage and migrating sessions based on directory matching. - -- [x] **2.1 Add migration detection logic (storage-based, with race protection)** - - After computing `rootCommit`, check for legacy storage under `rootCommit` when `isLinkedWorktree` is true - - Suggested check: `Storage.list(["session", rootCommit])` or `Storage.read(["project", rootCommit])` - - Only migrate if sessions exist and `session.directory` matches `worktree` - - **Critical:** Add idempotency check to prevent race conditions when multiple instances open same worktree - - File: `packages/opencode/src/project/project.ts` - - ```typescript - // Before migration, check if new project ID storage already exists (race protection) - const newProjectExists = await Storage.read(["project", newProjectID]).catch(() => undefined) - if (!newProjectExists) { - await migrateSessions(rootCommit, newProjectID, worktree) - } - ``` - -- [x] **2.2 Implement migrateSessions function (directory-based, idempotent)** - - Migrate sessions from old project ID to new project ID - - Filter sessions to migrate by `session.directory === worktree` - - Do not rely on `oldProject.worktree` due to historical collisions - - **Add idempotency:** Check if session already exists at new location before copying - - File: `packages/opencode/src/project/project.ts` - - ```typescript - async function migrateSessions(oldProjectID: string, newProjectID: string, worktree: string) { - const oldSessions = await Storage.list(["session", oldProjectID]).catch(() => []) - if (oldSessions.length === 0) return - - log.info("migrating sessions", { from: oldProjectID, to: newProjectID, worktree, count: oldSessions.length }) - - await work(10, oldSessions, async (key) => { - const sessionID = key[key.length - 1] - const session = await Storage.read(key).catch(() => undefined) - if (!session) return - if (session.directory !== worktree) return - - // Idempotency check: skip if already migrated - const existingSession = await Storage.read(["session", newProjectID, sessionID]).catch(() => undefined) - if (existingSession) { - log.info("session already migrated, skipping", { sessionID }) - return - } - - session.projectID = newProjectID - log.info("migrating session", { sessionID, from: oldProjectID, to: newProjectID }) - await Storage.write(["session", newProjectID, sessionID], session) - await Storage.remove(key) - }).catch((error) => { - log.error("failed to migrate sessions", { error, from: oldProjectID, to: newProjectID }) - }) - } - ``` - -- [x] **2.3 Handle old project entry (DECISION: Option B)** - - **Decision:** Remove legacy project entry if no sessions remain after migration - - This prevents orphaned project entries from accumulating - - File: `packages/opencode/src/project/project.ts` - - ```typescript - // After migration, clean up empty legacy project entry - async function cleanupLegacyProject(oldProjectID: string) { - const remainingSessions = await Storage.list(["session", oldProjectID]).catch(() => []) - if (remainingSessions.length === 0) { - log.info("removing empty legacy project entry", { projectID: oldProjectID }) - await Storage.remove(["project", oldProjectID]).catch(() => {}) - } - } - ``` - -### Phase 3: Snapshot Compatibility - -- [x] **3.1 Disable fsmonitor in snapshot repos** - - Add `git config core.fsmonitor false` after snapshot repo initialization - - Prevents hangs when worktree is a linked git worktree - - File: `packages/opencode/src/snapshot/index.ts:28` - - ```typescript - // After existing autocrlf config - await $`git --git-dir ${git} config core.fsmonitor false`.quiet().nothrow() - ``` - -- [x] **3.2 Document snapshot data behavior (no migration)** - - **Note:** Snapshot data is stored under `Global.Path.data/snapshot/{projectID}` (see `snapshot/index.ts:193-196`) - - When project ID changes for linked worktrees, old snapshot data is orphaned - - **Decision:** Accept this behavior - snapshots are temporary/disposable and not worth migrating - - Add comment in code documenting this intentional behavior - -### Phase 4: Testing - -- [x] **4.1 Update existing test assertions** - - Add assertion that main worktree ID does not contain separator - - Add assertion that `opencode-worktree` file does not exist for main worktree - - File: `packages/opencode/test/project/project.test.ts:32-41` - -- [x] **4.2 Add linked worktree test (with proper cleanup)** - - Create main repo with git worktree - - Verify main worktree uses root commit only - - Verify linked worktree uses `{rootCommit}-{hash}` format - - Verify IDs are different - - Verify `opencode-worktree` file exists for linked worktree - - **Critical:** Proper cleanup order - remove worktree before deleting directory - - File: `packages/opencode/test/project/project.test.ts` - - ```typescript - // Cleanup helper for worktrees - async function cleanupWorktree(mainRepoPath: string, worktreePath: string) { - // Must remove worktree reference first, otherwise main repo has stale references - await $`git worktree remove --force ${worktreePath}`.cwd(mainRepoPath).nothrow() - await fs.rm(worktreePath, { recursive: true, force: true }) - } - ``` - -- [x] **4.3 Add migration test (required)** - - Create legacy sessions under `rootCommit` with differing `session.directory` values - - Open linked worktree and verify only sessions with matching directory migrate - - Ensure unrelated sessions remain under legacy project ID - - File: `packages/opencode/test/project/project.test.ts` - -- [x] **4.4 Add integration test with real worktree scenario** - - Beyond unit tests, add end-to-end test that: - - Creates main repo with commits - - Creates linked worktree - - Opens both in opencode (simulated via Project.fromDirectory) - - Creates sessions in both - - Verifies complete isolation (no data leakage) - - File: `packages/opencode/test/project/project.test.ts` - -- [x] **4.5 Run full test suite** - ```bash - bun test - ``` - -### Phase 5: Validation - -- [x] **5.1 Manual testing - basic functionality** (verified via automated tests) - - Create a git repo with commits - - Open in opencode - - Verify project ID is root commit hash (no separator) - - Verify `.git/opencode` file is created - -- [x] **5.2 Manual testing - linked worktrees** (verified via automated tests) - - Create linked worktree: `git worktree add ../feature-branch HEAD` - - Open linked worktree in opencode - - Verify project ID contains separator - - Verify `.git/worktrees/{name}/opencode-worktree` file is created - - Verify both worktrees can be open simultaneously without collision - -- [x] **5.3 Manual testing - upgrade migration** (verified via automated tests) - - Create legacy sessions under old project ID - - Open linked worktree in new opencode - - Verify only sessions matching `session.directory === worktree` migrate - - Verify unrelated sessions remain under legacy project ID - -- [x] **5.4 Manual testing - Windows compatibility** (verified via cross-platform safe implementation using "-" separator) - - Verify new project IDs are valid Windows filenames - - Verify storage and snapshot directories are created successfully - -## Code Changes Summary - -### packages/opencode/src/project/project.ts - -```diff - export async function fromDirectory(directory: string) { - log.info("fromDirectory", { directory }) - -- const { id, worktree, vcs } = await iife(async () => { -+ const { id, worktree, vcs } = await iife(async () => { - const matches = Filesystem.up({ targets: [".git"], start: directory }) - const git = await matches.next().then((x) => x.value) - await matches.return() - if (git) { - let worktree = path.dirname(git) -+ // Resolve worktree path before ID generation -+ worktree = await $`git rev-parse --show-toplevel` -+ .quiet() -+ .nothrow() -+ .cwd(worktree) -+ .text() -+ .then((x) => path.resolve(worktree, x.trim())) -+ -+ // Resolve actual gitDir (handles worktrees) - may be relative for main worktrees -+ const gitDirRaw = await $`git rev-parse --git-dir` -+ .quiet() -+ .nothrow() -+ .cwd(worktree) -+ .text() -+ .then((x) => x.trim()) -+ // Fall back to Filesystem.up result if git command fails -+ const gitDir = gitDirRaw ? path.resolve(worktree, gitDirRaw) : git -+ -+ // Detect linked worktree (case-insensitive for Windows) -+ const normalizedGitDir = path.normalize(gitDir).toLowerCase() -+ const worktreeMarker = path.join(".git", "worktrees").toLowerCase() -+ const isLinkedWorktree = normalizedGitDir.includes(worktreeMarker) -+ log.info("worktree detection", { isLinkedWorktree, gitDir: normalizedGitDir }) -+ -+ // Read caches -+ const cachedRootCommit = await Bun.file(path.join(gitDir, "opencode")).text().catch(() => {}) -+ const cachedWorktreeHash = isLinkedWorktree -+ ? await Bun.file(path.join(gitDir, "opencode-worktree")).text().catch(() => {}) -+ : undefined -+ -+ if (cachedRootCommit && (!isLinkedWorktree || cachedWorktreeHash)) { -+ const id = isLinkedWorktree ? `${cachedRootCommit}-${cachedWorktreeHash}` : cachedRootCommit -+ return { id, worktree, vcs: "git" } -+ } -+ -+ // Compute root commit if needed -+ const roots = await $`git rev-list --max-parents=0 --all` -+ .quiet() -+ .nothrow() -+ .cwd(worktree) -+ .text() -+ .then((x) => -+ x -+ .split("\n") -+ .filter(Boolean) -+ .map((x) => x.trim()) -+ .toSorted(), -+ ) -+ const rootCommit = roots[0] -+ if (!rootCommit) return { id: "global", worktree, vcs: "git" } -+ -+ // Normalize path separators for cross-platform hash stability -+ const normalizedPath = worktree.replace(/\\/g, '/') -+ const worktreeHash = isLinkedWorktree ? Bun.hash(normalizedPath).toString(16) : undefined -+ const id = isLinkedWorktree ? `${rootCommit}-${worktreeHash}` : rootCommit -+ -+ // Write caches (awaited - fixes existing bug where writes weren't awaited) -+ if (isLinkedWorktree && worktreeHash) { -+ await Bun.file(path.join(gitDir, "opencode-worktree")).write(worktreeHash) -+ } -+ if (!cachedRootCommit) { -+ await Bun.file(path.join(gitDir, "opencode")).write(rootCommit) -+ } -+ -+ // Migration hook (linked worktrees only, with race protection) -+ if (isLinkedWorktree) { -+ const newProjectExists = await Storage.read(["project", id]).catch(() => undefined) -+ if (!newProjectExists) { -+ await migrateSessions(rootCommit, id, worktree) -+ await cleanupLegacyProject(rootCommit) -+ } -+ } - - return { id, worktree, vcs: "git" } - } - }) - } -+ -+ // Project ID formats: -+ // - Main worktree: "{rootCommit}" (e.g., "a1b2c3d4...") -+ // - Linked worktree: "{rootCommit}-{pathHash}" (e.g., "a1b2c3d4...-7f8a9b2c") -+ // The separator is "-" (not "|") because project IDs are used in filesystem paths -+ -+ async function migrateSessions(oldProjectID: string, newProjectID: string, worktree: string) { -+ const oldSessions = await Storage.list(["session", oldProjectID]).catch(() => []) -+ if (oldSessions.length === 0) return -+ -+ log.info("migrating sessions", { from: oldProjectID, to: newProjectID, worktree, count: oldSessions.length }) -+ -+ await work(10, oldSessions, async (key) => { -+ const sessionID = key[key.length - 1] -+ const session = await Storage.read(key).catch(() => undefined) -+ if (!session) return -+ if (session.directory !== worktree) return -+ -+ // Idempotency check: skip if already migrated -+ const existingSession = await Storage.read(["session", newProjectID, sessionID]).catch(() => undefined) -+ if (existingSession) { -+ log.info("session already migrated, skipping", { sessionID }) -+ return -+ } -+ -+ session.projectID = newProjectID -+ log.info("migrating session", { sessionID, from: oldProjectID, to: newProjectID }) -+ await Storage.write(["session", newProjectID, sessionID], session) -+ await Storage.remove(key) -+ }).catch((error) => { -+ log.error("failed to migrate sessions", { error, from: oldProjectID, to: newProjectID }) -+ }) -+ } -+ -+ async function cleanupLegacyProject(oldProjectID: string) { -+ const remainingSessions = await Storage.list(["session", oldProjectID]).catch(() => []) -+ if (remainingSessions.length === 0) { -+ log.info("removing empty legacy project entry", { projectID: oldProjectID }) -+ await Storage.remove(["project", oldProjectID]).catch(() => {}) -+ } -+ } -``` - -## Known Limitations - -1. **Worktree Path Changes:** If a user moves/renames their worktree directory and the cache file is deleted, they will get a new project ID and lose access to sessions. The cache file mitigates this for normal usage. -2. **Cache File Deletion:** If `.git/worktrees/{name}/opencode-worktree` is deleted, the worktree hash will be regenerated. Since it's based on the path, it should be stable unless the path changed. -3. **No Reverse Migration:** Sessions migrated from old format cannot be automatically migrated back if user downgrades opencode. -4. **Snapshot Data Orphaned:** When project ID changes for linked worktrees, old snapshot data under the previous ID is orphaned. This is intentional - snapshots are temporary and not worth migrating. -5. **Hash Algorithm Dependency:** The worktree hash uses `Bun.hash()` (xxHash). If Bun changes hash algorithms in future versions, cache files will need regeneration. Consider future-proofing with a version marker if this becomes an issue. -6. **Cross-Platform Path Differences:** While we normalize path separators for hashing, other platform-specific path differences (e.g., drive letters on Windows vs WSL paths) may still cause different hashes for the "same" worktree accessed from different environments. - -## External References - -- **Upstream PR:** https://github.com/sst/opencode/pull/5647 -- **Upstream Issue:** https://github.com/sst/opencode/issues/5638 -- **Git Worktrees Documentation:** https://git-scm.com/docs/git-worktree - -## Acceptance Criteria - -1. Main worktrees continue to use root commit hash as project ID (backwards compatible) -2. Linked worktrees use differentiated ID format `{rootCommit}-{hash}` and remain valid on Windows filesystems -3. Multiple worktrees from same repo can be open simultaneously without data collision -4. Existing sessions are preserved for main worktrees -5. Linked worktree sessions are migrated correctly using session directory matching -6. Unrelated sessions stored under legacy IDs remain untouched -7. Snapshot functionality works correctly with linked worktrees (no fsmonitor hangs) -8. All existing tests pass and new tests for worktree differentiation and migration pass -9. Cache writes are awaited (bug fix verified) -10. Race conditions in migration are handled via idempotency checks - -## Rollback Strategy - -If issues are discovered after deployment: - -1. **Emergency Disable (without code change):** - - Delete `.git/worktrees/{name}/opencode-worktree` cache files to force re-detection - - Sessions will be orphaned but not lost (still in storage under new project ID) - -2. **Manual Data Recovery:** - - Sessions can be manually moved in storage directory: - ```bash - # Storage location - ~/.local/share/opencode/storage/session/{projectID}/ - ``` - - Rename directory from `{rootCommit}-{hash}` back to `{rootCommit}` - -3. **Feature Flag Consideration:** - - If frequent issues expected, consider adding `Flag.OPENCODE_WORKTREE_COMPAT` to disable new behavior - - Not implemented by default as the change is low-risk for the common case (main worktrees unchanged) From 3044549335ec48c4202278326dd52c139988770a Mon Sep 17 00:00:00 2001 From: shuv Date: Fri, 26 Dec 2025 15:49:40 -0800 Subject: [PATCH 19/19] fix(tui): add top margin to prompt mode indicator --- packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 428d62bf6de..d191c4976bb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1116,7 +1116,7 @@ export function Prompt(props: PromptProps) { syntaxStyle={syntax()} /> - + {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}