From 013893923f962af6f24a731523bafb31b0361d20 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:32:19 +0000 Subject: [PATCH 01/34] fix(opencode): flush embedded server telemetry --- packages/opencode/src/cli/cmd/run.ts | 52 +++++++++++-------- packages/opencode/src/cli/effect-cmd.ts | 36 +++++++------ packages/opencode/src/server/server.ts | 6 ++- .../opencode/test/cli/run/run-process.test.ts | 29 +++++++++++ 4 files changed, 83 insertions(+), 40 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 3927f615a0..a1798667b3 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -123,6 +123,20 @@ async function toolError(part: ToolPart) { } } +async function embeddedServer() { + const { Server } = await import("@/server/server") + const server = Server.Default().app + const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const request = new Request(input, init) + const headers = new Headers(request.headers) + const { ServerAuth } = await import("@/server/auth") + const auth = ServerAuth.header() + if (auth) headers.set("Authorization", auth) + return server.fetch(new Request(request, { headers })) + }) as typeof globalThis.fetch + return { fetch, dispose: server.dispose } +} + export const RunCommand = effectCmd({ command: "run [message..]", describe: "run opencode with a message", @@ -902,19 +916,12 @@ export const RunCommand = effectCmd({ if (interactive && !args.attach && !args.session && !args.continue) { const model = pick(args.model) const { runInteractiveLocalMode } = await import("./run/runtime") - const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { - const { Server } = await import("@/server/server") - const request = new Request(input, init) - const headers = new Headers(request.headers) - const auth = ServerAuth.header() - if (auth) headers.set("Authorization", auth) - return Server.Default().app.fetch(new Request(request, { headers })) - }) as typeof globalThis.fetch + const server = await embeddedServer() try { return await runInteractiveLocalMode({ directory: directory ?? root, - fetch: fetchFn, + fetch: server.fetch, resolveAgent: localAgent, session, share, @@ -932,6 +939,8 @@ export const RunCommand = effectCmd({ }) } catch (error) { dieInteractive(error) + } finally { + await server.dispose() } } @@ -940,20 +949,17 @@ export const RunCommand = effectCmd({ return await execute(sdk) } - const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { - const { Server } = await import("@/server/server") - const request = new Request(input, init) - const headers = new Headers(request.headers) - const auth = ServerAuth.header() - if (auth) headers.set("Authorization", auth) - return Server.Default().app.fetch(new Request(request, { headers })) - }) as typeof globalThis.fetch - const sdk = createOpencodeClient({ - baseUrl: "http://opencode.internal", - fetch: fetchFn, - directory, - }) - await execute(sdk) + const server = await embeddedServer() + try { + const sdk = createOpencodeClient({ + baseUrl: "http://opencode.internal", + fetch: server.fetch, + directory, + }) + await execute(sdk) + } finally { + await server.dispose() + } }) }), }) diff --git a/packages/opencode/src/cli/effect-cmd.ts b/packages/opencode/src/cli/effect-cmd.ts index 9ac2bd3555..66475f84ad 100644 --- a/packages/opencode/src/cli/effect-cmd.ts +++ b/packages/opencode/src/cli/effect-cmd.ts @@ -60,6 +60,8 @@ interface EffectCmdOpts { * * Errors propagate to the existing top-level handler in `src/index.ts`; use * `fail("...")` for user-visible domain failures (clean exit, formatted message). + * The AppRuntime is disposed after the command finishes so its scoped resources + * are released without making the CLI entrypoint import the full application graph. * * Handlers are typically `Effect.fn("Cli.")(function*(args) { ... })`, * which adds a named tracing span per CLI invocation. Once all commands use @@ -74,23 +76,27 @@ export const effectCmd = (opts: EffectCmdOpts) => builder: opts.builder as never, async handler(rawArgs) { const { AppRuntime } = await import("@/effect/app-runtime") - // yargs typing wraps Args in ArgumentsCamelCase>; cast at the boundary. - const args = rawArgs as unknown as WithDoubleDash - const useInstance = typeof opts.instance === "function" ? opts.instance(args) : opts.instance !== false - if (!useInstance) { - await AppRuntime.runPromise(opts.handler(args)) - return - } - const { InstanceStore } = await import("@/project/instance-store") - const { InstanceRef } = await import("@/effect/instance-ref") - const directory = opts.directory?.(args) ?? process.cwd() - const { store, ctx } = await AppRuntime.runPromise( - InstanceStore.Service.use((store) => store.load({ directory }).pipe(Effect.map((ctx) => ({ store, ctx })))), - ) try { - await AppRuntime.runPromise(opts.handler(args).pipe(Effect.provideService(InstanceRef, ctx))) + // yargs typing wraps Args in ArgumentsCamelCase>; cast at the boundary. + const args = rawArgs as unknown as WithDoubleDash + const useInstance = typeof opts.instance === "function" ? opts.instance(args) : opts.instance !== false + if (!useInstance) { + await AppRuntime.runPromise(opts.handler(args)) + return + } + const { InstanceStore } = await import("@/project/instance-store") + const { InstanceRef } = await import("@/effect/instance-ref") + const directory = opts.directory?.(args) ?? process.cwd() + const { store, ctx } = await AppRuntime.runPromise( + InstanceStore.Service.use((store) => store.load({ directory }).pipe(Effect.map((ctx) => ({ store, ctx })))), + ) + try { + await AppRuntime.runPromise(opts.handler(args).pipe(Effect.provideService(InstanceRef, ctx))) + } finally { + await AppRuntime.runPromise(store.dispose(ctx)) + } } finally { - await AppRuntime.runPromise(store.dispose(ctx)) + await AppRuntime.dispose().catch(() => {}) } }, }) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 440b992c15..62f778b8ad 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -27,6 +27,7 @@ export type Listener = { type ServerApp = { fetch(request: Request): Response | Promise request(input: string | URL | Request, init?: RequestInit): Response | Promise + dispose(): Promise } type ListenOptions = CorsOptions & { @@ -54,12 +55,13 @@ class ListenerServerService extends Context.Service { - const handler = HttpApiApp.webHandler().handler + const web = HttpApiApp.webHandler() const app: ServerApp = { - fetch: (request: Request) => handler(request, HttpApiApp.context), + fetch: (request: Request) => web.handler(request, HttpApiApp.context), request(input, init) { return app.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init)) }, + dispose: web.dispose, } return { app } }) diff --git a/packages/opencode/test/cli/run/run-process.test.ts b/packages/opencode/test/cli/run/run-process.test.ts index bd5847e272..20a5971259 100644 --- a/packages/opencode/test/cli/run/run-process.test.ts +++ b/packages/opencode/test/cli/run/run-process.test.ts @@ -23,6 +23,35 @@ describe("opencode run (non-interactive subprocess)", () => { 60_000, ) + cliIt.concurrent( + "flushes server telemetry before exiting", + ({ llm, opencode }) => + Effect.gen(function* () { + const requests: string[] = [] + const collector = yield* Effect.acquireRelease( + Effect.sync(() => + Bun.serve({ + port: 0, + fetch(request) { + requests.push(new URL(request.url).pathname) + return new Response(null, { status: 200 }) + }, + }), + ), + (server) => Effect.sync(() => server.stop(true)), + ) + yield* llm.text("telemetry response") + + const result = yield* opencode.run("say hi", { + env: { OTEL_EXPORTER_OTLP_ENDPOINT: collector.url.toString().replace(/\/$/, "") }, + }) + + opencode.expectExit(result, 0) + expect(requests).toContain("/v1/traces") + }), + 60_000, + ) + cliIt.concurrent( "prints each completed text part in order around a tool continuation", ({ llm, opencode }) => From e024e2ef92293a81a06b6cc418422c52207ce85d Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:46:36 +1000 Subject: [PATCH 02/34] fix(app): align debug gutter text (#39782) --- packages/app/src/components/debug-bar.tsx | 54 +++++++++++++++-------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx index 67c9a89929..adadeda303 100644 --- a/packages/app/src/components/debug-bar.tsx +++ b/packages/app/src/components/debug-bar.tsx @@ -66,7 +66,7 @@ function Cell(props: {
- {props.label} -
-
- {props.value} +
+ {props.label} +
+
+ {props.value} +
) @@ -116,15 +124,25 @@ function FocusCell(props: { active: boolean; inline?: boolean; onClick: () => vo aria-pressed={props.active} classList={{ "flex min-w-0 items-center font-mono uppercase hover:bg-surface-raised-base focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-border-focus": true, - "min-h-[20px] w-fit flex-row justify-start gap-1.5 rounded px-1.5 py-0.5 text-left": !!props.inline, + "min-h-[20px] w-fit justify-start rounded px-1.5 py-0.5 text-left": !!props.inline, "min-h-[42px] w-full flex-col justify-center rounded-[8px] px-0.5 py-1 text-center": !props.inline, "bg-surface-raised-base text-text-strong": props.active, }} onClick={props.onClick} > - FOCUS - - {props.active ? "ON" : "OFF"} + + FOCUS + + {props.active ? "ON" : "OFF"} + ) From 7ea343cabd057e29d4f34d07d7aeae832293bfae Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:24:19 +0200 Subject: [PATCH 03/34] fix(app): prevent file tree tab clipping (#39770) --- packages/app/src/pages/session/session-side-panel.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 22c52e73fe..6f074fa203 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -59,6 +59,7 @@ import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/ses type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff +const FILE_TREE_WIDTH_MIN = 240 function renderDiff(value: ReviewDiff): value is RenderDiff { return typeof value.file === "string" @@ -104,13 +105,14 @@ export function SessionSidePanel(props: { }), ) const open = createMemo(() => reviewOpen() || fileOpen()) + const fileTreeWidth = createMemo(() => Math.max(FILE_TREE_WIDTH_MIN, layout.fileTree.width())) const reviewTab = createMemo(() => isDesktop()) const panelWidth = createMemo(() => { if (!open()) return "0px" if (reviewOpen()) return "auto" - return `${layout.fileTree.width()}px` + return `${fileTreeWidth()}px` }) - const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px")) + const treeWidth = createMemo(() => (fileOpen() ? `${fileTreeWidth()}px` : "0px")) const diffs = createMemo(() => props.diffs().filter(renderDiff)) const diffFiles = createMemo(() => diffs().map((d) => d.file)) @@ -845,8 +847,8 @@ export function SessionSidePanel(props: { { props.size.touch() From d3f30df1806ea9f95190948c59f6e8b4c97301d0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:38:34 +0000 Subject: [PATCH 04/34] fix(app): open legacy picker at home (#39804) Co-authored-by: Brendan Allan --- packages/app/src/components/dialog-select-directory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 2a6c877df1..96bcedb9db 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -57,9 +57,9 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const [filter, setFilter] = createSignal("") let list: ListRef | undefined - const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) + const missingHome = createMemo(() => !sync.data.path.home) const [fallbackPath] = createResource( - () => (missingBase() ? true : undefined), + () => (missingHome() ? true : undefined), async (): Promise => { if ((await sdk.protocol) !== "v1") return return sdk.client.path From db4dbaa2894a1a98e90e814416eb9f7c35711819 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:14:09 +0200 Subject: [PATCH 05/34] fix(app): prevent stale session tab reads (#39767) --- .../app/src/components/session/session-header.tsx | 12 ++++++------ packages/app/src/components/titlebar-tab-nav.tsx | 5 +++-- .../app/src/pages/new-session/new-session-view.tsx | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index da1770cd13..dc6cdaa6f3 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -289,9 +289,9 @@ export function SessionHeader() { return ( <> - + {(mount) => ( - +