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>
166 lines
4.9 KiB
Markdown
166 lines
4.9 KiB
Markdown
# @crema/action-bus
|
|
|
|
The "anything can drive the UI" command bus. One dispatch point for LLM tool
|
|
calls, scripts, and (optional) remote control over WebSocket — all funnel
|
|
through the same handlers, run against the same DOM contract.
|
|
|
|
Pairs with [`lib-aifirst-shell-ui`](https://git.sky-ai.com/CremaUIStudio/lib-aifirst-shell-ui)
|
|
which provides a `[data-action]`-tagged shell. Non-AI apps that want
|
|
scriptable / e2e-testable UIs can use this lib alone.
|
|
|
|
## Public API
|
|
|
|
```ts
|
|
import {
|
|
commandBus, // singleton
|
|
CommandBusProvider, // mounts cursor + registers `navigate`
|
|
parseScript, // DSL ↔ JSON
|
|
runScript, runScriptText,
|
|
buildSystemPrompt, // LLM bridge
|
|
extractActionBlocks, runActionBlocks,
|
|
estimateTokens, trimMessages,
|
|
connectCommandSocket, // optional WebSocket producer
|
|
} from "@crema/action-bus"
|
|
```
|
|
|
|
## Usage
|
|
|
|
Wrap your app:
|
|
|
|
```tsx
|
|
// app/root.tsx
|
|
import { CommandBusProvider } from "@crema/action-bus"
|
|
|
|
export default function App() {
|
|
return (
|
|
<CommandBusProvider>
|
|
<Outlet />
|
|
</CommandBusProvider>
|
|
)
|
|
}
|
|
```
|
|
|
|
Tag interactive elements:
|
|
|
|
```tsx
|
|
<button data-action="save-button">Save</button>
|
|
<input data-action="search-input" />
|
|
```
|
|
|
|
Drive the UI:
|
|
|
|
```ts
|
|
commandBus.dispatch({ type: "click", target: "save-button" })
|
|
commandBus.dispatch({ type: "fill", target: "search-input", value: "acme" })
|
|
|
|
// Or as text DSL:
|
|
import { runScriptText } from "@crema/action-bus"
|
|
runScriptText(`
|
|
navigate /resources
|
|
wait_for resources-table
|
|
fill search-input "acme"
|
|
`)
|
|
```
|
|
|
|
## Built-in commands
|
|
|
|
| Command | Args | Purpose |
|
|
|---|---|---|
|
|
| `navigate` | `path` | React Router navigation |
|
|
| `click` | `target` | Click `[data-action=target]` |
|
|
| `fill` | `target`, `value` | Set input value, fire input + change events |
|
|
| `submit` | `target` | Submit the form containing target |
|
|
| `select` | `target`, `value` | Set `<select>` value |
|
|
| `wait` | `ms` | Sleep |
|
|
| `wait_for` | `target`, `timeout?` | Poll until element exists |
|
|
| `scroll` | `target?` | Scroll element into view |
|
|
| `read` | `target?` | Return innerText |
|
|
| `expect` | `target`, `op`, `value?` | `to_contain` / `to_be_visible` / `to_have_value` |
|
|
| `set` | `name`, `value` | Set a variable |
|
|
|
|
Register your own:
|
|
|
|
```ts
|
|
commandBus.register("highlight", async (cmd) => {
|
|
document.querySelector(`[data-action="${cmd.target}"]`)?.classList.add("hot")
|
|
})
|
|
```
|
|
|
|
## DSL syntax
|
|
|
|
Plain text, one command per line. Quote values with spaces. `#` starts a
|
|
comment. `# speed: <n>` at the top adjusts cursor animation. `$name = <cmd>`
|
|
captures a result; `$name` interpolates.
|
|
|
|
```
|
|
# speed: 0.7
|
|
|
|
click sidebar-toggle
|
|
wait 500
|
|
$id = read row-1
|
|
click $id
|
|
expect detail-panel to_contain "Acme"
|
|
```
|
|
|
|
Round-trips: `parseScript(dsl)` → `{ options, commands }`, `stringifyScript(commands, options)` → text.
|
|
|
|
## LLM integration
|
|
|
|
```ts
|
|
import { buildSystemPrompt, runActionBlocks } from "@crema/action-bus"
|
|
|
|
// 1. Inject into the model's system prompt — includes a tight DSL ref +
|
|
// every visible [data-action] on screen right now.
|
|
const system = buildSystemPrompt({ path: window.location.pathname })
|
|
|
|
// 2. Extract and run any ```action ... ``` blocks the model emits.
|
|
const { ran, errors } = await runActionBlocks(message.content)
|
|
```
|
|
|
|
Plus token-budgeting helpers (`estimateTokens`, `trimMessages`) for keeping
|
|
chat history within a model's context window.
|
|
|
|
See [comfy-cloud's `docs/AI_FIRST.md`](https://git.sky-ai.com/CremaUIStudio/crema-app-aifirst-template/raw/branch/main/docs/AI_FIRST.md) for the full system tour.
|
|
|
|
## Visible cursor
|
|
|
|
By default, `CommandBusProvider` hooks a virtual cursor into the bus —
|
|
animates to the target element before each command, ripples on click. Makes
|
|
LLM-driven sessions legible to a watching human and gives demo recordings
|
|
for free. Speed is controlled by the `# speed:` directive in DSL scripts;
|
|
the `setCursorSpeed(n)` API sets it programmatically.
|
|
|
|
To run silently (e.g. tests), pass `{ silent: true }` to `dispatch()` /
|
|
`run()`.
|
|
|
|
## Optional WebSocket producer
|
|
|
|
```ts
|
|
import { connectCommandSocket } from "@crema/action-bus"
|
|
const sock = connectCommandSocket("ws://localhost:9229/ui")
|
|
// sock.close() to disconnect
|
|
```
|
|
|
|
Server messages: `{ id?, command }` | `{ id?, script }` | `{ id?, dsl }`.
|
|
Replies with `{ id, ok: true }` or `{ id, ok: false, error }`.
|
|
|
|
Not auto-connected — call when ready. **Add origin checks and an opt-in
|
|
toggle in production**; the lib provides the transport, not the policy.
|
|
|
|
## Conventions
|
|
|
|
- **`[data-action]` is the contract.** Every interactive element opts in by
|
|
declaring an id. The bus introspects the DOM at dispatch time — no central
|
|
registry to maintain.
|
|
- **No global state besides the singleton.** The bus is a class; you can
|
|
instantiate your own if you want isolation.
|
|
- **Theme-agnostic.** Cursor and ripple read CSS variables (`--primary`),
|
|
fall back to neutral colors.
|
|
|
|
## Peer dependencies
|
|
|
|
- `react`
|
|
- `react-router` (for `CommandBusProvider`'s `navigate` handler)
|
|
|
|
Both come from the consuming app.
|