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.
This commit is contained in:
James Long 2026-07-02 18:39:27 +00:00
commit 3dc36ec38b
2 changed files with 47 additions and 38 deletions

View file

@ -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(

View file

@ -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<CliRenderer>): Promise<CliRenderer> {
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"