| name | canvas |
| description | Update the browser UI, show progress, display data, or collect user input via the `canvas` CLI. Checks that the channel is attached and instructs the user how to recover if it isn't. |
agent-canvas
A channel plugin that gives this session a live browser UI. You drive it via the canvas CLI; user clicks and form submits flow back as <channel source="agent-canvas"> events.
Is the channel attached?
Look for a channel event from agent-canvas in your conversation context:
<channel source="agent-canvas" ...>agent-canvas running at http://0.0.0.0:8765</channel>
If you see it: the channel is connected. Extract the port, give the user the Tailscale FQDN URL, proceed.
If you do NOT see it: STOP. Tell the user verbatim:
"This session does not have an agent-canvas channel attached. Restart with cl (or your equivalent that passes --channels plugin:agent-canvas@agent-canvas-marketplace --dangerously-load-development-channels). I can't recover this in-session."
Do not run any canvas commands. Do not write to .canvas/ui-state.json. Exit.
How the channel attaches
Claude Code reads the plugin's .mcp.json at session startup, spawns start.sh, which runs bun channel/index.ts. The channel attaches via stdio MCP and emits the startup notification above. There is no manual or fallback startup path.
Updating the canvas
Use the canvas CLI for all updates. Set CANVAS once and reuse:
CANVAS="$HOME/.claude/plugins/marketplaces/agent-canvas/scripts/canvas.sh"
bash "$CANVAS" init --title "Build watcher" --theme dark
bash "$CANVAS" html status "<span class='badge badge-success badge-lg'>Running</span>"
bash "$CANVAS" log build info "Starting build"
bash "$CANVAS" log build success "Done"
bash "$CANVAS" markdown summary "## Results\n\nNo issues."
bash "$CANVAS" remove status
bash "$CANVAS" refresh
Commands
| Command | Effect |
|---|
canvas init [--title T] [--theme T] [--layout T] | Create state file with defaults if missing |
canvas meta [--title T] [--theme T] [--layout T] | Update top-level metadata fields |
canvas html <id> "<html>" | Upsert html component |
canvas markdown <id> "<md>" | Upsert markdown component |
canvas form <id> '<json>' (or @file.json, or - for stdin) | Upsert form |
canvas log <id> <level> "<msg>" | Append to log (creates if missing) |
canvas remove <id> | Remove component by id |
canvas refresh | Re-read file, re-render, push SSE |
canvas show | Print current state JSON |
canvas url | Print Tailscale FQDN URL with port |
canvas status | Print {channel_attached, port, ...} |
Upsert semantics: same id updates in place; new id appends. Type changes (e.g., canvas markdown foo after canvas html foo) overwrite.
For html/markdown content with quoting headaches, pipe via stdin: echo "<div>...</div>" | bash "$CANVAS" html status -.
Async input model
Clicks (data-input-event="...") and form submits arrive as <channel source="agent-canvas"> events whenever the user interacts. They may arrive in the middle of unrelated work — read what arrived, decide whether to react now or queue.
Event shapes:
- Custom click:
<channel ... event="custom" component_id="actions" event_name="confirm">User clicked confirm on actions</channel>
- Form submit:
<channel ... event="form_submit" component_id="config">Form submitted: config\n{"env":"prod","verbose":true}</channel>
Giving the user the URL
Always provide the Tailscale FQDN — works from any device on the tailnet:
bash "$CANVAS" url
Example output: http://josh-office.stork-spica.ts.net:8765
State file structure
canvas show produces this shape (kept here as a reference for canvas refresh after hand-edits or for component-type details):
{
"version": 1,
"title": "Page Title",
"theme": "dark",
"layout": "stack",
"components": []
}
- layout:
"stack" (vertical), "grid" (2-column), or "sidebar" (1/3 + 2/3)
- theme: any DaisyUI theme (
dark, light, cupcake, synthwave, etc.)
Component types
html — primary
Raw HTML with full DaisyUI + Tailwind classes. Make elements interactive with data-input-event:
bash "$CANVAS" html actions \
"<button class='btn btn-primary' data-input-event='confirm'>Confirm</button>"
markdown
Renders markdown to HTML. Supports raw HTML passthrough.
form
Structured fields, JSON-defined. Field types: text, textarea, number, select, checkbox, radio, range, date.
bash "$CANVAS" form config '{
"title": "Settings",
"fields": [
{"id":"env","label":"Env","type":"select","options":["dev","prod"],"default":"dev"},
{"id":"verbose","label":"Verbose","type":"checkbox","default":true}
],
"submitLabel": "Apply"
}'
log — append-only panel
Levels: info, success, warning, error. Use canvas log <id> <level> "<msg>" to append.
Local files
Reference local files via /files/ + absolute path:
bash "$CANVAS" html photo "<img src='/files/home/josh/images/photo.png' class='rounded-lg'>"
Design
The UI uses DaisyUI + Tailwind from CDN. Make it look intentional, not default.
- Theme: pick to fit the project mood.
dark is safe, synthwave/cyberpunk for personality, corporate/lofi for clean utility.
- Layout:
grid with DaisyUI stats for dashboards. sidebar for persistent nav. Mix component types.
- Hierarchy: lead with the most important info. DaisyUI badges for status,
text-2xl/stat-value for key numbers, text-base-content/60 for secondary info.
- Cards: wrap related content in
card bg-base-200 shadow-lg. Use card-actions for buttons.
- Spacing: container has
px-6 md:px-10; layout adds gap-6; inside cards use card-body. Generous whitespace > cramped layouts.
- Color with purpose: DaisyUI variants (
success, error, warning, info, primary, secondary, accent) carry meaning. Use them to communicate status, not decoration.
Tips
- Hand-edit
.canvas/ui-state.json directly if you need something the CLI doesn't cover — then run canvas refresh.
- Set
"visible": false on any component to hide without removing.
- Unknown component types are silently ignored.
- DaisyUI components work out of the box: badges, cards, stats, tables, steps, alerts, tabs, navbars, drawers, etc.
- For multi-view UIs, use DaisyUI tabs with radio buttons (pure CSS, no round-trip).