diff --git a/packages/simulation/src/backend/control.ts b/packages/simulation/src/backend/control.ts index e52c8296f64..a27a0cd2afe 100644 --- a/packages/simulation/src/backend/control.ts +++ b/packages/simulation/src/backend/control.ts @@ -25,10 +25,10 @@ import { SimulationNetwork } from "./network" type ControlSocket = Bun.ServerWebSocket<{ unsubscribe?: () => void }> function parseRequest(input: string | Buffer) { - return SimulationProtocol.JsonRpc.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString())) + return SimulationProtocol.Backend.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString())) } -async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc.Request): Promise { +async function handle(socket: ControlSocket, request: SimulationProtocol.Backend.Request): Promise { switch (request.method) { case "llm.attach": { socket.data.unsubscribe?.() @@ -38,23 +38,22 @@ async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc return { attached: true } } case "llm.chunk": { - const params = await SimulationProtocol.Backend.decodeChunkParams(request.params) await Effect.runPromise( SimulationLLMExchange.push( - params.id, - params.items.map((item) => ({ type: "item", item }) as const), + request.params.id, + request.params.items.map((item) => ({ type: "item", item }) as const), ), ) return { ok: true } } case "llm.finish": { - const params = await SimulationProtocol.Backend.decodeFinishParams(request.params) - await Effect.runPromise(SimulationLLMExchange.push(params.id, [{ type: "finish", reason: params.reason }])) + await Effect.runPromise( + SimulationLLMExchange.push(request.params.id, [{ type: "finish", reason: request.params.reason }]), + ) return { ok: true } } case "llm.disconnect": { - const params = await SimulationProtocol.Backend.decodeDisconnectParams(request.params) - await Effect.runPromise(SimulationLLMExchange.disconnect(params.id)) + await Effect.runPromise(SimulationLLMExchange.disconnect(request.params.id)) return { ok: true } } case "llm.pending": @@ -62,7 +61,6 @@ async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc case "network.log": return { entries: SimulationNetwork.log() } } - throw new Error(`Unknown simulation control method: ${request.method}`) } export function start(endpoint: string) { @@ -79,7 +77,7 @@ export function start(endpoint: string) { socket.data.unsubscribe?.() }, async message(socket, message) { - let request: SimulationProtocol.JsonRpc.Request | undefined + let request: SimulationProtocol.Backend.Request | undefined try { request = parseRequest(message) const result = await handle(socket, request) diff --git a/packages/simulation/src/frontend/actions.ts b/packages/simulation/src/frontend/actions.ts index d03e4a5f75a..9e5b7bdead5 100644 --- a/packages/simulation/src/frontend/actions.ts +++ b/packages/simulation/src/frontend/actions.ts @@ -47,7 +47,7 @@ function hit(renderer: CliRenderer, renderable: Renderable) { /** * Builds the harness the simulation server drives. * - * When the renderer is the fake simulation renderer, its TestRendererSetup + * When the renderer is the headless 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`. diff --git a/packages/simulation/src/frontend/renderer.ts b/packages/simulation/src/frontend/renderer.ts index 168a8999b62..e973d57a98d 100644 --- a/packages/simulation/src/frontend/renderer.ts +++ b/packages/simulation/src/frontend/renderer.ts @@ -4,7 +4,7 @@ import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testin const setups = new WeakMap() /** - * Creates the fake simulation renderer: a real CliRenderer backed by an + * Creates the headless 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. diff --git a/packages/simulation/src/frontend/server.ts b/packages/simulation/src/frontend/server.ts index 854b5e9846f..c717dbd4c6b 100644 --- a/packages/simulation/src/frontend/server.ts +++ b/packages/simulation/src/frontend/server.ts @@ -7,29 +7,20 @@ export interface Server { readonly stop: () => void } -function actionParam(params: unknown) { - return SimulationProtocol.Frontend.decodeActionParams(params).action -} - function parseRequest(input: string | Buffer) { - return SimulationProtocol.JsonRpc.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString())) + return SimulationProtocol.Frontend.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString())) } -async function handle(harness: Harness, request: SimulationProtocol.JsonRpc.Request) { +async function handle(harness: Harness, request: SimulationProtocol.Frontend.Request, headless: boolean) { switch (request.method) { case "ui.state": { + if (headless) await harness.renderOnce() const result = SimulationActions.state(harness) SimulationTrace.add("ui.state", { elements: result.elements.length, actions: result.actions.length }) return result } case "ui.action": - return SimulationActions.execute(harness, actionParam(request.params)) - case "ui.render": { - await harness.renderOnce() - const result = SimulationActions.state(harness) - SimulationTrace.add("ui.render", { elements: result.elements.length, actions: result.actions.length }) - return result - } + return SimulationActions.execute(harness, request.params.action) case "trace.list": return { records: SimulationTrace.list() } case "trace.clear": @@ -38,10 +29,9 @@ async function handle(harness: Harness, request: SimulationProtocol.JsonRpc.Requ case "trace.export": return SimulationTrace.exportTrace() } - throw new Error(`Unknown simulation method: ${request.method}`) } -export function start(harness: Harness, endpoint: string): Server { +export function start(harness: Harness, endpoint: string, headless: boolean): Server { const url = new URL(endpoint) const server = Bun.serve<{ readonly drive: true }>({ hostname: url.hostname, @@ -58,10 +48,10 @@ export function start(harness: Harness, endpoint: string): Server { SimulationTrace.add("control.disconnect") }, async message(socket, message) { - let request: SimulationProtocol.JsonRpc.Request | undefined + let request: SimulationProtocol.Frontend.Request | undefined try { request = parseRequest(message) - const result = await handle(harness, request) + const result = await handle(harness, request, headless) const next = SimulationProtocol.JsonRpc.success(request.id, result) if (next) socket.send(JSON.stringify(next)) } catch (error) { diff --git a/packages/simulation/src/frontend/simulation.ts b/packages/simulation/src/frontend/simulation.ts index 96e68e8dca1..c2a56ad8c1c 100644 --- a/packages/simulation/src/frontend/simulation.ts +++ b/packages/simulation/src/frontend/simulation.ts @@ -7,19 +7,18 @@ import { SimulationServer } from "./server" /** * Drive-mode renderer entry point. * - * Creates the renderer (fake when OPENCODE_DRIVE_RENDERER=fake, the normal + * Creates the renderer (headless when OPENCODE_DRIVE_RENDERER=headless, the normal * visible renderer otherwise) and starts the UI control * server against it. The server stops when the renderer is destroyed, so the * caller only manages the renderer lifecycle. */ export async function create(options: CliRendererConfig): Promise { - const renderer = - process.env.OPENCODE_DRIVE_RENDERER === "fake" - ? await SimulationRenderer.create(options) - : await createCliRenderer(options) + const headless = process.env.OPENCODE_DRIVE_RENDERER === "headless" + const renderer = headless ? await SimulationRenderer.create(options) : await createCliRenderer(options) const server = SimulationServer.start( SimulationActions.createHarness(renderer), DriveManifest.resolve().endpoints.ui, + headless, ) process.stderr.write(`opencode drive ui websocket: ${server.url}\n`) renderer.once("destroy", () => server.stop()) diff --git a/packages/simulation/src/protocol/index.ts b/packages/simulation/src/protocol/index.ts index b05f24d501a..2534db8367c 100644 --- a/packages/simulation/src/protocol/index.ts +++ b/packages/simulation/src/protocol/index.ts @@ -4,9 +4,12 @@ const JsonRpcID = Schema.Union([Schema.String, Schema.Number, Schema.Null]) type Json = Schema.Schema.Type export namespace JsonRpc { - export const Request = Schema.Struct({ + export const RequestFields = { jsonrpc: Schema.Literal("2.0"), id: Schema.optional(JsonRpcID), + } + export const Request = Schema.Struct({ + ...RequestFields, method: Schema.String, params: Schema.optional(Schema.Json), }) @@ -92,7 +95,16 @@ export namespace Frontend { export const ActionParams = Schema.Struct({ action: Action }) export interface ActionParams extends Schema.Schema.Type {} - export const decodeActionParams = Schema.decodeUnknownSync(ActionParams) + + export const Request = Schema.Union([ + Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.action"), params: ActionParams }), + Schema.Struct({ + ...JsonRpc.RequestFields, + method: Schema.Literals(["ui.state", "trace.list", "trace.clear", "trace.export"]), + }), + ]) + export type Request = Schema.Schema.Type + export const decodeRequest = Schema.decodeUnknownSync(Request) export const TraceRecord = Schema.Struct({ id: Schema.Number, @@ -130,6 +142,18 @@ export namespace Backend { export const DisconnectParams = Schema.Struct({ id: Schema.String }) export interface DisconnectParams extends Schema.Schema.Type {} + export const Request = Schema.Union([ + Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.chunk"), params: ChunkParams }), + Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.finish"), params: FinishParams }), + Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.disconnect"), params: DisconnectParams }), + Schema.Struct({ + ...JsonRpc.RequestFields, + method: Schema.Literals(["llm.attach", "llm.pending", "network.log"]), + }), + ]) + export type Request = Schema.Schema.Type + export const decodeRequest = Schema.decodeUnknownSync(Request) + export const OpenedExchange = Schema.Struct({ id: Schema.String, url: Schema.String, body: Schema.Json }) export interface OpenedExchange extends Schema.Schema.Type {} @@ -141,9 +165,6 @@ export namespace Backend { }) export interface NetworkLogEntry extends Schema.Schema.Type {} - export const decodeChunkParams = Schema.decodeUnknownPromise(ChunkParams) - export const decodeFinishParams = Schema.decodeUnknownPromise(FinishParams) - export const decodeDisconnectParams = Schema.decodeUnknownPromise(DisconnectParams) } export * as SimulationProtocol from "./index"