From 32f9dc6383aa4ae55c78979ecbff2d9404b623da Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:53:12 +0000 Subject: [PATCH 01/27] fix(ui): stop auto close of sidebar on resize (#18647) --- packages/app/src/pages/layout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2adcd3b563..0c10cc89bc 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -2368,14 +2368,12 @@ export default function Layout(props: ParentProps) { size={layout.sidebar.width()} min={244} max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64} - collapseThreshold={244} onResize={(w) => { setState("sizing", true) if (sizet !== undefined) clearTimeout(sizet) sizet = window.setTimeout(() => setState("sizing", false), 120) layout.sidebar.resize(w) }} - onCollapse={layout.sidebar.close} /> From e2d03ce38c9bae484bc1592238e0c88e8ffd90bb Mon Sep 17 00:00:00 2001 From: Dax Date: Sun, 22 Mar 2026 19:12:40 -0400 Subject: [PATCH 02/27] feat: interactive update flow for non-patch releases (#18662) --- packages/opencode/git | 0 packages/opencode/src/cli/cmd/tui/app.tsx | 50 ++++- .../src/cli/cmd/tui/ui/dialog-confirm.tsx | 14 +- packages/opencode/src/cli/upgrade.ts | 12 +- packages/opencode/src/flag/flag.ts | 1 + packages/opencode/src/installation/index.ts | 15 ++ packages/opencode/src/server/routes/global.ts | 60 +++++- packages/sdk/js/src/v2/gen/sdk.gen.ts | 126 ++++++++----- packages/sdk/js/src/v2/gen/types.gen.ts | 171 +++++++++++------- 9 files changed, 317 insertions(+), 132 deletions(-) create mode 100644 packages/opencode/git diff --git a/packages/opencode/git b/packages/opencode/git new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 8bb17ff133..dc052c4d2e 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -5,8 +5,8 @@ import { MouseButton, TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32" -import { Installation } from "@/installation" import { Flag } from "@/flag/flag" +import semver from "semver" import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" import { SDKProvider, useSDK } from "@tui/context/sdk" @@ -29,6 +29,7 @@ import { PromptHistoryProvider } from "./component/prompt/history" import { FrecencyProvider } from "./component/prompt/frecency" import { PromptStashProvider } from "./component/prompt/stash" import { DialogAlert } from "./ui/dialog-alert" +import { DialogConfirm } from "./ui/dialog-confirm" import { ToastProvider, useToast } from "./ui/toast" import { ExitProvider, useExit } from "./context/exit" import { Session as SessionApi } from "@/session" @@ -103,6 +104,7 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { } import type { EventSource } from "./context/sdk" +import { Installation } from "@/installation" export function tui(input: { url: string @@ -729,13 +731,51 @@ function App() { }) }) - sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => { + sdk.event.on("installation.update-available", async (evt) => { + const version = evt.properties.version + + const skipped = kv.get("skipped_version") + if (skipped && !semver.gt(version, skipped)) return + + const choice = await DialogConfirm.show( + dialog, + `Update Available`, + `A new release v${version} is available. Would you like to update now?`, + "skip", + ) + + if (choice === false) { + kv.set("skipped_version", version) + return + } + + if (choice !== true) return + toast.show({ variant: "info", - title: "Update Available", - message: `OpenCode v${evt.properties.version} is available. Run 'opencode upgrade' to update manually.`, - duration: 10000, + message: `Updating to v${version}...`, + duration: 30000, }) + + const result = await sdk.client.global.upgrade({ target: version }) + + if (result.error || !result.data?.success) { + toast.show({ + variant: "error", + title: "Update Failed", + message: "Update failed", + duration: 10000, + }) + return + } + + await DialogAlert.show( + dialog, + "Update Complete", + `Successfully updated to OpenCode v${result.data.version}. Please restart the application.`, + ) + + exit() }) return ( diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index b86bd43251..ef75764a29 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -11,8 +11,11 @@ export type DialogConfirmProps = { message: string onConfirm?: () => void onCancel?: () => void + label?: string } +export type DialogConfirmResult = boolean | undefined + export function DialogConfirm(props: DialogConfirmProps) { const dialog = useDialog() const { theme } = useTheme() @@ -45,7 +48,7 @@ export function DialogConfirm(props: DialogConfirmProps) { {props.message} - + {(key) => ( - {Locale.titlecase(key)} + {Locale.titlecase(key === "cancel" ? (props.label ?? key) : key)} )} @@ -68,8 +71,8 @@ export function DialogConfirm(props: DialogConfirmProps) { ) } -DialogConfirm.show = (dialog: DialogContext, title: string, message: string) => { - return new Promise((resolve) => { +DialogConfirm.show = (dialog: DialogContext, title: string, message: string, label?: string) => { + return new Promise((resolve) => { dialog.replace( () => ( message={message} onConfirm={() => resolve(true)} onCancel={() => resolve(false)} + label={label} /> ), - () => resolve(false), + () => resolve(undefined), ) }) } diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index 2d46ae39fa..e40750a2ec 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -8,12 +8,18 @@ export async function upgrade() { const method = await Installation.method() const latest = await Installation.latest(method).catch(() => {}) if (!latest) return - if (Installation.VERSION === latest) return - if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) { + if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) { + await Bus.publish(Installation.Event.UpdateAvailable, { version: latest }) return } - if (config.autoupdate === "notify") { + + if (Installation.VERSION === latest) return + if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return + + const kind = Installation.getReleaseType(Installation.VERSION, latest) + + if (config.autoupdate === "notify" || kind !== "patch") { await Bus.publish(Installation.Event.UpdateAvailable, { version: latest }) return } diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 05f04c85ce..0c55187b9d 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -18,6 +18,7 @@ export namespace Flag { export declare const OPENCODE_CONFIG_DIR: string | undefined export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"] export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE") + export const OPENCODE_ALWAYS_NOTIFY_UPDATE = truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE") export const OPENCODE_DISABLE_PRUNE = truthy("OPENCODE_DISABLE_PRUNE") export const OPENCODE_DISABLE_TERMINAL_TITLE = truthy("OPENCODE_DISABLE_TERMINAL_TITLE") export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"] diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 1e4e45f2cd..3551c861e4 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -15,11 +15,15 @@ declare global { const OPENCODE_CHANNEL: string } +import semver from "semver" + export namespace Installation { const log = Log.create({ service: "installation" }) export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown" + export type ReleaseType = "patch" | "minor" | "major" + export const Event = { Updated: BusEvent.define( "installation.updated", @@ -35,6 +39,17 @@ export namespace Installation { ), } + export function getReleaseType(current: string, latest: string): ReleaseType { + const currMajor = semver.major(current) + const currMinor = semver.minor(current) + const newMajor = semver.major(latest) + const newMinor = semver.minor(latest) + + if (newMajor > currMajor) return "major" + if (newMinor > currMinor) return "minor" + return "patch" + } + export const Info = z .object({ version: z.string(), diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index 4a6a3ebc7e..4dd30db2af 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -1,7 +1,8 @@ import { Hono } from "hono" -import { describeRoute, resolver, validator } from "hono-openapi" +import { describeRoute, validator, resolver } from "hono-openapi" import { streamSSE } from "hono/streaming" import z from "zod" +import { Bus } from "../../bus" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { AsyncQueue } from "@/util/queue" @@ -195,5 +196,62 @@ export const GlobalRoutes = lazy(() => }) return c.json(true) }, + ) + .post( + "/upgrade", + describeRoute({ + summary: "Upgrade opencode", + description: "Upgrade opencode to the specified version or latest if not specified.", + operationId: "global.upgrade", + responses: { + 200: { + description: "Upgrade result", + content: { + "application/json": { + schema: resolver( + z.union([ + z.object({ + success: z.literal(true), + version: z.string(), + }), + z.object({ + success: z.literal(false), + error: z.string(), + }), + ]), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.object({ + target: z.string().optional(), + }), + ), + async (c) => { + const method = await Installation.method() + if (method === "unknown") { + return c.json({ success: false, error: "Unknown installation method" }, 400) + } + const target = c.req.valid("json").target || (await Installation.latest(method)) + const result = await Installation.upgrade(method, target) + .then(() => ({ success: true as const, version: target })) + .catch((e) => ({ success: false as const, error: e instanceof Error ? e.message : String(e) })) + if (result.success) { + GlobalBus.emit("event", { + directory: "global", + payload: { + type: Installation.Event.Updated.type, + properties: { version: target }, + }, + }) + return c.json(result) + } + return c.json(result, 500) + }, ), ) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b6821322e2..f5e22ebfbe 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -46,6 +46,8 @@ import type { GlobalDisposeResponses, GlobalEventResponses, GlobalHealthResponses, + GlobalUpgradeErrors, + GlobalUpgradeResponses, InstanceDisposeResponses, LspStatusResponses, McpAddErrors, @@ -228,6 +230,62 @@ class HeyApiRegistry { } } +export class Auth extends HeyApiClient { + /** + * Remove auth credentials + * + * Remove authentication credentials + */ + public remove( + parameters: { + providerID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) + return (options?.client ?? this.client).delete({ + url: "/auth/{providerID}", + ...options, + ...params, + }) + } + + /** + * Set auth credentials + * + * Set authentication credentials + */ + public set( + parameters: { + providerID: string + auth?: Auth3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { key: "auth", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/auth/{providerID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Config extends HeyApiClient { /** * Get global configuration @@ -303,57 +361,20 @@ export class Global extends HeyApiClient { }) } - private _config?: Config - get config(): Config { - return (this._config ??= new Config({ client: this.client })) - } -} - -export class Auth extends HeyApiClient { /** - * Remove auth credentials + * Upgrade opencode * - * Remove authentication credentials + * Upgrade opencode to the specified version or latest if not specified. */ - public remove( - parameters: { - providerID: string + public upgrade( + parameters?: { + target?: string }, options?: Options, ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) - return (options?.client ?? this.client).delete({ - url: "/auth/{providerID}", - ...options, - ...params, - }) - } - - /** - * Set auth credentials - * - * Set authentication credentials - */ - public set( - parameters: { - providerID: string - auth?: Auth3 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { key: "auth", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put({ - url: "/auth/{providerID}", + const params = buildClientParams([parameters], [{ args: [{ in: "body", key: "target" }] }]) + return (options?.client ?? this.client).post({ + url: "/global/upgrade", ...options, ...params, headers: { @@ -363,6 +384,11 @@ export class Auth extends HeyApiClient { }, }) } + + private _config?: Config + get config(): Config { + return (this._config ??= new Config({ client: this.client })) + } } export class Project extends HeyApiClient { @@ -3906,16 +3932,16 @@ export class OpencodeClient extends HeyApiClient { OpencodeClient.__registry.set(this, args?.key) } - private _global?: Global - get global(): Global { - return (this._global ??= new Global({ client: this.client })) - } - private _auth?: Auth get auth(): Auth { return (this._auth ??= new Auth({ client: this.client })) } + private _global?: Global + get global(): Global { + return (this._global ??= new Global({ client: this.client })) + } + private _project?: Project get project(): Project { return (this._project ??= new Project({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index f7aab687e6..d284234cc7 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4,6 +4,36 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}) } +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + +export type OAuth = { + type: "oauth" + refresh: string + access: string + expires: number + accountId?: string + enterpriseUrl?: string +} + +export type ApiAuth = { + type: "api" + key: string +} + +export type WellKnownAuth = { + type: "wellknown" + key: string + token: string +} + +export type Auth = OAuth | ApiAuth | WellKnownAuth + export type EventInstallationUpdated = { type: "installation.updated" properties: { @@ -1506,36 +1536,6 @@ export type Config = { } } -export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false -} - -export type OAuth = { - type: "oauth" - refresh: string - access: string - expires: number - accountId?: string - enterpriseUrl?: string -} - -export type ApiAuth = { - type: "api" - key: string -} - -export type WellKnownAuth = { - type: "wellknown" - key: string - token: string -} - -export type Auth = OAuth | ApiAuth | WellKnownAuth - export type NotFoundError = { name: "NotFoundError" data: { @@ -1938,6 +1938,60 @@ export type FormatterStatus = { enabled: boolean } +export type AuthRemoveData = { + body?: never + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] + +export type AuthRemoveResponses = { + /** + * Successfully removed authentication credentials + */ + 200: boolean +} + +export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] + +export type AuthSetData = { + body?: Auth + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthSetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] + +export type AuthSetResponses = { + /** + * Successfully set authentication credentials + */ + 200: boolean +} + +export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] + export type GlobalHealthData = { body?: never path?: never @@ -2030,59 +2084,40 @@ export type GlobalDisposeResponses = { export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses] -export type AuthRemoveData = { - body?: never - path: { - providerID: string +export type GlobalUpgradeData = { + body?: { + target?: string } + path?: never query?: never - url: "/auth/{providerID}" + url: "/global/upgrade" } -export type AuthRemoveErrors = { +export type GlobalUpgradeErrors = { /** * Bad request */ 400: BadRequestError } -export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] +export type GlobalUpgradeError = GlobalUpgradeErrors[keyof GlobalUpgradeErrors] -export type AuthRemoveResponses = { +export type GlobalUpgradeResponses = { /** - * Successfully removed authentication credentials + * Upgrade result */ - 200: boolean + 200: + | { + success: true + version: string + } + | { + success: false + error: string + } } -export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] - -export type AuthSetData = { - body?: Auth - path: { - providerID: string - } - query?: never - url: "/auth/{providerID}" -} - -export type AuthSetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] - -export type AuthSetResponses = { - /** - * Successfully set authentication credentials - */ - 200: boolean -} - -export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] +export type GlobalUpgradeResponse = GlobalUpgradeResponses[keyof GlobalUpgradeResponses] export type ProjectListData = { body?: never From eb3bfffad453f1c8c3f0f92bba0d8e34c83fa244 Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 22 Mar 2026 23:32:01 +0000 Subject: [PATCH 03/27] release: v1.3.0 --- bun.lock | 32 ++--- 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-electron/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 | 2 +- packages/sdk/js/package.json | 2 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 122 ++++++++--------- packages/sdk/js/src/v2/gen/types.gen.ts | 168 ++++++++++++------------ 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 +- 21 files changed, 184 insertions(+), 184 deletions(-) diff --git a/bun.lock b/bun.lock index c44ad8de97..7dc8ad51f8 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -78,7 +78,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -112,7 +112,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -139,7 +139,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -163,7 +163,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -187,7 +187,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -220,7 +220,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -251,7 +251,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -280,7 +280,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -296,7 +296,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.27", + "version": "1.3.0", "bin": { "opencode": "./bin/opencode", }, @@ -420,7 +420,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -444,7 +444,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.27", + "version": "1.3.0", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -455,7 +455,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -490,7 +490,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -536,7 +536,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "zod": "catalog:", }, @@ -547,7 +547,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.27", + "version": "1.3.0", "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 3f4e2472f2..8181825c06 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.27", + "version": "1.3.0", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index cb0f91a64f..b90d77f405 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 1a75caea2f..47d218edd8 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.2.27", + "version": "1.3.0", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index fe327a5639..93e0ba71cb 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.27", + "version": "1.3.0", "$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 0525ffc21c..e0c677446c 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index bcb80d9e69..b7872acc98 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 73ec5278f6..c98e037be6 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index e61c75d0ec..724cc59493 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.27", + "version": "1.3.0", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 654e1dce75..9842aa49be 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.2.27" +version = "1.3.0" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index a20a61f74d..e8651be720 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.27", + "version": "1.3.0", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 824c3409c4..7b765e1cc2 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.27", + "version": "1.3.0", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 5dedb464c4..6907f2a332 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.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 8ae3dae9c3..bcc035d4bb 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.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f5e22ebfbe..7a4f4e40cf 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -230,62 +230,6 @@ class HeyApiRegistry { } } -export class Auth extends HeyApiClient { - /** - * Remove auth credentials - * - * Remove authentication credentials - */ - public remove( - parameters: { - providerID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) - return (options?.client ?? this.client).delete({ - url: "/auth/{providerID}", - ...options, - ...params, - }) - } - - /** - * Set auth credentials - * - * Set authentication credentials - */ - public set( - parameters: { - providerID: string - auth?: Auth3 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { key: "auth", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put({ - url: "/auth/{providerID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - export class Config extends HeyApiClient { /** * Get global configuration @@ -391,6 +335,62 @@ export class Global extends HeyApiClient { } } +export class Auth extends HeyApiClient { + /** + * Remove auth credentials + * + * Remove authentication credentials + */ + public remove( + parameters: { + providerID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) + return (options?.client ?? this.client).delete({ + url: "/auth/{providerID}", + ...options, + ...params, + }) + } + + /** + * Set auth credentials + * + * Set authentication credentials + */ + public set( + parameters: { + providerID: string + auth?: Auth3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { key: "auth", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/auth/{providerID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Project extends HeyApiClient { /** * List all projects @@ -3932,16 +3932,16 @@ export class OpencodeClient extends HeyApiClient { OpencodeClient.__registry.set(this, args?.key) } - private _auth?: Auth - get auth(): Auth { - return (this._auth ??= new Auth({ client: this.client })) - } - private _global?: Global get global(): Global { return (this._global ??= new Global({ client: this.client })) } + private _auth?: Auth + get auth(): Auth { + return (this._auth ??= new Auth({ client: this.client })) + } + private _project?: Project get project(): Project { return (this._project ??= new Project({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index d284234cc7..86a0c7e425 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4,36 +4,6 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}) } -export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false -} - -export type OAuth = { - type: "oauth" - refresh: string - access: string - expires: number - accountId?: string - enterpriseUrl?: string -} - -export type ApiAuth = { - type: "api" - key: string -} - -export type WellKnownAuth = { - type: "wellknown" - key: string - token: string -} - -export type Auth = OAuth | ApiAuth | WellKnownAuth - export type EventInstallationUpdated = { type: "installation.updated" properties: { @@ -1536,6 +1506,36 @@ export type Config = { } } +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + +export type OAuth = { + type: "oauth" + refresh: string + access: string + expires: number + accountId?: string + enterpriseUrl?: string +} + +export type ApiAuth = { + type: "api" + key: string +} + +export type WellKnownAuth = { + type: "wellknown" + key: string + token: string +} + +export type Auth = OAuth | ApiAuth | WellKnownAuth + export type NotFoundError = { name: "NotFoundError" data: { @@ -1938,60 +1938,6 @@ export type FormatterStatus = { enabled: boolean } -export type AuthRemoveData = { - body?: never - path: { - providerID: string - } - query?: never - url: "/auth/{providerID}" -} - -export type AuthRemoveErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] - -export type AuthRemoveResponses = { - /** - * Successfully removed authentication credentials - */ - 200: boolean -} - -export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] - -export type AuthSetData = { - body?: Auth - path: { - providerID: string - } - query?: never - url: "/auth/{providerID}" -} - -export type AuthSetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] - -export type AuthSetResponses = { - /** - * Successfully set authentication credentials - */ - 200: boolean -} - -export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] - export type GlobalHealthData = { body?: never path?: never @@ -2119,6 +2065,60 @@ export type GlobalUpgradeResponses = { export type GlobalUpgradeResponse = GlobalUpgradeResponses[keyof GlobalUpgradeResponses] +export type AuthRemoveData = { + body?: never + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] + +export type AuthRemoveResponses = { + /** + * Successfully removed authentication credentials + */ + 200: boolean +} + +export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] + +export type AuthSetData = { + body?: Auth + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthSetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] + +export type AuthSetResponses = { + /** + * Successfully set authentication credentials + */ + 200: boolean +} + +export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] + export type ProjectListData = { body?: never path?: never diff --git a/packages/slack/package.json b/packages/slack/package.json index a29ffc4000..732b452280 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 3f42296889..bb907a4a64 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index b48b755f3c..0f6a3c31ff 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.27", + "version": "1.3.0", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 6a2e48f7d3..051ae6402d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.27", + "version": "1.3.0", "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 2adf05f419..ee9307de1a 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.2.27", + "version": "1.3.0", "publisher": "sst-dev", "repository": { "type": "git", From 5460bf9989110fa8937619664c8734da90dcba31 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 22 Mar 2026 23:13:51 +0000 Subject: [PATCH 04/27] chore: generate --- packages/sdk/openapi.json | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 9f3a69c54c..a66ef63647 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -158,6 +158,82 @@ ] } }, + "/global/upgrade": { + "post": { + "operationId": "global.upgrade", + "summary": "Upgrade opencode", + "description": "Upgrade opencode to the specified version or latest if not specified.", + "responses": { + "200": { + "description": "Upgrade result", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": true + }, + "version": { + "type": "string" + } + }, + "required": ["success", "version"] + }, + { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": false + }, + "error": { + "type": "string" + } + }, + "required": ["success", "error"] + } + ] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "target": { + "type": "string" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.upgrade({\n ...\n})" + } + ] + } + }, "/auth/{providerID}": { "put": { "operationId": "auth.set", From 0d6c60136562cca785b428aed446428d61f42616 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 22 Mar 2026 19:45:23 -0400 Subject: [PATCH 05/27] changelog slash command --- .opencode/command/changelog.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .opencode/command/changelog.md diff --git a/.opencode/command/changelog.md b/.opencode/command/changelog.md new file mode 100644 index 0000000000..8a8e57d1bf --- /dev/null +++ b/.opencode/command/changelog.md @@ -0,0 +1,5 @@ +go through each PR merged since the last tag + +for each PR spawn a subagent to summarize what the PR was about. focus on user facing changes. if it was entirely internal or code related you can ignore it. also skip docs updates. each subagent should append its summary to UPCOMING_CHANGELOG.md + +once that is done, read UPCOMING_CHANGELOG.md and group it into sections for better readability. make sure all PR references are preserved From 8dd817023a2e5798fe977f25d18478995380d347 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:19:21 +1000 Subject: [PATCH 06/27] chore: bump Bun to 1.3.11 (#18144) --- bun.lock | 8 ++++---- package.json | 4 ++-- packages/console/core/package.json | 2 +- packages/containers/bun-node/Dockerfile | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bun.lock b/bun.lock index 7dc8ad51f8..3497e146c0 100644 --- a/bun.lock +++ b/bun.lock @@ -129,7 +129,7 @@ "devDependencies": { "@cloudflare/workers-types": "catalog:", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.0", + "@types/bun": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "drizzle-kit": "catalog:", @@ -611,7 +611,7 @@ "@tailwindcss/vite": "4.1.11", "@tsconfig/bun": "1.0.9", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.9", + "@types/bun": "1.3.11", "@types/luxon": "3.7.1", "@types/node": "22.13.9", "@types/semver": "7.7.1", @@ -2057,7 +2057,7 @@ "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], @@ -2453,7 +2453,7 @@ "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], - "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="], diff --git a/package.json b/package.json index 7ce06896ad..915e2ef0ac 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.10", + "packageManager": "bun@1.3.11", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev:desktop": "bun --cwd packages/desktop tauri dev", @@ -26,7 +26,7 @@ ], "catalog": { "@effect/platform-node": "4.0.0-beta.35", - "@types/bun": "1.3.9", + "@types/bun": "1.3.11", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", "ulid": "3.0.1", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 47d218edd8..f2bb6ac745 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -42,7 +42,7 @@ "devDependencies": { "@cloudflare/workers-types": "catalog:", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.0", + "@types/bun": "catalog:", "@types/node": "catalog:", "drizzle-kit": "catalog:", "mysql2": "3.14.4", diff --git a/packages/containers/bun-node/Dockerfile b/packages/containers/bun-node/Dockerfile index e6cad9c272..485375dd9f 100644 --- a/packages/containers/bun-node/Dockerfile +++ b/packages/containers/bun-node/Dockerfile @@ -4,7 +4,7 @@ FROM ${REGISTRY}/build/base:24.04 SHELL ["/bin/bash", "-lc"] ARG NODE_VERSION=24.4.0 -ARG BUN_VERSION=1.3.5 +ARG BUN_VERSION=1.3.11 ENV BUN_INSTALL=/opt/bun ENV PATH=/opt/bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin From 2b171828b0365e8a3cc25cc89de30a740132b947 Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:20:49 +0000 Subject: [PATCH 07/27] tui: prevent project avatar popover flicker when switching projects (#18660) Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> --- .../sidebar/sidebar-popover-actions.spec.ts | 43 ++++++++++++++++++- .../app/src/pages/layout/sidebar-project.tsx | 9 +++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts index d10fca0e49..4dc5e6acdc 100644 --- a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts +++ b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts @@ -1,6 +1,15 @@ import { test, expect } from "../fixtures" -import { cleanupSession, closeSidebar, hoverSessionItem } from "../actions" +import { + cleanupSession, + cleanupTestProject, + closeSidebar, + createTestProject, + hoverSessionItem, + openSidebar, + waitSession, +} from "../actions" import { projectSwitchSelector } from "../selectors" +import { dirSlug } from "../utils" test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => { const stamp = Date.now() @@ -37,3 +46,35 @@ test("collapsed sidebar popover stays open when archiving a session", async ({ p await cleanupSession({ sdk, sessionID: two.id }) } }) + +test("open sidebar project popover stays closed after clicking avatar", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const other = await createTestProject() + const slug = dirSlug(other) + + try { + await withProject( + async () => { + await openSidebar(page) + + const project = page.locator(projectSwitchSelector(slug)).first() + const card = page.locator('[data-component="hover-card-content"]') + + await expect(project).toBeVisible() + await project.hover() + await expect(card.getByText(/recent sessions/i)).toBeVisible() + + await page.mouse.down() + await expect(card).toHaveCount(0) + await page.mouse.up() + + await waitSession(page, { directory: other }) + await expect(card).toHaveCount(0) + }, + { extra: [other] }, + ) + } finally { + await cleanupTestProject(other) + } +}) diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index 2528264561..99f1edb74d 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -109,8 +109,14 @@ const ProjectTile = (props: { "bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(), }} onPointerDown={(event) => { + if (event.button === 0 && !event.ctrlKey) { + props.setOpen(false) + props.setSuppressHover(true) + return + } if (!props.overlay()) return if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return + props.setOpen(false) props.setSuppressHover(true) event.preventDefault() }} @@ -130,12 +136,11 @@ const ProjectTile = (props: { props.onProjectFocus(props.project.worktree) }} onClick={() => { + props.setOpen(false) if (props.selected()) { - props.setSuppressHover(true) layout.sidebar.toggle() return } - props.setSuppressHover(false) props.navigateToProject(props.project.worktree) }} onBlur={() => props.setOpen(false)} From e9a9c75c1f772e4d7fe8aaececddde46a978df9d Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:23:45 +0000 Subject: [PATCH 08/27] tweak(ui): fix padding bottom on the context tab (#18680) --- packages/app/src/components/session/session-context-tab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 9aa101bdb9..4d90930a0e 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -267,14 +267,14 @@ export function SessionContextTab() { return ( { scroll = el restoreScroll() }} onScroll={handleScroll} > -
+
{(stat) => [0])} value={stat.value()} />} From 3b3549902deadf981195da261365af61389da0e1 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 23 Mar 2026 00:29:45 +0000 Subject: [PATCH 09/27] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 53b17622ef..9b6094b354 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-u+uZX7mhtm5eywGybB7/MjBMG2xl4Ve9VG33AAFgNno=", - "aarch64-linux": "sha256-pc1Xhd2bkwNohGMtzRnEuS5ZN1qWhJncYhNVAXega1g=", - "aarch64-darwin": "sha256-A5qUpqgm9ZFvWVhn/WdiX4lVs4ihbAclJDvCFAmx5Wg=", - "x86_64-darwin": "sha256-ECLrMGE51AlYJ4JKDtziDKxhyK7WLt8R+8RVFdXH1WU=" + "x86_64-linux": "sha256-BPuw6KcVksnES59psBplMjMkj1tG/Rd6OLLtqAis81E=", + "aarch64-linux": "sha256-b/rgEy0YsS8OH13nRuOeun/+O0aVTDuMv4oJAMMXxm0=", + "aarch64-darwin": "sha256-d5PE/ejwpQlk4HmuQ4+Umn4Hf7VuhU76We+XTtA3SEY=", + "x86_64-darwin": "sha256-kLqo9lEEfPS7UuCXxfEFcTM+vnRgKCyqlW1jq0AotMQ=" } } From afe9b9727415ea046dc08990f981e00e27ec4a43 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:39:46 +1000 Subject: [PATCH 10/27] fix(app): restore keyboard project switching in open sidebar (#18682) --- .../sidebar/sidebar-popover-actions.spec.ts | 38 +++++++++++++++++++ packages/ui/src/components/hover-card.tsx | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts index 4dc5e6acdc..1317d2bb68 100644 --- a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts +++ b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "../fixtures" import { + defocus, cleanupSession, cleanupTestProject, closeSidebar, @@ -78,3 +79,40 @@ test("open sidebar project popover stays closed after clicking avatar", async ({ await cleanupTestProject(other) } }) + +test("open sidebar project switch activates on first tabbed enter", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const other = await createTestProject() + const slug = dirSlug(other) + + try { + await withProject( + async () => { + await openSidebar(page) + await defocus(page) + + const project = page.locator(projectSwitchSelector(slug)).first() + + await expect(project).toBeVisible() + + let hit = false + for (let i = 0; i < 20; i++) { + hit = await project.evaluate((el) => { + return el.matches(":focus") || !!el.parentElement?.matches(":focus") + }) + if (hit) break + await page.keyboard.press("Tab") + } + + expect(hit).toBe(true) + + await page.keyboard.press("Enter") + await waitSession(page, { directory: other }) + }, + { extra: [other] }, + ) + } finally { + await cleanupTestProject(other) + } +}) diff --git a/packages/ui/src/components/hover-card.tsx b/packages/ui/src/components/hover-card.tsx index 210fd54160..8330375aa3 100644 --- a/packages/ui/src/components/hover-card.tsx +++ b/packages/ui/src/components/hover-card.tsx @@ -13,7 +13,7 @@ export function HoverCard(props: HoverCardProps) { return ( - + {local.trigger} From 40e49c5b49ad017e3bfcd53f7bc1631d6cdd5c0d Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:45:11 +0000 Subject: [PATCH 11/27] tui: keep patch tool counts visible with long filenames (#18678) --- packages/ui/src/components/basic-tool.css | 15 ++++++++++++++- packages/ui/src/components/basic-tool.tsx | 4 +++- packages/ui/src/components/message-part.css | 18 +++++++++++++++--- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/basic-tool.css b/packages/ui/src/components/basic-tool.css index 1dbfce26ec..f52a5e5762 100644 --- a/packages/ui/src/components/basic-tool.css +++ b/packages/ui/src/components/basic-tool.css @@ -8,7 +8,10 @@ justify-content: flex-start; [data-slot="basic-tool-tool-trigger-content"] { + flex: 0 1 auto; width: auto; + max-width: calc(100% - 24px); + min-width: 0; display: flex; align-items: center; align-self: stretch; @@ -51,12 +54,16 @@ [data-slot="basic-tool-tool-info"] { flex: 0 1 auto; min-width: 0; + max-width: 100%; font-size: 14px; } [data-slot="basic-tool-tool-info-structured"] { + flex: 0 1 auto; width: auto; - display: flex; + max-width: 100%; + min-width: 0; + display: inline-flex; align-items: center; gap: 8px; justify-content: flex-start; @@ -151,4 +158,10 @@ letter-spacing: var(--letter-spacing-normal); color: var(--text-base); } + + [data-slot="basic-tool-tool-action"] { + display: inline-flex; + align-items: center; + flex-shrink: 0; + } } diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index 0b2c1e1ce4..a02fe941b1 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -174,7 +174,9 @@ export function BasicTool(props: BasicToolProps) {
- {trigger().action} + + {trigger().action} +
)} diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 8031bf2631..aa685392a9 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -424,7 +424,7 @@ display: flex; align-items: center; justify-content: space-between; - gap: 8px; + gap: 12px; width: 100%; [data-slot="message-part-title-area"] { @@ -436,10 +436,11 @@ } [data-slot="message-part-title"] { - flex-shrink: 0; + flex: 1 1 auto; display: flex; align-items: center; gap: 8px; + min-width: 0; font-family: var(--font-family-sans); font-size: 14px; font-style: normal; @@ -466,12 +467,17 @@ } [data-slot="message-part-title-text"] { + flex-shrink: 0; text-transform: capitalize; color: var(--text-strong); } [data-slot="message-part-title-filename"] { /* No text-transform - preserve original filename casing */ + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-weight: var(--font-weight-regular); } @@ -501,6 +507,7 @@ gap: 16px; align-items: center; justify-content: flex-end; + flex-shrink: 0; } } @@ -1183,6 +1190,7 @@ display: flex; flex-grow: 1; min-width: 0; + overflow: hidden; } [data-slot="apply-patch-directory"] { @@ -1196,7 +1204,11 @@ [data-slot="apply-patch-filename"] { color: var(--text-strong); - flex-shrink: 0; + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } [data-slot="apply-patch-trigger-actions"] { From 71e7603d7171bfcfd2cbe297cf7b6754e67113fe Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 23 Mar 2026 01:45:34 +0100 Subject: [PATCH 12/27] Upgrade opentui to 0.1.90 (#18551) --- bun.lock | 20 +++++++++---------- packages/opencode/package.json | 4 ++-- .../src/cli/cmd/tui/context/theme.tsx | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 2 ++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index 3497e146c0..6116d26fa0 100644 --- a/bun.lock +++ b/bun.lock @@ -337,8 +337,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.88", - "@opentui/solid": "0.1.88", + "@opentui/core": "0.1.90", + "@opentui/solid": "0.1.90", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -1447,21 +1447,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.88", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.88", "@opentui/core-darwin-x64": "0.1.88", "@opentui/core-linux-arm64": "0.1.88", "@opentui/core-linux-x64": "0.1.88", "@opentui/core-win32-arm64": "0.1.88", "@opentui/core-win32-x64": "0.1.88", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-eaDVZfAzZraddOIkgWSHMVkyaY0O20foYnPWKPQx1TY4t7G1oatIoan2zkytx67epW+4BZQ9vGib+61/uNM1MA=="], + "@opentui/core": ["@opentui/core@0.1.90", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.90", "@opentui/core-darwin-x64": "0.1.90", "@opentui/core-linux-arm64": "0.1.90", "@opentui/core-linux-x64": "0.1.90", "@opentui/core-win32-arm64": "0.1.90", "@opentui/core-win32-x64": "0.1.90", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Os2dviqWVETU3kaK36lbSvdcI93GAWhw0xb9ng/d0DWYuM9scRmAhLHiOayp61saWv/BR8OJXeuQYHvrp5rd6A=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.88", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oGRexWwZFeQJymOK5ORrLrwJUbPHMYaFa0EcLnlhvPnymm1xyMcRKm39ez0WSIdtiCCi/PmMHX95CfyyJB5VMA=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.90", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XFrm2zCg1SlHPQ5A2HX/I4dCrmTjYaCJIIpo3QuPIvZBGH3aBMdWDJh2tXw7AB5Mmh8X1K4hDkP5nlK9x0Ewow=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.88", "", { "os": "darwin", "cpu": "x64" }, "sha512-ddnruYpXt7gXsAqZoQzNrHtZ50niYQfESVT3rhE5qgsz7zoWBdKe/RxLKcb6zQmHMZML6SjSh0NrMG86lsH4dQ=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.90", "", { "os": "darwin", "cpu": "x64" }, "sha512-vbDpUsnlZ+0CeVKyBBXE+l2+X1XoVncMxMOhXTiMtud2/Cwu+Vfs/g3LC/6Zv08yaytA+9g7Z8sdf0QCqFyQ4w=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.88", "", { "os": "linux", "cpu": "arm64" }, "sha512-jfcU/Sw8re3aWWb9cQ4OXmVNp/pchu6lgDRqvfy0EKTpzd7CNIu6a0xm+rcUKiPO7BrTrwtumT5/jZWWgCdHlg=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.90", "", { "os": "linux", "cpu": "arm64" }, "sha512-OTbvBTP5mVQ4uwKyuz6b59ElG+D0i1Ln+q6cVhNkLgeRLySIn1uXEzUFQGlnVgb8lFDANsn3yQmdv+R+Cpw0og=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.88", "", { "os": "linux", "cpu": "x64" }, "sha512-nyfilOYLu6XWRlPl1R0Y6WzdL+jVdIFnwShBWcZL+QC5HiJnQc6LKy5yX8uv0fVbY5xs1wBvlHVeUj1UwFQyFQ=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.90", "", { "os": "linux", "cpu": "x64" }, "sha512-2PJi/LLlO7tGk9Ful/n+6iBdg1RFrA9ibU7wVneE6Z1P0LCYeu7bpwMzea1TXL0eAQWPHsjTs9aPlqPxln0EJw=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.88", "", { "os": "win32", "cpu": "arm64" }, "sha512-jv/dQwcku7YZ4lNnYjivVvjPwTfDfzGfcplUqHxmirnv1Q1pZL1qS5wH1PV6RhAKN779vHTvnYMD4OgHWzqVaA=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.90", "", { "os": "win32", "cpu": "arm64" }, "sha512-+sTRaOb7gCMZ6iLuuG4y9kzyweJzBDcIJN0Xh49ikFWTwVECDXEVtXahNGlw57avm2yYUoNzmpBjK/LV7zBj9A=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.88", "", { "os": "win32", "cpu": "x64" }, "sha512-saGvsQqwL8H7B0VBCQ+szMCKh9WIfTebOR8cwPa2+DR+1FnrEG2I4kiikoj4hfYfRMX18A0A11vQxSh3vvy8Ig=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.90", "", { "os": "win32", "cpu": "x64" }, "sha512-aVFyErckWp4oW9NJ/ZDKBUAlTlfVUiRXGP63JXFOoeqI7EYaM8uBt6rgZAJuUdFWCN2Q66WRS8Y2mk+0BJwVBg=="], - "@opentui/solid": ["@opentui/solid@0.1.88", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.88", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-hAqMBk3u/MnUapOmRPdMZinXPOFC+5ccmW1rEQRf9HpShRlZfyg9/u+wUI5rUavyeNFtka92Mtjf/N4AKQpwuA=="], + "@opentui/solid": ["@opentui/solid@0.1.90", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.90", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-zEHDpJOTGS707ts5j4diqoWuFLSqV6yARKl1H0FJkwWOotu+rxCyksL+C0gX0jJUonAw2cjlZ2NNtZY8g78zkg=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 7b765e1cc2..691724dd4c 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -101,8 +101,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.88", - "@opentui/solid": "0.1.88", + "@opentui/core": "0.1.90", + "@opentui/solid": "0.1.90", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 2320c08ccc..d65fbf40ad 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -428,7 +428,7 @@ export function tint(base: RGBA, overlay: RGBA, alpha: number): RGBA { function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson { const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!) const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!) - const transparent = RGBA.fromInts(0, 0, 0, 0) + const transparent = RGBA.fromValues(bg.r, bg.g, bg.b, 0) const isDark = mode == "dark" const col = (i: number) => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 4682c50df1..0d9ddc746c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1465,6 +1465,8 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess streaming={true} content={props.part.text.trim()} conceal={ctx.conceal()} + fg={theme.markdownText} + bg={theme.background} /> From 8035c3435b866926169307ec54914b359aa4fbfd Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 23 Mar 2026 01:03:20 +0000 Subject: [PATCH 13/27] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 9b6094b354..3d2754f88e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-BPuw6KcVksnES59psBplMjMkj1tG/Rd6OLLtqAis81E=", - "aarch64-linux": "sha256-b/rgEy0YsS8OH13nRuOeun/+O0aVTDuMv4oJAMMXxm0=", - "aarch64-darwin": "sha256-d5PE/ejwpQlk4HmuQ4+Umn4Hf7VuhU76We+XTtA3SEY=", - "x86_64-darwin": "sha256-kLqo9lEEfPS7UuCXxfEFcTM+vnRgKCyqlW1jq0AotMQ=" + "x86_64-linux": "sha256-CxeVxNDKEvMsdZpvGZOShklSQ+pWAYq4S3cKDoo6cPQ=", + "aarch64-linux": "sha256-qkMacyXRgbFW9ZvAPepDM5O8GROpXtIZhgQsPOHVogg=", + "aarch64-darwin": "sha256-jPGGoMViHvMBYFqe8BdtWVT1tvPUKYiLCrOy34PjOG0=", + "x86_64-darwin": "sha256-Dss5ChrHZV5/J32iNIh4E+ASltlZsp4QKIiKNlDfAWw=" } } From 84d9b388734166476055bd5c185a09df48d9d1fa Mon Sep 17 00:00:00 2001 From: James Long Date: Sun, 22 Mar 2026 23:35:17 -0400 Subject: [PATCH 14/27] fix(core): fix file watcher test (#18698) --- packages/opencode/test/file/watcher.test.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/opencode/test/file/watcher.test.ts b/packages/opencode/test/file/watcher.test.ts index 8cbd478cba..6658634e54 100644 --- a/packages/opencode/test/file/watcher.test.ts +++ b/packages/opencode/test/file/watcher.test.ts @@ -177,13 +177,17 @@ describeWatcher("FileWatcher", () => { await withWatcher(tmp.path, Effect.void) // Now write a file — no watcher should be listening - await Effect.runPromise( - noUpdate( - tmp.path, - (e) => e.file === file, - Effect.promise(() => fs.writeFile(file, "gone")), - ), - ) + await Instance.provide({ + directory: tmp.path, + fn: () => + Effect.runPromise( + noUpdate( + tmp.path, + (e) => e.file === file, + Effect.promise(() => fs.writeFile(file, "gone")), + ), + ), + }) }) test("ignores .git/index changes", async () => { From db9619dad6a414882b2eab9e472729b9b4ed1a3b Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:27:35 +1000 Subject: [PATCH 15/27] Add 'write' role to vouch manage action (#18718) --- .github/workflows/vouch-manage-by-issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/vouch-manage-by-issue.yml b/.github/workflows/vouch-manage-by-issue.yml index 9604bf87f3..79687639df 100644 --- a/.github/workflows/vouch-manage-by-issue.yml +++ b/.github/workflows/vouch-manage-by-issue.yml @@ -33,6 +33,6 @@ jobs: with: issue-id: ${{ github.event.issue.number }} comment-id: ${{ github.event.comment.id }} - roles: admin,maintain + roles: admin,maintain,write env: GITHUB_TOKEN: ${{ steps.committer.outputs.token }} From fc68c244333a3829177fd0594aa3d5c018203487 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 06:28:47 +0000 Subject: [PATCH 16/27] Update VOUCHED list https://github.com/anomalyco/opencode/issues/18718#issuecomment-4108322776 --- .github/VOUCHED.td | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 831f32edbf..8bce65ae92 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -10,6 +10,8 @@ adamdotdevin -agusbasari29 AI PR slop ariane-emory +-atharvau AI review spamming literally every PR +-danieljoshuanazareth -danieljoshuanazareth edemaine -florianleibert @@ -23,4 +25,3 @@ r44vc0rp rekram1-node -spider-yamet clawdbot/llm psychosis, spam pinging the team thdxr --danieljoshuanazareth From 9239d877b9602a5a80e9e69e744abfe011f5f991 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Mon, 23 Mar 2026 12:14:17 +0530 Subject: [PATCH 17/27] fix(app): batch multi-file prompt attachments (#18722) --- packages/app/src/components/prompt-input.tsx | 8 +--- .../components/prompt-input/attachments.ts | 38 +++++++++------- .../prompt-input/build-request-parts.test.ts | 26 +++++++++++ packages/app/src/i18n/en.ts | 2 +- packages/app/src/utils/prompt.test.ts | 44 +++++++++++++++++++ .../.storybook/mocks/app/context/language.ts | 2 +- 6 files changed, 95 insertions(+), 25 deletions(-) create mode 100644 packages/app/src/utils/prompt.test.ts diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index f3d3e135de..34f83b13e2 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1043,7 +1043,7 @@ export const PromptInput: Component = (props) => { return true } - const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({ + const { addAttachments, removeAttachment, handlePaste } = createPromptAttachments({ editor: () => editorRef, isDialogActive: () => !!dialog.active, setDraggingType: (type) => setStore("draggingType", type), @@ -1388,11 +1388,7 @@ export const PromptInput: Component = (props) => { class="hidden" onChange={(e) => { const list = e.currentTarget.files - if (list) { - for (const file of Array.from(list)) { - void addAttachment(file) - } - } + if (list) void addAttachments(Array.from(list)) e.currentTarget.value = "" }} /> diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index eca508c6ce..fa9930f683 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -71,6 +71,18 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { const addAttachment = (file: File) => add(file) + const addAttachments = async (files: File[], toast = true) => { + let found = false + + for (const file of files) { + const ok = await add(file, false) + if (ok) found = true + } + + if (!found && files.length > 0 && toast) warn() + return found + } + const removeAttachment = (id: string) => { const current = prompt.current() const next = current.filter((part) => part.type !== "image" || part.id !== id) @@ -84,18 +96,14 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { event.preventDefault() event.stopPropagation() - const items = Array.from(clipboardData.items) - const fileItems = items.filter((item) => item.kind === "file") + const files = Array.from(clipboardData.items).flatMap((item) => { + if (item.kind !== "file") return [] + const file = item.getAsFile() + return file ? [file] : [] + }) - if (fileItems.length > 0) { - let found = false - for (const item of fileItems) { - const file = item.getAsFile() - if (!file) continue - const ok = await add(file, false) - if (ok) found = true - } - if (!found) warn() + if (files.length > 0) { + await addAttachments(files) return } @@ -169,12 +177,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { const dropped = event.dataTransfer?.files if (!dropped) return - let found = false - for (const file of Array.from(dropped)) { - const ok = await add(file, false) - if (ok) found = true - } - if (!found && dropped.length > 0) warn() + await addAttachments(Array.from(dropped)) } onMount(() => { @@ -191,6 +194,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { return { addAttachment, + addAttachments, removeAttachment, handlePaste, } diff --git a/packages/app/src/components/prompt-input/build-request-parts.test.ts b/packages/app/src/components/prompt-input/build-request-parts.test.ts index 4c2e2d8bec..ce09ae9217 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.test.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.test.ts @@ -49,6 +49,32 @@ describe("buildRequestParts", () => { expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true) }) + test("keeps multiple uploaded attachments in order", () => { + const result = buildRequestParts({ + prompt: [{ type: "text", content: "check these", start: 0, end: 11 }], + context: [], + images: [ + { type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, + { + type: "image", + id: "img_2", + filename: "b.pdf", + mime: "application/pdf", + dataUrl: "data:application/pdf;base64,BBB", + }, + ], + text: "check these", + messageID: "msg_multi", + sessionID: "ses_multi", + sessionDirectory: "/repo", + }) + + const files = result.requestParts.filter((part) => part.type === "file" && part.url.startsWith("data:")) + + expect(files).toHaveLength(2) + expect(files.map((part) => (part.type === "file" ? part.filename : ""))).toEqual(["a.png", "b.pdf"]) + }) + test("deduplicates context files when prompt already includes same path", () => { const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }] diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 8efd9d3bc9..579b740d3a 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -276,7 +276,7 @@ export const dict = { "prompt.context.includeActiveFile": "Include active file", "prompt.context.removeActiveFile": "Remove active file from context", "prompt.context.removeFile": "Remove file from context", - "prompt.action.attachFile": "Add file", + "prompt.action.attachFile": "Add files", "prompt.attachment.remove": "Remove attachment", "prompt.action.send": "Send", "prompt.action.stop": "Stop", diff --git a/packages/app/src/utils/prompt.test.ts b/packages/app/src/utils/prompt.test.ts new file mode 100644 index 0000000000..1ecaf02c97 --- /dev/null +++ b/packages/app/src/utils/prompt.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test" +import type { Part } from "@opencode-ai/sdk/v2" +import { extractPromptFromParts } from "./prompt" + +describe("extractPromptFromParts", () => { + test("restores multiple uploaded attachments", () => { + const parts = [ + { + id: "text_1", + type: "text", + text: "check these", + sessionID: "ses_1", + messageID: "msg_1", + }, + { + id: "file_1", + type: "file", + mime: "image/png", + url: "data:image/png;base64,AAA", + filename: "a.png", + sessionID: "ses_1", + messageID: "msg_1", + }, + { + id: "file_2", + type: "file", + mime: "application/pdf", + url: "data:application/pdf;base64,BBB", + filename: "b.pdf", + sessionID: "ses_1", + messageID: "msg_1", + }, + ] satisfies Part[] + + const result = extractPromptFromParts(parts) + + expect(result).toHaveLength(3) + expect(result[0]).toMatchObject({ type: "text", content: "check these" }) + expect(result.slice(1)).toMatchObject([ + { type: "image", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, + { type: "image", filename: "b.pdf", mime: "application/pdf", dataUrl: "data:application/pdf;base64,BBB" }, + ]) + }) +}) diff --git a/packages/storybook/.storybook/mocks/app/context/language.ts b/packages/storybook/.storybook/mocks/app/context/language.ts index 8744655422..f75240b9c4 100644 --- a/packages/storybook/.storybook/mocks/app/context/language.ts +++ b/packages/storybook/.storybook/mocks/app/context/language.ts @@ -8,7 +8,7 @@ const dict: Record = { "prompt.placeholder.shell": "Run a shell command...", "prompt.placeholder.summarizeComment": "Summarize this comment", "prompt.placeholder.summarizeComments": "Summarize these comments", - "prompt.action.attachFile": "Attach file", + "prompt.action.attachFile": "Attach files", "prompt.action.send": "Send", "prompt.action.stop": "Stop", "prompt.attachment.remove": "Remove attachment", From 5ea95451dd485b15696877a9dd82c30a532b68e0 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Mon, 23 Mar 2026 12:55:30 +0530 Subject: [PATCH 18/27] fix(app): prevent stale session hover preview on refocus (#18727) --- packages/app/src/pages/layout.tsx | 18 ++++- .../app/src/pages/layout/sidebar-items.tsx | 65 +++++++++++-------- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 0c10cc89bc..88572503fc 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -211,13 +211,22 @@ export default function Layout(props: ParentProps) { onMount(() => { const stop = () => setState("sizing", false) + const blur = () => reset() + const hide = () => { + if (document.visibilityState !== "hidden") return + reset() + } window.addEventListener("pointerup", stop) window.addEventListener("pointercancel", stop) window.addEventListener("blur", stop) + window.addEventListener("blur", blur) + document.addEventListener("visibilitychange", hide) onCleanup(() => { window.removeEventListener("pointerup", stop) window.removeEventListener("pointercancel", stop) window.removeEventListener("blur", stop) + window.removeEventListener("blur", blur) + document.removeEventListener("visibilitychange", hide) }) }) @@ -237,6 +246,12 @@ export default function Layout(props: ParentProps) { navLeave.current = undefined } + const reset = () => { + disarm() + setState("hoverSession", undefined) + setHoverProject(undefined) + } + const arm = () => { if (layout.sidebar.opened()) return if (state.hoverProject === undefined) return @@ -305,8 +320,7 @@ export default function Layout(props: ParentProps) { const clearSidebarHoverState = () => { if (layout.sidebar.opened()) return - setState("hoverSession", undefined) - setHoverProject(undefined) + reset() } const navigateWithSidebarReset = (href: string) => { diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index f8e16f3e12..a9627c5dbc 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -157,34 +157,45 @@ const SessionHoverPreview = (props: { messageLabel: (message: Message) => string | undefined onMessageSelect: (message: Message) => void trigger: JSX.Element -}): JSX.Element => ( - props.setHoverSession(open ? props.session.id : undefined)} - > - {props.language.t("session.messages.loading")}
} +}): JSX.Element => { + let ref: HTMLDivElement | undefined + + return ( + {props.trigger}} + open={props.hoverSession() === props.session.id} + onOpenChange={(open) => { + if (!open) { + props.setHoverSession(undefined) + return + } + if (!ref?.matches(":hover")) return + props.setHoverSession(props.session.id) + }} > -
- -
- -
-) + {props.language.t("session.messages.loading")}} + > +
+ +
+
+ + ) +} export const SessionItem = (props: SessionItemProps): JSX.Element => { const params = useParams() From 0f5626d2e46f9f8abfe616a33a4fd4f4d989e396 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Mon, 23 Mar 2026 13:30:24 +0530 Subject: [PATCH 19/27] fix(app): prefer cmd+k for command palette (#18731) --- packages/app/e2e/actions.ts | 4 ++-- packages/app/e2e/app/palette.spec.ts | 11 ++++++++++- packages/app/e2e/settings/settings-keybinds.spec.ts | 2 +- packages/app/src/context/command-keybind.test.ts | 7 +++++++ .../app/src/pages/session/use-session-commands.tsx | 2 +- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index aced0756c0..90af177ed1 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -175,9 +175,9 @@ export async function runTerminal(page: Page, input: { cmd: string; token: strin await expect.poll(() => terminalHas(page, { term, token: input.token }), { timeout }).toBe(true) } -export async function openPalette(page: Page) { +export async function openPalette(page: Page, key = "K") { await defocus(page) - await page.keyboard.press(`${modKey}+P`) + await page.keyboard.press(`${modKey}+${key}`) const dialog = page.getByRole("dialog") await expect(dialog).toBeVisible() diff --git a/packages/app/e2e/app/palette.spec.ts b/packages/app/e2e/app/palette.spec.ts index 3ccfd7a925..4c701fab27 100644 --- a/packages/app/e2e/app/palette.spec.ts +++ b/packages/app/e2e/app/palette.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { openPalette } from "../actions" +import { closeDialog, openPalette } from "../actions" test("search palette opens and closes", async ({ page, gotoSession }) => { await gotoSession() @@ -9,3 +9,12 @@ test("search palette opens and closes", async ({ page, gotoSession }) => { await page.keyboard.press("Escape") await expect(dialog).toHaveCount(0) }) + +test("search palette also opens with cmd+p", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openPalette(page, "P") + + await closeDialog(page, dialog) + await expect(dialog).toHaveCount(0) +}) diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts index 5789dc0eb0..4fc50b68d7 100644 --- a/packages/app/e2e/settings/settings-keybinds.spec.ts +++ b/packages/app/e2e/settings/settings-keybinds.spec.ts @@ -241,7 +241,7 @@ test("changing file open keybind works", async ({ page, gotoSession }) => { await expect(keybindButton).toBeVisible() const initialKeybind = await keybindButton.textContent() - expect(initialKeybind).toContain("P") + expect(initialKeybind).toContain("K") await keybindButton.click() await expect(keybindButton).toHaveText(/press/i) diff --git a/packages/app/src/context/command-keybind.test.ts b/packages/app/src/context/command-keybind.test.ts index 4e38efd8da..d804195c40 100644 --- a/packages/app/src/context/command-keybind.test.ts +++ b/packages/app/src/context/command-keybind.test.ts @@ -40,4 +40,11 @@ describe("command keybind helpers", () => { expect(display.includes("Alt") || display.includes("⌥")).toBe(true) expect(formatKeybind("none")).toBe("") }) + + test("formatKeybind prefers the first combo", () => { + const display = formatKeybind("mod+k,mod+p") + + expect(display.includes("K") || display.includes("k")).toBe(true) + expect(display.includes("P") || display.includes("p")).toBe(false) + }) }) diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 1a2e777f52..f17e3f7a1f 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -255,7 +255,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { id: "file.open", title: language.t("command.file.open"), description: language.t("palette.search.placeholder"), - keybind: "mod+p", + keybind: "mod+k,mod+p", slash: "open", onSelect: () => dialog.show(() => ), }), From 4c27e7fc6499dee385e718d523c4f0612bd8a063 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 23 Mar 2026 16:44:23 +0800 Subject: [PATCH 20/27] electron: more robust sidecar kill handling (#18742) --- packages/desktop-electron/src/main/cli.ts | 3 ++- packages/desktop-electron/src/main/index.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/desktop-electron/src/main/cli.ts b/packages/desktop-electron/src/main/cli.ts index fba301f36c..f2d918bd21 100644 --- a/packages/desktop-electron/src/main/cli.ts +++ b/packages/desktop-electron/src/main/cli.ts @@ -35,6 +35,7 @@ export type CommandEvent = export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" } export type CommandChild = { + pid: number | undefined kill: () => void } @@ -191,7 +192,7 @@ export function spawnCommand(args: string, extraEnv: Record) { treeKill(child.pid) } - return { events, child: { kill }, exit } + return { events, child: { pid: child.pid, kill }, exit } } function handleSqliteProgress(events: EventEmitter, line: string) { diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 484e4feb20..032343204c 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -81,6 +81,17 @@ function setupApp() { killSidecar() }) + app.on("will-quit", () => { + killSidecar() + }) + + for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.on(signal, () => { + killSidecar() + app.exit(0) + }) + } + void app.whenReady().then(async () => { // migrate() app.setAsDefaultProtocolClient("opencode") @@ -234,8 +245,15 @@ registerIpcHandlers({ function killSidecar() { if (!sidecar) return + const pid = sidecar.pid sidecar.kill() sidecar = null + // tree-kill is async; also send process group signal as immediate fallback + if (pid && process.platform !== "win32") { + try { + process.kill(-pid, "SIGTERM") + } catch {} + } } function ensureLoopbackNoProxy() { From 0a7dfc03ee1dbc29d65605e8ca37ed9d137bd2ec Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 23 Mar 2026 16:58:20 +0800 Subject: [PATCH 21/27] fix(app): lift up project hover state to layout (#18732) --- packages/app/src/pages/layout.tsx | 4 +++ .../app/src/pages/layout/sidebar-project.tsx | 29 +++++-------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 88572503fc..d8b0732580 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1989,6 +1989,10 @@ export default function Layout(props: ParentProps) { onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event), onProjectMouseLeave: (worktree) => aim.leave(worktree), onProjectFocus: (worktree) => aim.activate(worktree), + onHoverOpenChanged: (worktree, hoverOpen) => { + if (!hoverOpen && state.hoverProject && state.hoverProject !== worktree) return + setState("hoverProject", hoverOpen ? worktree : undefined) + }, navigateToProject, openSidebar: () => layout.sidebar.open(), closeProject, diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index 99f1edb74d..aff0645dd8 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -23,6 +23,7 @@ export type ProjectSidebarContext = { onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseLeave: (worktree: string) => void onProjectFocus: (worktree: string) => void + onHoverOpenChanged: (worktree: string, hovered: boolean) => void navigateToProject: (directory: string) => void openSidebar: () => void closeProject: (directory: string) => void @@ -197,7 +198,6 @@ const ProjectPreviewPanel = (props: { projectChildren: Accessor> workspaceSessions: (directory: string) => ReturnType workspaceChildren: (directory: string) => Map - setOpen: (value: boolean) => void ctx: ProjectSidebarContext language: ReturnType }): JSX.Element => ( @@ -264,7 +264,7 @@ const ProjectPreviewPanel = (props: { class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent" onClick={() => { props.ctx.openSidebar() - props.setOpen(false) + props.ctx.onHoverOpenChanged(props.project.worktree, false) if (props.selected()) return props.ctx.navigateToProject(props.project.worktree) }} @@ -289,28 +289,16 @@ export const SortableProject = (props: { const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) const [state, setState] = createStore({ - open: false, menu: false, suppressHover: false, }) + const isHoverProject = () => props.ctx.hoverProject() === props.project.worktree const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) - const active = createMemo( - () => state.menu || (preview() ? state.open : overlay() && props.ctx.hoverProject() === props.project.worktree), - ) + const active = createMemo(() => state.menu || (preview() ? isHoverProject() : overlay() && isHoverProject())) - createEffect(() => { - if (preview()) return - if (!state.open) return - setState("open", false) - }) - - createEffect(() => { - if (!selected()) return - if (!state.open) return - setState("open", false) - }) + const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu const label = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) @@ -351,7 +339,7 @@ export const SortableProject = (props: { workspacesEnabled={props.ctx.workspacesEnabled} closeProject={props.ctx.closeProject} setMenu={(value) => setState("menu", value)} - setOpen={(value) => setState("open", value)} + setOpen={(value) => props.ctx.onHoverOpenChanged(props.project.worktree, value)} setSuppressHover={(value) => setState("suppressHover", value)} language={language} /> @@ -362,7 +350,7 @@ export const SortableProject = (props: {
{ if (state.menu) return if (value && state.suppressHover) return - setState("open", value) + props.ctx.onHoverOpenChanged(props.project.worktree, value) if (value) props.ctx.setHoverSession(undefined) }} > @@ -386,7 +374,6 @@ export const SortableProject = (props: { projectChildren={projectChildren} workspaceSessions={workspaceSessions} workspaceChildren={workspaceChildren} - setOpen={(value) => setState("open", value)} ctx={props.ctx} language={language} /> From 8e1b53b32c2f74c4983e0762cd90f4c2ecc7fda8 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 23 Mar 2026 18:34:32 +0800 Subject: [PATCH 22/27] fix(app): handle session busy state better (#18758) --- bun.lock | 8 ++- packages/app/package.json | 1 + .../src/pages/session/message-timeline.tsx | 55 ++++++------------- 3 files changed, 26 insertions(+), 38 deletions(-) diff --git a/bun.lock b/bun.lock index 6116d26fa0..d706f926d7 100644 --- a/bun.lock +++ b/bun.lock @@ -41,6 +41,7 @@ "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "catalog:", + "@solid-primitives/timer": "1.4.4", "@solid-primitives/websocket": "1.3.1", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", @@ -57,6 +58,7 @@ "shiki": "catalog:", "solid-js": "catalog:", "solid-list": "catalog:", + "solid-presence": "0.2.0", "tailwindcss": "catalog:", "virtua": "catalog:", "zod": "catalog:", @@ -1889,6 +1891,8 @@ "@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="], + "@solid-primitives/timer": ["@solid-primitives/timer@1.4.4", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ayjyb3+v1hyU92vuLUN0tVHq2mmTCPGxSDLGJMsDydRqx9ZfJIc9xj6cxK4XvdY3pif3ps2mIv52pjgToybEpQ=="], + "@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng=="], "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], @@ -4261,7 +4265,7 @@ "solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="], - "solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="], + "solid-presence": ["solid-presence@0.2.0", "", { "dependencies": { "@corvu/utils": "~0.4.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-YM92o+jvpzX3XGaD4rLYmq/Kc2ZVh47GSCLEufHBFQQIurvZTs8SoGJxO8BJGNDxBKdcS8F3dYhW1SDXp4BNjA=="], "solid-prevent-scroll": ["solid-prevent-scroll@0.1.10", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw=="], @@ -5115,6 +5119,8 @@ "@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], + "@kobalte/core/solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="], + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], diff --git a/packages/app/package.json b/packages/app/package.json index 8181825c06..61265c28aa 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -51,6 +51,7 @@ "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "catalog:", + "@solid-primitives/timer": "1.4.4", "@solid-primitives/websocket": "1.3.1", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 7ced21353f..fe61f16854 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -1,4 +1,4 @@ -import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js" +import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX, createSignal } from "solid-js" import { createStore, produce } from "solid-js/store" import { useNavigate } from "@solidjs/router" import { useMutation } from "@tanstack/solid-query" @@ -30,6 +30,7 @@ import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { messageAgentColor } from "@/utils/agent" import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" +import { makeTimer } from "@solid-primitives/timer" type MessageComment = { path: string @@ -250,38 +251,21 @@ export function MessageTimeline(props: { const working = createMemo(() => !!pending() || sessionStatus().type !== "idle") const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent)) - const [slot, setSlot] = createStore({ - open: false, - show: false, - fade: false, + const [timeoutDone, setTimeoutDone] = createSignal(true) + + const workingStatus = createMemo<"hidden" | "showing" | "hiding">((prev) => { + if (working()) return "showing" + if (prev === "showing" || !timeoutDone()) return "hiding" + return "hidden" }) - let f: number | undefined - const clear = () => { - if (f !== undefined) window.clearTimeout(f) - f = undefined - } + createEffect(() => { + if (workingStatus() !== "hiding") return + + setTimeoutDone(false) + makeTimer(() => setTimeoutDone(true), 260, setTimeout) + }) - onCleanup(clear) - createEffect( - on( - working, - (on, prev) => { - clear() - if (on) { - setSlot({ open: true, show: true, fade: false }) - return - } - if (prev) { - setSlot({ open: false, show: true, fade: true }) - f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260) - return - } - setSlot({ open: false, show: false, fade: false }) - }, - { defer: true }, - ), - ) const activeMessageID = createMemo(() => { const parentID = pending()?.parentID if (parentID) { @@ -676,17 +660,15 @@ export function MessageTimeline(props: {
-
Date: Mon, 23 Mar 2026 10:35:30 +0000 Subject: [PATCH 23/27] chore: generate --- bun.lock | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index d706f926d7..2a6a28b7d4 100644 --- a/bun.lock +++ b/bun.lock @@ -58,7 +58,6 @@ "shiki": "catalog:", "solid-js": "catalog:", "solid-list": "catalog:", - "solid-presence": "0.2.0", "tailwindcss": "catalog:", "virtua": "catalog:", "zod": "catalog:", @@ -4265,7 +4264,7 @@ "solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="], - "solid-presence": ["solid-presence@0.2.0", "", { "dependencies": { "@corvu/utils": "~0.4.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-YM92o+jvpzX3XGaD4rLYmq/Kc2ZVh47GSCLEufHBFQQIurvZTs8SoGJxO8BJGNDxBKdcS8F3dYhW1SDXp4BNjA=="], + "solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="], "solid-prevent-scroll": ["solid-prevent-scroll@0.1.10", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw=="], @@ -5119,8 +5118,6 @@ "@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], - "@kobalte/core/solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="], - "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], From eb74e4a6d22775089b376d7a0b777c82a6c2fab7 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 23 Mar 2026 10:37:23 +0000 Subject: [PATCH 24/27] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 3d2754f88e..1fe3266c35 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-CxeVxNDKEvMsdZpvGZOShklSQ+pWAYq4S3cKDoo6cPQ=", - "aarch64-linux": "sha256-qkMacyXRgbFW9ZvAPepDM5O8GROpXtIZhgQsPOHVogg=", - "aarch64-darwin": "sha256-jPGGoMViHvMBYFqe8BdtWVT1tvPUKYiLCrOy34PjOG0=", - "x86_64-darwin": "sha256-Dss5ChrHZV5/J32iNIh4E+ASltlZsp4QKIiKNlDfAWw=" + "x86_64-linux": "sha256-TnrYykX8Mf/Ugtkix6V", + "aarch64-linux": "sha256-TnrYykX8Mf/Ugtkix6V", + "aarch64-darwin": "sha256-TnrYykX8Mf/Ugtkix6V", + "x86_64-darwin": "sha256-TnrYykX8Mf/Ugtkix6V" } } From 6926dc26d159080c506247ca414ec9a305f32c1b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 23 Mar 2026 10:52:56 +0000 Subject: [PATCH 25/27] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 1fe3266c35..a965ed58bd 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-TnrYykX8Mf/Ugtkix6V", - "aarch64-linux": "sha256-TnrYykX8Mf/Ugtkix6V", - "aarch64-darwin": "sha256-TnrYykX8Mf/Ugtkix6V", - "x86_64-darwin": "sha256-TnrYykX8Mf/Ugtkix6V" + "x86_64-linux": "sha256-E5neEbBiwQDhIQ5QVhijpHCCP9hcxm319S9WrDKngSw=", + "aarch64-linux": "sha256-lnwaGSEirl9izskDooB/xQ0ZdirW0t3/S+OoOnfYaoQ=", + "aarch64-darwin": "sha256-RDxxW9NMlGMIdIxTsbOYVqxunflkILv2dA7JqjnJgm4=", + "x86_64-darwin": "sha256-1tvvktu2NRg6N6ASuKzqzcEmMrzH3/LFey0Vxr4E8zg=" } } From 36dfe1646b2bb4c329238f765c8100981014022b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 23 Mar 2026 19:48:34 +0800 Subject: [PATCH 26/27] fix(app): only navigate prompt history when input is empty (#18775) --- packages/app/e2e/prompt/prompt-history.spec.ts | 7 +++++-- packages/app/src/components/prompt-input/history.test.ts | 7 +++++-- packages/app/src/components/prompt-input/history.ts | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/app/e2e/prompt/prompt-history.spec.ts b/packages/app/e2e/prompt/prompt-history.spec.ts index ec68998144..1c9c079550 100644 --- a/packages/app/e2e/prompt/prompt-history.spec.ts +++ b/packages/app/e2e/prompt/prompt-history.spec.ts @@ -108,7 +108,10 @@ test("prompt history restores unsent draft with arrow navigation", async ({ page await page.keyboard.type(draft) await wait(page, draft) - await edge(page, "start") + // Clear the draft before navigating history (ArrowUp only works when prompt is empty) + await prompt.fill("") + await wait(page, "") + await page.keyboard.press("ArrowUp") await wait(page, second) @@ -119,7 +122,7 @@ test("prompt history restores unsent draft with arrow navigation", async ({ page await wait(page, second) await page.keyboard.press("ArrowDown") - await wait(page, draft) + await wait(page, "") }) }) diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts index 37b5ce1962..5e9c2c66ea 100644 --- a/packages/app/src/components/prompt-input/history.test.ts +++ b/packages/app/src/components/prompt-input/history.test.ts @@ -126,7 +126,7 @@ describe("prompt-input history", () => { test("canNavigateHistoryAtCursor only allows prompt boundaries", () => { const value = "a\nb\nc" - expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true) + expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(false) expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false) expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false) @@ -135,11 +135,14 @@ describe("prompt-input history", () => { expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false) expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true) - expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(false) expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true) expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false) expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false) + expect(canNavigateHistoryAtCursor("up", "", 0)).toBe(true) + expect(canNavigateHistoryAtCursor("down", "", 0)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true) expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true) expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true) diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts index de62653211..79e8abc0d9 100644 --- a/packages/app/src/components/prompt-input/history.ts +++ b/packages/app/src/components/prompt-input/history.ts @@ -27,7 +27,7 @@ export function canNavigateHistoryAtCursor(direction: "up" | "down", text: strin const atStart = position === 0 const atEnd = position === text.length if (inHistory) return atStart || atEnd - if (direction === "up") return position === 0 + if (direction === "up") return position === 0 && text.length === 0 return position === text.length } From 77b3b46788623317115ae920cf0072e54aa2643c Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:06:43 +0000 Subject: [PATCH 27/27] tui: keep file tree open at its minimum resized width (#18777) --- packages/app/src/pages/session/session-side-panel.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 3b8b0c96bf..58c650fcd1 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -438,12 +438,10 @@ export function SessionSidePanel(props: { size={layout.fileTree.width()} min={200} max={480} - collapseThreshold={160} onResize={(width) => { props.size.touch() layout.fileTree.resize(width) }} - onCollapse={layout.fileTree.close} />