From 565285d951334e56aa9db46b0faa09a4122998f8 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:32:08 +1000 Subject: [PATCH 1/4] fix(tui): preserve renderer initialization errors --- packages/tui/src/app.tsx | 32 +++++++++++++----------- packages/tui/test/app-lifecycle.test.tsx | 31 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 57b99c45ce3a..0a9036e15e5d 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -183,21 +183,23 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { const result = yield* Effect.scoped( Effect.gen(function* () { const renderer = yield* Effect.acquireRelease( - Effect.tryPromise(() => - createCliRenderer({ - externalOutputMode: "passthrough", - targetFps: 60, - gatherStats: false, - exitOnCtrlC: false, - useKittyKeyboard: {}, - autoFocus: false, - openConsoleOnError: false, - useMouse: !Flag.OPENCODE_DISABLE_MOUSE && input.config.mouse, - consoleOptions: { - keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], - }, - }), - ), + Effect.tryPromise({ + try: () => + createCliRenderer({ + externalOutputMode: "passthrough", + targetFps: 60, + gatherStats: false, + exitOnCtrlC: false, + useKittyKeyboard: {}, + autoFocus: false, + openConsoleOnError: false, + useMouse: !Flag.OPENCODE_DISABLE_MOUSE && input.config.mouse, + consoleOptions: { + keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], + }, + }), + catch: (error) => (error instanceof Error ? error : new Error(String(error))), + }), (renderer) => Effect.sync(() => { destroyRenderer(renderer) diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx index d3983ae8e04b..559af1e29e8c 100644 --- a/packages/tui/test/app-lifecycle.test.tsx +++ b/packages/tui/test/app-lifecycle.test.tsx @@ -6,6 +6,37 @@ import { Global } from "@opencode-ai/core/global" import { createTuiResolvedConfig } from "./fixture/tui-runtime" import { createEventSource, createFetch, directory, json } from "./fixture/tui-sdk" +test("renderer initialization preserves the original error message", async () => { + const core = await import("@opentui/core") + const message = 'Failed to open library "opentui.dll": error code 126' + mock.module("@opentui/core", () => ({ + ...core, + createCliRenderer: async () => { + throw new Error(message) + }, + })) + + try { + const { run } = await import("../src/app") + await expect( + Effect.runPromise( + run({ + url: "http://test", + directory, + config: createTuiResolvedConfig({ plugin_enabled: {} }), + args: {}, + pluginHost: { + async start() {}, + async dispose() {}, + }, + }).pipe(Effect.provide(Global.defaultLayer)), + ), + ).rejects.toThrow(message) + } finally { + mock.restore() + } +}) + test("SIGHUP clears title and disposes scoped resources once", async () => { const setup = await createTestRenderer({ width: 80, height: 24, useThread: false }) const core = await import("@opentui/core") From 7409c954dcf241fe5fdc5fcc22f9667fb7f31835 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:37:44 +1000 Subject: [PATCH 2/4] test(tui): inject renderer factory --- packages/tui/src/app.tsx | 44 +++++++++++++----------- packages/tui/test/app-lifecycle.test.tsx | 33 ++++-------------- 2 files changed, 30 insertions(+), 47 deletions(-) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 0a9036e15e5d..b675ade175fb 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -177,33 +177,35 @@ function isVersionGreater(left: string, right: string) { return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0 } +export function initializeRenderer(config: TuiConfig.Resolved, factory = createCliRenderer) { + return Effect.tryPromise({ + try: () => + factory({ + externalOutputMode: "passthrough", + targetFps: 60, + gatherStats: false, + exitOnCtrlC: false, + useKittyKeyboard: {}, + autoFocus: false, + openConsoleOnError: false, + useMouse: !Flag.OPENCODE_DISABLE_MOUSE && config.mouse, + consoleOptions: { + keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], + }, + }), + catch: (error) => (error instanceof Error ? error : new Error(String(error))), + }) +} + export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { const global = yield* Global.Service const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown } const result = yield* Effect.scoped( Effect.gen(function* () { - const renderer = yield* Effect.acquireRelease( - Effect.tryPromise({ - try: () => - createCliRenderer({ - externalOutputMode: "passthrough", - targetFps: 60, - gatherStats: false, - exitOnCtrlC: false, - useKittyKeyboard: {}, - autoFocus: false, - openConsoleOnError: false, - useMouse: !Flag.OPENCODE_DISABLE_MOUSE && input.config.mouse, - consoleOptions: { - keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], - }, - }), - catch: (error) => (error instanceof Error ? error : new Error(String(error))), + const renderer = yield* Effect.acquireRelease(initializeRenderer(input.config), (renderer) => + Effect.sync(() => { + destroyRenderer(renderer) }), - (renderer) => - Effect.sync(() => { - destroyRenderer(renderer) - }), ) win32DisableProcessedInput() const keymap = createDefaultOpenTuiKeymap(renderer) diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx index 559af1e29e8c..9ac6b21015d0 100644 --- a/packages/tui/test/app-lifecycle.test.tsx +++ b/packages/tui/test/app-lifecycle.test.tsx @@ -7,34 +7,15 @@ import { createTuiResolvedConfig } from "./fixture/tui-runtime" import { createEventSource, createFetch, directory, json } from "./fixture/tui-sdk" test("renderer initialization preserves the original error message", async () => { - const core = await import("@opentui/core") const message = 'Failed to open library "opentui.dll": error code 126' - mock.module("@opentui/core", () => ({ - ...core, - createCliRenderer: async () => { - throw new Error(message) - }, - })) - - try { - const { run } = await import("../src/app") - await expect( - Effect.runPromise( - run({ - url: "http://test", - directory, - config: createTuiResolvedConfig({ plugin_enabled: {} }), - args: {}, - pluginHost: { - async start() {}, - async dispose() {}, - }, - }).pipe(Effect.provide(Global.defaultLayer)), - ), - ).rejects.toThrow(message) - } finally { - mock.restore() + const { initializeRenderer } = await import("../src/app") + const factory = async () => { + throw new Error(message) } + + await expect( + Effect.runPromise(initializeRenderer(createTuiResolvedConfig({ plugin_enabled: {} }), factory)), + ).rejects.toThrow(message) }) test("SIGHUP clears title and disposes scoped resources once", async () => { From 51e4cabfda66859d1d5198c8109858ad48a39560 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 26 Jun 2026 14:44:01 +0530 Subject: [PATCH 3/4] fix(tui): inline renderer error handling --- packages/tui/src/app.tsx | 46 ++++++++++---------- packages/tui/test/app-lifecycle.test.tsx | 12 ----- packages/tui/test/app-renderer-init.test.tsx | 35 +++++++++++++++ 3 files changed, 57 insertions(+), 36 deletions(-) create mode 100644 packages/tui/test/app-renderer-init.test.tsx diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index b675ade175fb..fecf71b6c8c8 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -8,7 +8,7 @@ import { ClipboardProvider, useClipboard } from "./context/clipboard" import { ExitProvider, useExit } from "./context/exit" import { EpilogueProvider } from "./context/epilogue" import * as Selection from "./util/selection" -import { createCliRenderer, MouseButton, type CliRenderer } from "@opentui/core" +import { createCliRenderer, MouseButton } from "@opentui/core" import { RouteProvider, useRoute } from "./context/route" import { Switch, @@ -177,35 +177,33 @@ function isVersionGreater(left: string, right: string) { return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0 } -export function initializeRenderer(config: TuiConfig.Resolved, factory = createCliRenderer) { - return Effect.tryPromise({ - try: () => - factory({ - externalOutputMode: "passthrough", - targetFps: 60, - gatherStats: false, - exitOnCtrlC: false, - useKittyKeyboard: {}, - autoFocus: false, - openConsoleOnError: false, - useMouse: !Flag.OPENCODE_DISABLE_MOUSE && config.mouse, - consoleOptions: { - keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], - }, - }), - catch: (error) => (error instanceof Error ? error : new Error(String(error))), - }) -} - export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { const global = yield* Global.Service const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown } const result = yield* Effect.scoped( Effect.gen(function* () { - const renderer = yield* Effect.acquireRelease(initializeRenderer(input.config), (renderer) => - Effect.sync(() => { - destroyRenderer(renderer) + const renderer = yield* Effect.acquireRelease( + Effect.tryPromise({ + try: () => + createCliRenderer({ + externalOutputMode: "passthrough", + targetFps: 60, + gatherStats: false, + exitOnCtrlC: false, + useKittyKeyboard: {}, + autoFocus: false, + openConsoleOnError: false, + useMouse: !Flag.OPENCODE_DISABLE_MOUSE && input.config.mouse, + consoleOptions: { + keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], + }, + }), + catch: (error) => (error instanceof Error ? error : new Error(String(error))), }), + (renderer) => + Effect.sync(() => { + destroyRenderer(renderer) + }), ) win32DisableProcessedInput() const keymap = createDefaultOpenTuiKeymap(renderer) diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx index 9ac6b21015d0..d3983ae8e04b 100644 --- a/packages/tui/test/app-lifecycle.test.tsx +++ b/packages/tui/test/app-lifecycle.test.tsx @@ -6,18 +6,6 @@ import { Global } from "@opencode-ai/core/global" import { createTuiResolvedConfig } from "./fixture/tui-runtime" import { createEventSource, createFetch, directory, json } from "./fixture/tui-sdk" -test("renderer initialization preserves the original error message", async () => { - const message = 'Failed to open library "opentui.dll": error code 126' - const { initializeRenderer } = await import("../src/app") - const factory = async () => { - throw new Error(message) - } - - await expect( - Effect.runPromise(initializeRenderer(createTuiResolvedConfig({ plugin_enabled: {} }), factory)), - ).rejects.toThrow(message) -}) - test("SIGHUP clears title and disposes scoped resources once", async () => { const setup = await createTestRenderer({ width: 80, height: 24, useThread: false }) const core = await import("@opentui/core") diff --git a/packages/tui/test/app-renderer-init.test.tsx b/packages/tui/test/app-renderer-init.test.tsx new file mode 100644 index 000000000000..89f9e122bbc3 --- /dev/null +++ b/packages/tui/test/app-renderer-init.test.tsx @@ -0,0 +1,35 @@ +import { expect, mock, test } from "bun:test" +import { Effect } from "effect" +import { Global } from "@opencode-ai/core/global" +import { createTuiResolvedConfig } from "./fixture/tui-runtime" + +test("run preserves the original renderer initialization error message", async () => { + const message = 'Failed to open library "opentui.dll": error code 126' + const core = await import("@opentui/core") + mock.module("@opentui/core", () => ({ + ...core, + createCliRenderer: async () => { + throw new Error(message) + }, + })) + + try { + const { run } = await import("../src/app") + + await expect( + Effect.runPromise( + run({ + url: "http://test", + config: createTuiResolvedConfig({ plugin_enabled: {} }), + args: {}, + pluginHost: { + async start() {}, + async dispose() {}, + }, + }).pipe(Effect.provide(Global.defaultLayer)), + ), + ).rejects.toThrow(message) + } finally { + mock.restore() + } +}) From 609d7b4d237364ce088ecc839ff24198fc1ec8c9 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 26 Jun 2026 15:13:27 +0530 Subject: [PATCH 4/4] test(tui): remove renderer init mock test --- packages/tui/test/app-renderer-init.test.tsx | 35 -------------------- 1 file changed, 35 deletions(-) delete mode 100644 packages/tui/test/app-renderer-init.test.tsx diff --git a/packages/tui/test/app-renderer-init.test.tsx b/packages/tui/test/app-renderer-init.test.tsx deleted file mode 100644 index 89f9e122bbc3..000000000000 --- a/packages/tui/test/app-renderer-init.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { expect, mock, test } from "bun:test" -import { Effect } from "effect" -import { Global } from "@opencode-ai/core/global" -import { createTuiResolvedConfig } from "./fixture/tui-runtime" - -test("run preserves the original renderer initialization error message", async () => { - const message = 'Failed to open library "opentui.dll": error code 126' - const core = await import("@opentui/core") - mock.module("@opentui/core", () => ({ - ...core, - createCliRenderer: async () => { - throw new Error(message) - }, - })) - - try { - const { run } = await import("../src/app") - - await expect( - Effect.runPromise( - run({ - url: "http://test", - config: createTuiResolvedConfig({ plugin_enabled: {} }), - args: {}, - pluginHost: { - async start() {}, - async dispose() {}, - }, - }).pipe(Effect.provide(Global.defaultLayer)), - ), - ).rejects.toThrow(message) - } finally { - mock.restore() - } -})