canvas
Canvas Skill
Display HTML content on connected Otto nodes (Mac app, iOS, Android).
Overview
The canvas tool lets you present web content on any connected node's canvas view. Great for:
- Displaying games, visualizations, dashboards
- Showing generated HTML content
- Interactive demos
How It Works
Architecture
┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Canvas Host │────▶│ Node Bridge │────▶│ Node App │
│ (HTTP Server) │ │ (TCP Server) │ │ (Mac/iOS/ │
│ Port 18793 │ │ Port 18790 │ │ Android) │
└─────────────────┘ └──────────────────┘ └─────────────┘
- Canvas Host Server: Serves static HTML/CSS/JS files from
canvasHost.rootdirectory - Node Bridge: Communicates canvas URLs to connected nodes
- Node Apps: Render the content in a WebView
Tailscale Integration
The canvas host server binds based on gateway.bind setting:
| Bind Mode | Server Binds To | Canvas URL Uses |
|---|---|---|
loopback |
127.0.0.1 | localhost (local only) |
lan |
LAN interface | LAN IP address |
tailnet |
Tailscale interface | Tailscale hostname |
auto |
Best available | Tailscale > LAN > loopback |
Key insight: The canvasHostHostForBridge is derived from bridgeHost. When bound to Tailscale, nodes receive URLs like:
http://<tailscale-hostname>:18793/__otto__/canvas/<file>.html
This is why localhost URLs don't work - the node receives the Tailscale hostname from the bridge!
Actions
| Action | Description |
|---|---|
present |
Show canvas with optional target URL |
hide |
Hide the canvas |
navigate |
Navigate to a new URL |
eval |
Execute JavaScript in the canvas |
snapshot |
Capture screenshot of canvas |
Configuration
In ~/.otto/otto.json:
{
"canvasHost": {
"enabled": true,
"port": 18793,
"root": "/Users/you/clawd/canvas",
"liveReload": true
},
"gateway": {
"bind": "auto"
}
}
Live Reload
When liveReload: true (default), the canvas host:
- Watches the root directory for changes (via chokidar)
- Injects a WebSocket client into HTML files
- Automatically reloads connected canvases when files change
Great for development!
Workflow
1. Create HTML content
Place files in the canvas root directory (default ~/clawd/canvas/):
cat > ~/clawd/canvas/my-game.html << 'HTML'
<!DOCTYPE html>
<html>
<head><title>My Game</title></head>
<body>
<h1>Hello Canvas!</h1>
</body>
</html>
HTML
2. Find your canvas host URL
Check how your gateway is bound:
cat ~/.otto/otto.json | jq '.gateway.bind'
Then construct the URL:
- loopback:
http://127.0.0.1:18793/__otto__/canvas/<file>.html - lan/tailnet/auto:
http://<hostname>:18793/__otto__/canvas/<file>.html
Find your Tailscale hostname:
tailscale status --json | jq -r '.Self.DNSName' | sed 's/\.$//'
3. Find connected nodes
otto nodes list
Look for Mac/iOS/Android nodes with canvas capability.
4. Present content
canvas action:present node:<node-id> target:<full-url>
Example:
canvas action:present node:mac-63599bc4-b54d-4392-9048-b97abd58343a target:http://peters-mac-studio-1.sheep-coho.ts.net:18793/__otto__/canvas/snake.html
5. Navigate, snapshot, or hide
canvas action:navigate node:<node-id> url:<new-url>
canvas action:snapshot node:<node-id>
canvas action:hide node:<node-id>
Debugging
White screen / content not loading
Cause: URL mismatch between server bind and node expectation.
Debug steps:
- Check server bind:
cat ~/.otto/otto.json | jq '.gateway.bind' - Check what port canvas is on:
lsof -i :18793 - Test URL directly:
curl http://<hostname>:18793/__otto__/canvas/<file>.html
Solution: Use the full hostname matching your bind mode, not localhost.
"node required" error
Always specify node:<node-id> parameter.
"node not connected" error
Node is offline. Use otto nodes list to find online nodes.
Content not updating
If live reload isn't working:
- Check
liveReload: truein config - Ensure file is in the canvas root directory
- Check for watcher errors in logs
URL Path Structure
The canvas host serves from /__otto__/canvas/ prefix:
http://<host>:18793/__otto__/canvas/index.html → ~/clawd/canvas/index.html
http://<host>:18793/__otto__/canvas/games/snake.html → ~/clawd/canvas/games/snake.html
The /__otto__/canvas/ prefix is defined by CANVAS_HOST_PATH constant.
Tips
- Keep HTML self-contained (inline CSS/JS) for best results
- Use the default index.html as a test page (has bridge diagnostics)
- The canvas persists until you
hideit or navigate away - Live reload makes development fast - just save and it updates!
- A2UI JSON push is WIP - use HTML files for now
More from elizaos/eliza
nano-pdf
Edits PDF files using natural-language instructions via the nano-pdf CLI. Supports modifying text, changing titles, fixing typos, and updating content on specific pages. Use when the user wants to edit a PDF, modify PDF content, update PDF text, fix a typo in a PDF, change a PDF title, or rewrite part of a PDF page.
30wacli
Send WhatsApp messages to other people or search/sync WhatsApp history via the wacli CLI (not for normal user chats). Use when the user asks to send a WhatsApp message, text someone on WhatsApp, search WhatsApp chat history, sync WhatsApp conversations, backfill message history, or forward a file via WhatsApp to a third party.
27nano-banana-pro
Generate or edit images via Gemini 3 Pro Image (Nano Banana Pro). Use when the user asks to create an image, generate a picture, produce AI-generated artwork, edit a photo, compose multiple images, or upscale an image to higher resolution. Supports text-to-image generation, single-image editing, and multi-image composition using the Gemini API.
27obsidian
Work with Obsidian vaults (plain Markdown notes) and automate via obsidian-cli. Use when the user asks about notes, vault management, PKM, knowledge base organization, wikilinks, or personal knowledge management in Obsidian.
25session-logs
Search and analyze session logs (older/parent conversations) stored as JSONL files using jq and rg. Use when the user asks about prior chats, previous conversations, conversation history, what was said before, session costs, token usage, or tool usage breakdown across past sessions.
24discord
Use when you need to control Discord from Otto via the discord tool: send messages, react, post or upload stickers, upload emojis, run polls, manage threads/pins/search, create/edit/delete channels and categories, fetch permissions or member/role/channel info, set bot presence/activity, or handle moderation actions in Discord DMs or channels.
24