From 3dc36ec38b99a85cb5a05f1466ab324c5b57c10d Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 2 Jul 2026 18:39:27 +0000 Subject: [PATCH] refactor(tui): collapse simulation activation into renderer creation app.tsx now has a single simulation touchpoint inside the renderer acquire: in simulation mode it delegates to Simulation.createSimulation, which picks the fake or visible renderer and starts the control server. The server is tied to renderer destroy, so no extra finalizer is needed in app code. --- packages/tui/src/app.tsx | 60 +++++++++-------------- packages/tui/src/simulation/simulation.ts | 25 ++++++++++ 2 files changed, 47 insertions(+), 38 deletions(-) create mode 100644 packages/tui/src/simulation/simulation.ts diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 2cf079b898..6f629e4222 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -180,25 +180,31 @@ function isVersionGreater(left: string, right: string) { 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 simulationEnabled = process.env.OPENCODE_SIMULATION === "1" || process.env.OPENCODE_SIMULATION === "true" const result = yield* Effect.scoped( Effect.gen(function* () { - let renderer = yield* Effect.acquireRelease( + 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" }], - }, - }), + try: async () => { + const createVisibleRenderer = () => + 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" }], + }, + }) + if (process.env.OPENCODE_SIMULATION === "1" || process.env.OPENCODE_SIMULATION === "true") { + const { Simulation } = await import("./simulation/simulation") + return Simulation.createSimulation(createVisibleRenderer) + } + return createVisibleRenderer() + }, catch: (error) => (error instanceof Error ? error : new Error(String(error))), }), (renderer) => @@ -206,28 +212,6 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { destroyRenderer(renderer) }), ) - - if (simulationEnabled) { - if (process.env.OPENCODE_SIMULATION_RENDERER === "fake") { - const { SimulationRenderer } = yield* Effect.promise(() => import("./simulation/renderer")) - destroyRenderer(renderer) - renderer = yield* Effect.acquireRelease( - Effect.promise(() => SimulationRenderer.create()), - (fake) => Effect.sync(() => destroyRenderer(fake)), - ) - } - const target = renderer - const simulation = yield* Effect.promise(async () => { - const { SimulationActions } = await import("./simulation/actions") - const { SimulationServer } = await import("./simulation/server") - return SimulationServer.start(SimulationActions.createHarness(target)) - }) - if (simulation) { - process.stderr.write(`opencode simulation websocket: ${simulation.url}\n`) - yield* Effect.addFinalizer(() => Effect.sync(() => simulation.stop())) - } - } - win32DisableProcessedInput() const keymap = createDefaultOpenTuiKeymap(renderer) yield* Effect.acquireRelease( diff --git a/packages/tui/src/simulation/simulation.ts b/packages/tui/src/simulation/simulation.ts new file mode 100644 index 0000000000..934c83a285 --- /dev/null +++ b/packages/tui/src/simulation/simulation.ts @@ -0,0 +1,25 @@ +import type { CliRenderer } from "@opentui/core" +import { SimulationActions } from "./actions" +import { SimulationRenderer } from "./renderer" +import { SimulationServer } from "./server" + +/** + * Simulation-mode renderer entry point. + * + * Creates the renderer (fake when OPENCODE_SIMULATION_RENDERER=fake, the + * normal visible renderer otherwise) and starts the simulation control + * server against it. The server stops when the renderer is destroyed, so the + * caller only manages the renderer lifecycle. + */ +export async function createSimulation(createVisibleRenderer: () => Promise): Promise { + const renderer = + process.env.OPENCODE_SIMULATION_RENDERER === "fake" ? await SimulationRenderer.create() : await createVisibleRenderer() + const server = SimulationServer.start(SimulationActions.createHarness(renderer)) + if (server) { + process.stderr.write(`opencode simulation websocket: ${server.url}\n`) + renderer.once("destroy", () => server.stop()) + } + return renderer +} + +export * as Simulation from "./simulation"