From agent-canvas
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/agent-canvas:canvasThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
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.
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.
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
| 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 -.
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:
<channel ... event="custom" component_id="actions" event_name="confirm">User clicked confirm on actions</channel><channel ... event="form_submit" component_id="config">Form submitted: config\n{"env":"prod","verbose":true}</channel>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
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": []
}
"stack" (vertical), "grid" (2-column), or "sidebar" (1/3 + 2/3)dark, light, cupcake, synthwave, etc.)html — primaryRaw 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>"
markdownRenders markdown to HTML. Supports raw HTML passthrough.
formStructured 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 panelLevels: info, success, warning, error. Use canvas log <id> <level> "<msg>" to append.
Reference local files via /files/ + absolute path:
bash "$CANVAS" html photo "<img src='/files/home/josh/images/photo.png' class='rounded-lg'>"
The UI uses DaisyUI + Tailwind from CDN. Make it look intentional, not default.
dark is safe, synthwave/cyberpunk for personality, corporate/lofi for clean utility.grid with DaisyUI stats for dashboards. sidebar for persistent nav. Mix component types.text-2xl/stat-value for key numbers, text-base-content/60 for secondary info.card bg-base-200 shadow-lg. Use card-actions for buttons.px-6 md:px-10; layout adds gap-6; inside cards use card-body. Generous whitespace > cramped layouts.success, error, warning, info, primary, secondary, accent) carry meaning. Use them to communicate status, not decoration..canvas/ui-state.json directly if you need something the CLI doesn't cover — then run canvas refresh."visible": false on any component to hide without removing.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub frankjoshua/agent-canvas --plugin agent-canvas