refactor(tui): specialize semantic snapshot provider
This commit is contained in:
parent
4e38121a60
commit
7690ae9897
5 changed files with 50 additions and 65 deletions
|
|
@ -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<void>>()
|
||||
|
|
|
|||
|
|
@ -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] }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
export type QueryHandler = (
|
||||
params: unknown,
|
||||
context: { readonly id: number; readonly name: string },
|
||||
) => unknown | Promise<unknown>
|
||||
|
||||
export function provideTerminalControlQueries(options: {
|
||||
readonly application: { readonly name: string; readonly version?: string }
|
||||
readonly queries: Readonly<Record<string, QueryHandler>>
|
||||
readonly socketPath?: string | null
|
||||
readonly onError?: (error: unknown) => void
|
||||
}): {
|
||||
readonly enabled: boolean
|
||||
readonly ready: Promise<boolean>
|
||||
close(): void
|
||||
}
|
||||
12
packages/tui/src/terminal-control/semantic-provider.d.mts
Normal file
12
packages/tui/src/terminal-control/semantic-provider.d.mts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export interface SemanticProvider {
|
||||
readonly enabled: boolean
|
||||
readonly ready: Promise<boolean>
|
||||
close(): void
|
||||
}
|
||||
|
||||
export function provideTerminalControlSemanticSnapshot(options: {
|
||||
readonly application: { readonly name: string; readonly version?: string }
|
||||
readonly snapshot: () => unknown | Promise<unknown>
|
||||
readonly socketPath?: string | null
|
||||
readonly onError?: (error: unknown) => void
|
||||
}): SemanticProvider
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue