diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 7b319eecb4..be56984755 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -235,10 +235,10 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { (renderer) => Effect.sync(() => destroyRenderer(renderer)), ) }) - if (process.env.TERMCTRL_QUERY_SOCKET) { - const { startTerminalControlQueries } = yield* Effect.promise(() => import("./terminal-control")) - const queries = startTerminalControlQueries(renderer) - yield* Effect.addFinalizer(() => Effect.sync(() => queries.close())) + if (process.env.TERMCTRL_SEMANTIC_SOCKET) { + const { startTerminalControlSemantics } = yield* Effect.promise(() => import("./terminal-control")) + const semantics = startTerminalControlSemantics(renderer) + yield* Effect.addFinalizer(() => Effect.sync(() => semantics.close())) } win32DisableProcessedInput() const finalizers = new Set<() => Promise>() diff --git a/packages/tui/src/terminal-control/index.ts b/packages/tui/src/terminal-control/index.ts index bc1d92dd06..cbbe52a492 100644 --- a/packages/tui/src/terminal-control/index.ts +++ b/packages/tui/src/terminal-control/index.ts @@ -1,36 +1,32 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { Logging } from "@opencode-ai/core/observability/logging" import { SimulationActions } from "@opencode-ai/simulation/frontend/actions" import type { CliRenderer } from "@opentui/core" -import { provideTerminalControlQueries } from "./query-provider.mjs" +import { provideTerminalControlSemanticSnapshot } from "./semantic-provider.mjs" -export function startTerminalControlQueries(renderer: CliRenderer) { +export function startTerminalControlSemantics(renderer: CliRenderer) { const harness = SimulationActions.createHarness(renderer) - return provideTerminalControlQueries({ + return provideTerminalControlSemanticSnapshot({ application: { name: "opencode", version: InstallationVersion }, - queries: { - "ui.snapshot": async () => { - await harness.renderOnce() - const snapshot = SimulationActions.snapshot(harness) - const semanticElements = new Set(snapshot.nodes.map((node) => node.element)) - const semanticIDs = new Set(snapshot.nodes.map((node) => node.id)) - const inferred = SimulationActions.elements(renderer) - .filter((element) => !semanticElements.has(element.num)) - .map((element) => { - const id = element.id && !semanticIDs.has(element.id) ? element.id : `renderable-${element.num}` - semanticIDs.add(id) - return { - id, - role: element.editor ? "textbox" : element.clickable ? "button" : "control", - label: element.id || undefined, - element: element.num, - focused: element.focused || element.editor, - disabled: false, - } - }) - return { ...snapshot, nodes: [...snapshot.nodes, ...inferred] } - }, - "log-files": () => Logging.file(), + snapshot: async () => { + await harness.renderOnce() + const snapshot = SimulationActions.snapshot(harness) + const semanticElements = new Set(snapshot.nodes.map((node) => node.element)) + const semanticIDs = new Set(snapshot.nodes.map((node) => node.id)) + const inferred = SimulationActions.elements(renderer) + .filter((element) => !semanticElements.has(element.num)) + .map((element) => { + const id = element.id && !semanticIDs.has(element.id) ? element.id : `renderable-${element.num}` + semanticIDs.add(id) + return { + id, + role: element.editor ? "textbox" : element.clickable ? "button" : "control", + label: element.id || undefined, + element: element.num, + focused: element.focused || element.editor, + disabled: false, + } + }) + return { format: "termctrl-semantic-snapshot-v1", nodes: [...snapshot.nodes, ...inferred] } }, }) } diff --git a/packages/tui/src/terminal-control/query-provider.d.mts b/packages/tui/src/terminal-control/query-provider.d.mts deleted file mode 100644 index d48880e657..0000000000 --- a/packages/tui/src/terminal-control/query-provider.d.mts +++ /dev/null @@ -1,15 +0,0 @@ -export type QueryHandler = ( - params: unknown, - context: { readonly id: number; readonly name: string }, -) => unknown | Promise - -export function provideTerminalControlQueries(options: { - readonly application: { readonly name: string; readonly version?: string } - readonly queries: Readonly> - readonly socketPath?: string | null - readonly onError?: (error: unknown) => void -}): { - readonly enabled: boolean - readonly ready: Promise - close(): void -} diff --git a/packages/tui/src/terminal-control/semantic-provider.d.mts b/packages/tui/src/terminal-control/semantic-provider.d.mts new file mode 100644 index 0000000000..964746e5db --- /dev/null +++ b/packages/tui/src/terminal-control/semantic-provider.d.mts @@ -0,0 +1,12 @@ +export interface SemanticProvider { + readonly enabled: boolean + readonly ready: Promise + close(): void +} + +export function provideTerminalControlSemanticSnapshot(options: { + readonly application: { readonly name: string; readonly version?: string } + readonly snapshot: () => unknown | Promise + readonly socketPath?: string | null + readonly onError?: (error: unknown) => void +}): SemanticProvider diff --git a/packages/tui/src/terminal-control/query-provider.mjs b/packages/tui/src/terminal-control/semantic-provider.mjs similarity index 71% rename from packages/tui/src/terminal-control/query-provider.mjs rename to packages/tui/src/terminal-control/semantic-provider.mjs index 4d49267f4c..c91acbbdb5 100644 --- a/packages/tui/src/terminal-control/query-provider.mjs +++ b/packages/tui/src/terminal-control/semantic-provider.mjs @@ -1,17 +1,15 @@ import { createConnection } from "node:net" -const socketFromEnvironment = () => process.env.TERMCTRL_QUERY_SOCKET +const socketFromEnvironment = () => process.env.TERMCTRL_SEMANTIC_SOCKET -export function provideTerminalControlQueries({ +export function provideTerminalControlSemanticSnapshot({ application, - queries, + snapshot, socketPath = socketFromEnvironment(), onError = () => {}, }) { if (!application?.name) throw new TypeError("application.name is required") - if (!queries || Object.values(queries).some((handler) => typeof handler !== "function")) { - throw new TypeError("queries must be an object of query handlers") - } + if (typeof snapshot !== "function") throw new TypeError("snapshot must be a function") if (!socketPath) { return { enabled: false, ready: Promise.resolve(false), close() {} } } @@ -32,7 +30,7 @@ export function provideTerminalControlQueries({ socket.setEncoding("utf8") const handshakeTimer = setTimeout(() => { - fail(new Error("Terminal Control query handshake timed out")) + fail(new Error("Terminal Control semantic handshake timed out")) }, 5_000) handshakeTimer.unref?.() @@ -41,7 +39,7 @@ export function provideTerminalControlQueries({ type: "hello", protocolVersion: 1, application, - queries: Object.keys(queries), + capabilities: ["semantic.snapshot"], }) }) @@ -73,26 +71,20 @@ export function provideTerminalControlQueries({ return } if ( - message?.type !== "query" || + message?.type !== "semantic.snapshot" || !protocolReady || - !Number.isSafeInteger(message.id) || - typeof message.name !== "string" + !Number.isSafeInteger(message.id) ) { - throw new Error("Terminal Control sent an invalid query message") + throw new Error("Terminal Control sent an invalid semantic snapshot request") } - const handler = queries[message.name] - if (!handler) { - sendError(message.id, "QUERY_NOT_SUPPORTED", `Unsupported query ${message.name}`) - return - } Promise.resolve() - .then(() => handler(message.params, { id: message.id, name: message.name })) + .then(snapshot) .then((value) => send({ type: "result", id: message.id, value })) .catch((error) => sendError( message.id, - typeof error?.code === "string" ? error.code : "QUERY_FAILED", + typeof error?.code === "string" ? error.code : "SNAPSHOT_FAILED", error instanceof Error ? error.message : String(error), ), )