feat: initial commit — extracted from comfy-cloud
Anything-can-drive-the-UI command bus. Single dispatch point for LLM tool
calls, scripts, and (optional) WebSocket remote control. JSON canonical
form plus a plain-text DSL sugar layer. Includes a visible virtual cursor
that animates to elements before commands act on them.
Modules:
- bus.ts — CommandBus class, dispatch, handlers, vars, history,
listActions, readState. Built-in handlers: navigate, click,
fill, submit, select, wait, wait_for, scroll, read, expect,
set
- parser.ts — DSL ↔ JSON, with quoted values, # comments, # speed:
directive, $name = <cmd> assignment, $var interpolation,
expect assertions
- script.ts — script runner; loads /scripts/<name>.script, supports
sub-scripts via `run` command
- cursor.ts — virtual cursor + ripple, speed-controlled, aria-hidden
- provider.tsx — React glue: registers `navigate` via react-router, mounts
cursor, exposes window.commandBus / runScript /
runScriptText for console use
- ws.ts — optional WebSocket producer (connectCommandSocket)
- llm-bridge.ts — buildSystemPrompt (DSL ref + live action snapshot),
extractActionBlocks, runActionBlocks, estimateTokens,
trimMessages
- index.tsx — barrel re-exports
Peer deps: react, react-router (consuming app provides).
Originally inline in `comfy-cloud/app/lib/`. Extracted as part of the
crema-app-aifirst-template scaffolding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
72
src/ws.ts
Normal file
72
src/ws.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// Optional WebSocket producer — pipes external JSON commands into the bus.
|
||||
// Server -> client message shapes:
|
||||
// { id?: string, command: Command }
|
||||
// { id?: string, script: Command[] }
|
||||
// { id?: string, dsl: string }
|
||||
// Client -> server replies: { id, ok: true } | { id, ok: false, error: string }
|
||||
|
||||
import { commandBus, type Command } from "./bus"
|
||||
import { runScriptText } from "./script"
|
||||
|
||||
type ServerMessage =
|
||||
| { id?: string; command: Command }
|
||||
| { id?: string; script: Command[] }
|
||||
| { id?: string; dsl: string }
|
||||
|
||||
export type CommandSocket = {
|
||||
close: () => void
|
||||
socket: () => WebSocket | null
|
||||
}
|
||||
|
||||
export function connectCommandSocket(
|
||||
url: string,
|
||||
opts: { reconnect?: boolean; reconnectDelayMs?: number } = {},
|
||||
): CommandSocket {
|
||||
const { reconnect = true, reconnectDelayMs = 2000 } = opts
|
||||
let ws: WebSocket | null = null
|
||||
let closed = false
|
||||
|
||||
const open = () => {
|
||||
ws = new WebSocket(url)
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
console.log("[command-ws] connected", url)
|
||||
})
|
||||
|
||||
ws.addEventListener("message", async (event) => {
|
||||
let msg: ServerMessage
|
||||
try {
|
||||
msg = JSON.parse(event.data)
|
||||
} catch (err) {
|
||||
ws?.send(JSON.stringify({ error: "invalid_json", detail: String(err) }))
|
||||
return
|
||||
}
|
||||
try {
|
||||
if ("dsl" in msg) await runScriptText(msg.dsl)
|
||||
else if ("script" in msg) await commandBus.run(msg.script)
|
||||
else if ("command" in msg) await commandBus.dispatch(msg.command)
|
||||
else throw new Error("missing 'command', 'script', or 'dsl'")
|
||||
ws?.send(JSON.stringify({ id: msg.id, ok: true }))
|
||||
} catch (err) {
|
||||
ws?.send(JSON.stringify({ id: msg.id, ok: false, error: String(err) }))
|
||||
}
|
||||
})
|
||||
|
||||
ws.addEventListener("close", () => {
|
||||
if (closed || !reconnect) return
|
||||
setTimeout(open, reconnectDelayMs)
|
||||
})
|
||||
|
||||
ws.addEventListener("error", () => ws?.close())
|
||||
}
|
||||
|
||||
open()
|
||||
|
||||
return {
|
||||
close: () => {
|
||||
closed = true
|
||||
ws?.close()
|
||||
},
|
||||
socket: () => ws,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user