diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index c84ff537b5..4e45f69672 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -8,8 +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 type { TestRendererSetup } from "@opentui/core/testing" +import { createCliRenderer, MouseButton } from "@opentui/core" import { RouteProvider, useRoute } from "./context/route" import { Switch, @@ -184,43 +183,31 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { const simulationEnabled = process.env.OPENCODE_SIMULATION === "1" || process.env.OPENCODE_SIMULATION === "true" const result = yield* Effect.scoped( Effect.gen(function* () { - const acquired = yield* Effect.acquireRelease( + const renderer = yield* Effect.acquireRelease( Effect.tryPromise({ - try: async (): Promise<{ renderer: CliRenderer; setup?: TestRendererSetup }> => { - // The fake renderer is a real CliRenderer backed by an in-memory - // screen buffer; everything downstream (keymap, harness, render) - // treats both renderer kinds identically. - if (simulationEnabled && process.env.OPENCODE_SIMULATION_RENDERER === "fake") { - const { createTestRenderer } = await import("@opentui/core/testing") - const setup = await createTestRenderer({ - width: Number(process.env.OPENCODE_SIMULATION_TUI_WIDTH) || 100, - height: Number(process.env.OPENCODE_SIMULATION_TUI_HEIGHT) || 40, - }) - return { renderer: setup.renderer, setup } - } - const renderer = await 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" }], - }, - }) - return { renderer } - }, + try: () => + simulationEnabled && process.env.OPENCODE_SIMULATION_RENDERER === "fake" + ? import("./simulation/renderer").then(({ SimulationRenderer }) => SimulationRenderer.create()) + : 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))), }), - (acquired) => + (renderer) => Effect.sync(() => { - destroyRenderer(acquired.renderer) + destroyRenderer(renderer) }), ) - const renderer = acquired.renderer win32DisableProcessedInput() const keymap = createDefaultOpenTuiKeymap(renderer) yield* Effect.acquireRelease( @@ -250,7 +237,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { const simulation = yield* Effect.promise(async () => { const { SimulationActions } = await import("./simulation/actions") const { SimulationServer } = await import("./simulation/server") - return SimulationServer.start(SimulationActions.createHarness(renderer, acquired.setup)) + return SimulationServer.start(SimulationActions.createHarness(renderer)) }) if (simulation) { process.stderr.write(`opencode simulation websocket: ${simulation.url}\n`) diff --git a/packages/tui/src/simulation/actions.ts b/packages/tui/src/simulation/actions.ts index 0dc341e49a..68725ec252 100644 --- a/packages/tui/src/simulation/actions.ts +++ b/packages/tui/src/simulation/actions.ts @@ -1,11 +1,6 @@ import type { CliRenderer, Renderable } from "@opentui/core" -import { - createMockKeys, - createMockMouse, - type MockInput, - type MockMouse, - type TestRendererSetup, -} from "@opentui/core/testing" +import { createMockKeys, createMockMouse, type MockInput, type MockMouse } from "@opentui/core/testing" +import { SimulationRenderer } from "./renderer" import { SimulationTrace } from "./trace" export interface KeyModifiers { @@ -77,12 +72,13 @@ function hit(renderer: CliRenderer, renderable: Renderable) { /** * Builds the harness the simulation server drives. * - * When the renderer came from `createTestRenderer` (fake renderer), pass its - * `TestRendererSetup` so the harness uses the supported testing APIs. Without - * it (visible terminal renderer) the harness falls back to `requestRender` + - * `idle` and reading the private `currentRenderBuffer`. + * When the renderer is the fake simulation renderer, its TestRendererSetup + * provides the supported testing APIs. For the visible terminal renderer the + * harness falls back to `requestRender` + `idle` and reading the private + * `currentRenderBuffer`. */ -export function createHarness(renderer: CliRenderer, setup?: TestRendererSetup): Harness { +export function createHarness(renderer: CliRenderer): Harness { + const setup = SimulationRenderer.setupFor(renderer) return { renderer, mockInput: setup?.mockInput ?? createMockKeys(renderer), diff --git a/packages/tui/src/simulation/renderer.ts b/packages/tui/src/simulation/renderer.ts new file mode 100644 index 0000000000..b59f3c9826 --- /dev/null +++ b/packages/tui/src/simulation/renderer.ts @@ -0,0 +1,25 @@ +import type { CliRenderer } from "@opentui/core" +import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testing" + +const setups = new WeakMap() + +/** + * Creates the fake simulation renderer: a real CliRenderer backed by an + * in-memory screen buffer instead of a terminal. The TestRendererSetup is + * kept module-side (keyed by renderer) so the harness can use the supported + * testing APIs without app code carrying it around. + */ +export async function create(): Promise { + const setup = await createTestRenderer({ + width: Number(process.env.OPENCODE_SIMULATION_TUI_WIDTH) || 100, + height: Number(process.env.OPENCODE_SIMULATION_TUI_HEIGHT) || 40, + }) + setups.set(setup.renderer, setup) + return setup.renderer +} + +export function setupFor(renderer: CliRenderer): TestRendererSetup | undefined { + return setups.get(renderer) +} + +export * as SimulationRenderer from "./renderer"