Display and control HTML content on connected Bitterbot nodes (Mac, iOS, Android) via the canvas host server. Use when presenting games, visualizations, dashboards, or interactive demos on a connected device, navigating canvas URLs, capturing canvas snapshots, or debugging canvas connectivity.
71
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
Display HTML content on connected Bitterbot nodes (Mac app, iOS, Android).
The canvas tool lets you present web content on any connected node's canvas view. Great for:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Canvas Host │────▶│ Node Bridge │────▶│ Node App │
│ (HTTP Server) │ │ (TCP Server) │ │ (Mac/iOS/ │
│ Port 18793 │ │ Port 18790 │ │ Android) │
└─────────────────┘ └──────────────────┘ └─────────────┘canvasHost.root directoryThe 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/__bitterbot__/canvas/<file>.htmlThis is why localhost URLs don't work - the node receives the Tailscale hostname from the bridge!
| 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 |
In ~/.bitterbot/bitterbot.json:
{
"canvasHost": {
"enabled": true,
"port": 18793,
"root": "/Users/you/bitterbot/canvas",
"liveReload": true
},
"gateway": {
"bind": "auto"
}
}When liveReload: true (default), the canvas host:
Great for development!
Place files in the canvas root directory (default ~/bitterbot/canvas/):
cat > ~/bitterbot/canvas/my-game.html << 'HTML'
<!DOCTYPE html>
<html>
<head><title>My Game</title></head>
<body>
<h1>Hello Canvas!</h1>
</body>
</html>
HTMLCheck how your gateway is bound:
cat ~/.bitterbot/bitterbot.json | jq '.gateway.bind'Then construct the URL:
http://127.0.0.1:18793/__bitterbot__/canvas/<file>.htmlhttp://<hostname>:18793/__bitterbot__/canvas/<file>.htmlFind your Tailscale hostname:
tailscale status --json | jq -r '.Self.DNSName' | sed 's/\.$//'bitterbot nodes listLook for Mac/iOS/Android nodes with canvas capability.
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/__bitterbot__/canvas/snake.htmlcanvas action:navigate node:<node-id> url:<new-url>
canvas action:snapshot node:<node-id>
canvas action:hide node:<node-id>Cause: URL mismatch between server bind and node expectation.
Debug steps:
cat ~/.bitterbot/bitterbot.json | jq '.gateway.bind'lsof -i :18793curl http://<hostname>:18793/__bitterbot__/canvas/<file>.htmlSolution: Use the full hostname matching your bind mode, not localhost.
Always specify node:<node-id> parameter.
Node is offline. Use bitterbot nodes list to find online nodes.
If live reload isn't working:
liveReload: true in configThe canvas host serves from /__bitterbot__/canvas/ prefix:
http://<host>:18793/__bitterbot__/canvas/index.html → ~/bitterbot/canvas/index.html
http://<host>:18793/__bitterbot__/canvas/games/snake.html → ~/bitterbot/canvas/games/snake.htmlThe /__bitterbot__/canvas/ prefix is defined by CANVAS_HOST_PATH constant.
hide it or navigate awaye73c717
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.