A cross-platform canvas server for OpenClaw. Serves HTML content, renders A2UI v0.9 surfaces, and provides a WebSocket gateway for agent-driven UI control.
npm run setup
npm run build
npm startOpen http://localhost:3456 in a browser.
packages/
├── a2ui-sdk/ # @haliphax-openclaw/a2ui-sdk
│ └── src/
│ ├── index.ts # Barrel exports
│ ├── types.ts # Shared types (A2UISurfaceState, DataSource, PackageDefinition, etc.)
│ ├── filters.ts # applyFilters, computeAggregate, formatCompact
│ ├── ws.ts # sendEvent / registerWsSend
│ ├── composables/ # useDataSource, useFilterBind, useOptionsFrom, useSortable
│ └── utils/ # format-string, deep-link
├── a2ui-catalog-basic/ # @haliphax-openclaw/a2ui-catalog-basic
│ ├── catalog.json # JSON Schema catalog definition
│ └── src/
│ ├── index.ts # PackageDefinition
│ └── *.vue # Component implementations
├── a2ui-catalog-extended/ # @haliphax-openclaw/a2ui-catalog-extended
│ ├── catalog.json
│ └── src/
│ ├── index.ts # PackageDefinition
│ └── *.vue
├── a2ui-catalog-all/ # @haliphax-openclaw/a2ui-catalog-all
│ ├── catalog.json
│ └── src/
│ └── index.ts # Meta-catalog — re-exports basic + extended
src/
├── build/
│ └── vite-plugin-catalogs.ts # Vite plugin — discovers catalogs, generates virtual:openclaw-catalogs
├── server/
│ ├── index.ts # Express server, startup, shutdown
│ ├── services/
│ │ ├── gateway.ts # WebSocket server (/gateway for agents, /ws for SPA)
│ │ ├── session-manager.ts
│ │ ├── file-resolver.ts # Path resolution with traversal guard
│ │ ├── file-watcher.ts # chokidar live reload
│ │ ├── jsonl-watcher.ts # JSONL file watcher for A2UI auto-push
│ │ ├── node-client.ts # OpenClaw gateway node registration (Ed25519 auth, invoke handling)
│ │ ├── a2ui-manager.ts # A2UI surface state (in-memory cache, backed by a2ui-store)
│ │ ├── a2ui-store.ts # SQLite persistence for A2UI surfaces (better-sqlite3)
│ │ ├── a2ui-pipeline.ts # A2UI command processing pipeline
│ │ ├── a2ui-commands.ts # v0.8 → v0.9 normalization layer
│ │ └── catalog-registry.ts # Discovers catalog packages in node_modules/
│ ├── shared/
│ │ ├── deep-link-script.ts # Injected script for openclaw:// deep links
│ │ └── snapshot-script.ts # Injected script for canvas snapshots
│ ├── commands/
│ │ ├── canvas.ts # show, hide, navigate, navigateExternal, eval, snapshot
│ │ └── a2ui.ts # push (JSONL), reset
│ └── routes/
│ ├── canvas.ts # GET /:session/:path
│ ├── catalogs.ts # GET /api/catalogs
│ ├── canvas-config.ts # GET /api/canvas-config
│ ├── agent-proxy.ts # POST /api/agent → gateway /tools/invoke
│ ├── file-spawn.ts # POST /api/file-spawn → read prompt → sessions_spawn
│ └── scaffold.ts # GET /scaffold
├── client/
│ ├── main.ts # Vue app entry, wires registerWsSend
│ ├── router.ts # Vue Router
│ ├── virtual-openclaw-catalogs.d.ts # Type declaration for virtual module
│ ├── views/
│ │ ├── CanvasView.vue # Main canvas — iframe, A2UI, external URLs
│ │ └── ScaffoldView.vue # Placeholder when no index.html
│ ├── components/
│ │ ├── A2UINode.vue # Component resolver (catalog-based two-tier lookup)
│ │ ├── A2UIRenderer.vue # Surface renderer (DaisyUI theming via data-theme)
│ │ └── DeepLinkConfirm.vue # Deep link confirmation dialog
│ ├── store/
│ │ ├── index.ts # Vuex root
│ │ └── a2ui.ts # A2UI surface state (surfaces, components, dataModel, theme, catalogId)
│ ├── services/
│ │ ├── ws-client.ts # Browser WebSocket client
│ │ ├── url-rewriter.ts # openclaw-canvas:// URL rewriter
│ │ └── deep-link.ts # Deep link handling
│ ├── utils/
│ │ ├── format-string.ts # String formatting utilities
│ │ └── url-schemes.ts # URL scheme parser (openclaw://, openclaw-fileprompt://, openclaw-canvas://)
│ └── styles/
│ ├── custom.css # Custom styleeet
│ └── tailwind.css # Tailwind + DaisyUI (all themes)
test/ # vitest tests
This project uses npm workspaces. All packages live in packages/:
| Package | Description |
|---|---|
@haliphax-openclaw/a2ui-sdk |
Component SDK — types, composables, filters, event helpers |
@haliphax-openclaw/a2ui-catalog-basic |
Basic catalog — Column, Row, Text, Button, Image, Tabs, Divider, Slider, Checkbox, ChoicePicker |
@haliphax-openclaw/a2ui-catalog-extended |
Extended catalog — Badge, Table, Stack, Spacer, ProgressBar, Repeat, Accordion |
@haliphax-openclaw/a2ui-catalog-all |
All catalog — re-exports basic + extended |
Install all dependencies (including workspace packages) from the repo root:
npm cinpm automatically symlinks workspace packages into node_modules/, so cross-package imports resolve locally during development.
- Create a directory under
packages/(e.g.,packages/a2ui-my-catalog/) - Add a
package.jsonwith theopenclaw-canvas-webfield pointing to yourcatalog.jsonand entry module - Run
npm installat the root to link the new workspace - Restart the dev server — the Vite catalog plugin discovers it automatically
See docs/creating-catalog-packages.md for the full guide on authoring catalog packages.
The canvas server registers as an OpenClaw gateway node, exposing a tool interface that agents can access via openclaw node invoke. On startup, it connects to the gateway WebSocket, authenticates with Ed25519 signatures, and advertises the following commands:
| Command | Description |
|---|---|
canvas.present |
Show/present canvas content |
canvas.hide |
Hide the canvas panel |
canvas.navigate |
Navigate to a canvas session/path or external URL |
canvas.eval |
Execute JavaScript in the canvas context |
canvas.snapshot |
Capture the current canvas as a base64 PNG |
canvas.a2ui.push |
Push A2UI surface commands (structured) |
canvas.a2ui.pushJSONL |
Push A2UI JSONL payload (string) |
canvas.a2ui.reset |
Clear A2UI surface state |
Node identity (Ed25519 keypair and device ID) is generated on first run and stored at ~/.openclaw-canvas/node-identity.json. The gateway URL and auth token are read from environment variables or openclaw.json.
The project includes an MCP server that agents can invoke using mcporter which exposes all of the same commands as the gateway node interface without the node-routing context overhead.
A complementary Canvas agent skill is available with usage instructions, JSONL command reference, component docs, and examples for agents interacting with this server. The skill expects that the MCP server is configured.
Each canvas session is accessed via its session ID in the URL path:
http://<host>:<port>/<sessionId>/
For example:
http://localhost:3456/main/— the defaultmainsessionhttp://localhost:3456/developer/— thedevelopersession
When running behind a reverse proxy with a base path (e.g., OPENCLAW_CANVAS_BASE_PATH=/canvas):
https://example.com/canvas/developer/
The root path (/) redirects to /main/ by default.
| Command | Description |
|---|---|
npm run build |
Build the Vue SPA to dist/client/ |
npm run clean |
Delete build artifacts and dependencies |
npm run dev |
Run server + Vite dev server concurrently |
npm run setup |
Cleanly install all dependencies for all projects |
npm start |
Start the production server |
npm test |
Run tests (vitest) |
| Variable | Default | Description |
|---|---|---|
OPENCLAW_CANVAS_HOST |
0.0.0.0 |
Bind address |
OPENCLAW_CANVAS_PORT |
3456 |
Listen port |
OPENCLAW_CANVAS_BASE_PATH |
/ |
Public base path when behind a reverse proxy (e.g. /canvas) |
OPENCLAW_CANVAS_SKIP_CONFIRM |
false |
Skip deep link confirmation dialog when true |
OPENCLAW_CANVAS_A2UI_DB |
~/.openclaw-canvas/a2ui-cache.db |
Path to SQLite database for A2UI surface persistence |
OPENCLAW_GATEWAY_WS_URL |
ws://127.0.0.1:18789 |
Gateway WebSocket URL for deep link and file-spawn proxying |
OPENCLAW_GATEWAY_TOKEN |
(from openclaw.json) | Gateway auth token for agent deep links and file-spawn (/tools/invoke). Falls back to gateway.auth.token in openclaw.json |
Agent deep links and file-spawn both use the gateway's /tools/invoke endpoint with sessions_spawn. The following settings control deep link behavior:
| Setting | Type | Description |
|---|---|---|
gateway.auth.token |
string |
Bearer token used by the canvas server to authenticate with the gateway. Must match the OPENCLAW_GATEWAY_TOKEN environment variable (or be readable from openclaw.json) |
gateway.tools.allow |
string[] |
Must include "sessions_spawn" to permit agent deep links and file-spawn via /tools/invoke |
Example configuration:
{
"gateway": {
"auth": {
"mode": "token",
"token": "your-gateway-token"
},
"tools": {
"allow": ["sessions_spawn", "sessions_send", "sessions_list"]
}
}
}Without gateway.auth.token and sessions_spawn in gateway.tools.allow, the canvas server's /api/agent and /api/file-spawn proxies will receive an authentication failure or 404 from the gateway.
OpenClaw includes a built-in canvas tool designed for the desktop app. When using the canvas web server, this tool can cause confusion — agents may attempt to use it instead of openclaw nodes invoke, and its jsonlPath parameter rejects paths outside the OpenClaw state directory. To prevent this, add canvas to the global tool denylist:
{
"tools": {
"deny": ["canvas"]
}
}Connect via WebSocket to /gateway. Send JSON messages with a command field. Responses include the original id if provided.
canvas.show — Show the canvas panel.
{ "id": "1", "command": "canvas.show", "session": "my-project" }canvas.hide — Hide the canvas panel.
{ "id": "2", "command": "canvas.hide" }canvas.navigate — Navigate to a session/path.
{
"id": "3",
"command": "canvas.navigate",
"session": "demo",
"path": "page.html"
}canvas.navigateExternal — Load an external URL (http/https only).
{
"id": "4",
"command": "canvas.navigateExternal",
"url": "https://example.com"
}canvas.eval — Evaluate JS in the canvas iframe.
{ "id": "5", "command": "canvas.eval", "js": "document.title" }canvas.snapshot — Capture the canvas as a base64 PNG. A snapshot helper script (using dom-to-image-more) is injected into canvas HTML at serve time — the same pattern as deep link injection. When a snapshot is requested, the parent SPA sends a postMessage to the iframe, the injected script captures document.body from within the frame, and sends the image back via postMessage. This works for same-origin files and data: URLs. External cross-origin URLs cannot be captured. Falls back to parent-level DOM capture for A2UI surfaces. 30s timeout.
{ "id": "6", "command": "canvas.snapshot" }
→ { "id": "6", "ok": true, "image": "data:image/png;base64,..." }a2ui.push — Push A2UI JSONL payload.
{
"id": "7",
"command": "a2ui.push",
"payload": "{\"updateComponents\":{...}}\n{\"createSurface\":{...}}"
}a2ui.reset — Clear all A2UI surfaces.
{ "id": "8", "command": "a2ui.reset" }Trigger agent runs from links inside canvas HTML. When a user clicks an openclaw:// link in the canvas iframe, a confirmation dialog appears, and on approval the request is proxied to the gateway to start an agent run.
<a href="openclaw://agent?message=run+my+task">Run Task</a>See docs/deep-linking.md for the full URL format, parameters, confirmation dialog, script injection details, and security considerations.
Spawn a subagent whose task is the contents of a file. The path after the scheme identifies the file (not ?file=). The server resolves it under <agent workspace>/canvas when agentId matches a configured agent, otherwise under OPENCLAW_CANVAS_ROOT. See docs/deep-linking.md.
<a href="openclaw-fileprompt://jsonl/deploy-notes.md?agentId=developer">Deploy</a>Reference files in other canvas sessions without hardcoding the server origin or base path. The SPA rewrites these URLs at runtime to the correct /_c/<session>/<path> route.
Format: openclaw-canvas://<session>/<path>
Example:
<img src="openclaw-canvas://my-project/logo.png" />
<a href="openclaw-canvas://dashboard/index.html">Open Dashboard</a>These are rewritten to http(s)://<host>:<port>/<base>/_c/<session>/<path> based on the current origin and OPENCLAW_CANVAS_BASE_PATH.
| Endpoint | Method | Description |
|---|---|---|
/api/agent |
POST | Proxies deep link requests to the gateway's /tools/invoke endpoint (sessions_spawn) |
/api/file-spawn |
POST | Reads a prompt file from <agent>/canvas or OPENCLAW_CANVAS_ROOT; spawns via /tools/invoke (sessions_spawn) — see docs/deep-linking.md |
/api/canvas-config |
GET | Returns canvas configuration for the SPA |
Returns configuration used by the SPA for deep link handling.
{
"skipConfirmation": false,
"agents": ["main", "openclaw-expert", "developer"],
"allowedAgentIds": ["main", "openclaw-expert"]
}skipConfirmation— controlled byOPENCLAW_CANVAS_SKIP_CONFIRMenv varagents— agent IDs read fromopenclaw.jsonallowedAgentIds— agents allowed for deep link execution, fromhooks.allowedAgentIds
When a user clicks an openclaw:// link in canvas content, a confirmation dialog appears (unless OPENCLAW_CANVAS_SKIP_CONFIRM=true or the URL includes a key parameter).
The dialog includes a collapsible "Options" section with controls for:
- Agent — dropdown populated from the canvas config API
- Model — free-text input
- Thinking — on / off / stream
- Session Key — free-text input
Place HTML/CSS/JS files in the agent's canvas/ workspace directory. The server serves them at /<session>/<path>. File changes trigger live reload in the browser.
A2UI surface state is persisted to a local SQLite database so it survives server restarts. On startup, all cached surfaces are loaded from the database and replayed to connecting SPA clients.
- The database is managed by
A2UIStore(better-sqlite3, synchronous) - The in-memory
MapinA2UIManagerremains the primary data source; SQLite is the backing store - Every mutation (
upsertSurface,setRoot,updateDataModel,deleteSurface,clearAll) writes through to SQLite - DB location defaults to
~/.openclaw-canvas/a2ui-cache.db, configurable viaOPENCLAW_CANVAS_A2UI_DB
The server includes a normalization layer (src/server/services/a2ui-commands.ts) that auto-converts v0.8 commands and component shapes to v0.9 format with deprecation warnings logged. v0.8 payloads still work but are deprecated:
| v0.8 (deprecated) | v0.9 |
|---|---|
surfaceUpdate |
updateComponents |
beginRendering |
createSurface |
dataModelUpdate |
updateDataModel |
usageHint (Text prop) |
variant |
Wrapped component shape: { id, component: { "Text": { "text": "..." } } } |
Flat component shape: { id, component: "Text", "text": "..." } |
dataSourcePush and deleteSurface are unchanged.
A2UI surfaces support a reactive data-binding layer that lets agents push structured data sources and bind UI components to live, filterable data.
Key capabilities:
- Data Sources — Push named datasets via
updateDataModel(with$sources) or thedataSourcePushJSONL shorthand. Supports incremental merges withprimaryKey. - Filtering — Select and MultiSelect components can
bindto data sources, applying filter operations (eq,contains,gte,lte,range,in) that reactively update all bound displays. Clearing a MultiSelect shows all data. - Sorting — Table and Repeat components support optional sorting via the
sortableprop. Tables sort by clicking column headers (⬆/⬇ indicators); Repeat components include a sort direction dropdown. Sorting operates on raw data values. - Display Binding — Table, Badge, and Text components accept a
dataSourceprop for dynamic content with built-in aggregates (count,sum,avg,min,max) and compact number formatting. - Repeat — The Repeat component iterates over filtered rows, rendering a template per row with
{{field}}placeholders and transforms likepercentOfMax.
See docs/a2ui-reactive.md for the full data binding guide and docs/components.md for the complete component reference.
The canvas web server provides feature parity with the macOS OpenClaw app's canvas panel, with a few browser-inherent limitations:
file://URLs — The macOS app supportsfile://URLs in canvas.navigate. Browsers block these for security reasons. Useopenclaw-canvas://orhttp(s)URLs instead.- Snapshot fidelity — The macOS app captures snapshots natively via WKWebView. The web server injects
dom-to-image-moreinto canvas HTML and captures from within the iframe viapostMessage. This works for locally-served canvas files anddata:URLs. External cross-origin URLs (http/https from other domains) cannot be captured since the snapshot script can't be injected into third-party content. - Panel geometry — The macOS app's canvas is a floating, resizable panel sharing screen space with the menu bar, webchat, and voice overlay. It supports
canvas.geometrycommands and persists size/position per session. The web server omits this — the canvas owns the full browser tab, so viewport sizing is handled by the browser itself.
Features available in the web server that are not present in the macOS app:
data:URL support —canvas.presentandcanvas.navigateacceptdata:text/htmlURLs with automatic deep link and snapshot script injection.openclaw-fileprompt://deep links — Spawn subagents with prompts loaded from files under<agent>/canvas(URL path after the scheme). The server reads the file and passes its contents as the task. See Custom URL Protocols.- Enhanced confirmation dialog — Collapsible "Options" section with controls for agent, model, thinking mode, and session key.
- Skip confirmation globally —
OPENCLAW_CANVAS_SKIP_CONFIRM=trueenv var bypasses the deep link confirmation dialog for all requests. - Canvas config API —
GET /api/canvas-configexposes available agents and configuration to the SPA.
Public Domain. See LICENSE.