An auto-saved infinite canvas for live Pi Coding Agents. Each node contains a real, interactive Pi terminal; a directed connection represents delegated work, not automatic message passing. Agents can explicitly send messages to a running Pi node via the local API.
Requires Node.js 22.13+ (built-in SQLite), npm and an authenticated pi on your PATH.
npm install
npm run devOpen http://127.0.0.1:5173. Enter an existing working directory's absolute path, or choose one from the saved Pi session folders dropdown, then either launch a new Pi session and type directly in its terminal, or select Resume · pi -r to pick an existing session inside the terminal. Canvas nodes and Pi conversations can be restored after server restart (see below). Newly created agents (New or Resume) are brought into view, selected, and focused so you can type immediately; reopening a stopped node also focuses its terminal. Initial page load and reconnect do not steal focus. Click inside any terminal to switch to it. Use the □ button in an agent's header to expand it to fill the browser window without entering browser fullscreen; click the return icon to go back to the whiteboard without changing its saved size or restarting Pi. Use the bell button in the top bar to opt into browser system notifications when an agent finishes a turn; the browser will ask for permission. The agent name is the notification title and a short plain-text preview of its final response is the body. Notifications are suppressed while its terminal is focused, and clicking an alert brings the agent into view. The bell can also turn notifications off. Keep the Canvas tab open for alerts (no background service worker). Stop ends its process but leaves the node and its output. A stopped node has a Remove button to delete it and its connections from this canvas (not Pi's saved session).
Use the single mouse/touchpad icon button beneath the bottom-left zoom controls to switch modes (the icon shows the current mode): Mouse uses right-drag to pan and the wheel to zoom; Touchpad uses two-finger scroll to pan and pinch to zoom. Drag nodes by their headers, resize selected nodes, and connect right-to-left handles to record delegation. Click a connection's label to edit its display text; the underlying relationship remains a delegation.
Right-click an agent name in its header and choose Copy agent name to copy its full name, or Copy agent UUID to copy the ID used by the API, including when the title is truncated or the agent is maximized. Keyboard users can focus the name and press Shift+F10 or the context-menu key, then use Up/Down to choose an action and Enter to copy. Escape closes the menu; left-dragging the header still moves the node. Works in both Agent Canvas and Card box; clipboard access needs localhost or HTTPS and browser permission.
Use + Note to add a yellow sticky note. Notes display only their body text, with no title. Drag only the paper's top edge to move it, resize the note when selected, and edit its text directly (saved when the editor loses focus). Text scales with the canvas while staying at least 14 screen pixels for readability. Hover over the note to reveal × in the top-right corner and delete it (also available with keyboard focus or on touch devices). Notes are independent of agents and are saved with the Canvas.
The backend still binds only to 127.0.0.1; use Tailscale Serve for HTTPS access from permitted tailnet devices. This is not application authentication: anyone allowed to reach the proxy can control terminals and access files as your OS account. Restrict access to your own user/devices using Tailscale grants/ACLs. Never use Funnel or expose Canvas to the public internet.
With Tailscale installed and connected:
npm run tailscale:setup # read-only inspection; prints hostname and commands
npm run tailscale:setup -- --apply # explicitly enable tailnet-only HTTPS proxy
# Use the exact hostname printed by setup:
AGENT_CANVAS_PUBLIC_ORIGIN=https://your-machine.your-tailnet.ts.net npm run devUse dev mode only for this flow: Serve proxies 127.0.0.1:5173 (Vite), which forwards /api and terminal WebSockets to the loopback backend on port 3001. No build step or npm start is needed. Stop your existing dev process safely before restarting it with the setting; the script never restarts it or changes data/agents. Tailscale may prompt you to authorize HTTPS in your tailnet. Open the printed HTTPS URL, not the raw 100.x IP. Vite modules/HMR, API, SSE, attachments and terminal WebSockets use the same proxy; HTTPS enables secure-context browser features such as notifications.
The single AGENT_CANVAS_PUBLIC_ORIGIN must be an HTTPS origin (no credentials, paths, query, fragment or wildcard). It is added to the existing localhost browser-origin allowlist for HTTP and WebSocket requests. No forwarded Host/header is trusted to expand that list. Local Pi agents continue using their loopback API URL. With this setting absent, public browser origins remain rejected.
Serve persists across reboots; Canvas must still be running with the origin setting on each start. The script does not write an .env file, configure login startup, change access policies, or enable Funnel. It refuses conflicting existing Serve/Funnel/foreground/Service configuration rather than overwriting or resetting it; an exactly matching proxy is left unchanged. To inspect: tailscale serve status. To disable this port's proxy: tailscale serve --https=443 off (do not use reset, which clears other Serve configurations).
The script finds tailscale on PATH or the bundled macOS CLI at /Applications/Tailscale.app/Contents/MacOS/Tailscale, using TAILSCALE_BE_CLI=1. You can set TAILSCALE_BIN=/absolute/path/to/cli. Serve always targets Vite port 5173; --port/production options are not supported. Vite uses strict port selection, so a busy 5173 causes an error instead of silently using 5174. Only the configured HTTPS hostname is explicitly allowed (not allowedHosts: true); HMR automatically uses ws:// locally and wss:// through the HTTPS proxy. If you explicitly set a different backend PORT, both backend and Vite's API proxy use it, but Serve still targets 5173. HTTPS Serve remains on port 443. Check setup's --help for usage.
Use + Card to create a separate Markdown card on the Canvas; sticky notes are unchanged. Cards have no title. The front renders Markdown; Edit opens the source editor; valid content and tags auto-save about 500 ms after typing stops. Done returns to reading without discarding the draft. A small status shows Saving / Saved / Not saved, with Retry on write failures. Metadata ↷ flips to the immutable ID, editable comma-separated tags and Links (other card IDs, separated by commas or whitespace). Links auto-save with content/tags; metadata does not count toward the 400-unit content limit. Flip back with Content ↶. Drag the card header and resize selected cards. Markdown and Interactive Cards have a maximize/return button that fills the webpage without changing their saved placement or remounting their content. Canvas card content, metadata and editor text adapt to browser viewport width (CSS pixels): 11px at widths ≤1024px, smoothly increasing to 13px at ≥1440px. The screen-pixel minimum when zoomed out increases with the same width range from 10px to 11px. Markdown headings H1–H6 and code use relative sizes, so they scale with the body. Resizing the window updates text without changing card positions, dimensions or stored content. Sticky notes retain their 14-screen-pixel minimum. In Agent Canvas, × removes only its Canvas placement; Delete permanently… requires confirmation and removes the card from the library too.
Card box shows the selected view's cards plus one shared Pi terminal in a fixed right sidebar; Agent Canvas remains independent. Use the top-bar Agent toggle or the sidebar's hide arrow to collapse it and reclaim canvas space. Its visibility preference is remembered in this browser. Hiding the sidebar or switching workspaces keeps the same terminal/session alive; maximize/return and Stop/Resume remain available. The docked agent is not included in card fit/overview. Open Library for a responsive left-side card grid with search over content, tags and ID. Multiple terms match any word (OR), with case-insensitive Unicode-normalized substring matching. Results are paginated in batches of 24 rather than mounting the entire library. Click a preview for the complete read-only card; use its separate + Add / ✓ Remove button to toggle current-view membership. The panel stays open so you can add several cards. Card × removes only its current view placement; Delete permanently… confirms deletion from the library and all views. Cards not already on Agent Canvas retain the + header action to place them there.
The fixed Unfiled system view dynamically shows cards that have no membership in any other saved or temporary view. It cannot be renamed, deleted, saved or manually edited for membership. Removing a card from its last ordinary view moves it to Unfiled; adding it elsewhere removes it from Unfiled. Its remembered layout returns if the card becomes unfiled again. Use + New view for an empty temporary view, Rename to name it, and Save view to make it a saved dropdown entry. Names are unique (case-insensitive/Unicode-normalized). Leaving a temporary view offers Save and switch / Discard and switch / Cancel. Temporary views survive reload and normal backend restart when persistence is enabled; ordinary unsaved card text drafts still do not survive page reload. Saved views automatically save card membership, positions/sizes and pan/zoom. View deletion requires confirmation and never deletes cards or the shared agent. Every view edits the same library content/tags/Links, while card layouts are independent. On a Card box card's metadata face, click a tag to create an auto-fitted temporary view containing every exact tag match; click a linked card ID to create one containing the source, target and every recursively connected card. Existing temporary-view Save / Discard / Cancel protection still applies. Switching views retains in-page invalid card drafts, flushes pending valid autosaves, and keeps the same terminal DOM/WebSocket.
All browsers share the current Card box view. View selection and its saved viewport/layout changes synchronize via SSE; workspace selection stays local. Every newly created card, including from Agent Canvas or the normal API, joins the library and current view only, in a free slot without moving existing cards. When Unfiled is current, the card remains unassigned and therefore appears there. Agents can search the library and create/activate/name/save views or add/remove members using the documented API; natural-language interpretation happens in Pi, not a separate search service. Existing Card box placements migrate to a saved 原有 Card box view with their positions/sizes preserved. This upgrades Canvas JSON to schema 4; SQLite uses schema 5. Older Canvas versions cannot read the new save—use isolated data when checking out old code and retain backups.
Cards may also be agent-authored Interactive Cards with a required title and sandboxed HTML/CSS/JavaScript. They retain tags, reciprocal links, Library/views/Unfiled, front/back metadata and both workspace placements; the front title is read-only, while humans can edit title/tags/links from the metadata back after pressing Edit; source is API-only. Pure SVG/HTML is immediate, dynamic content uses Play/Restart, and hidden runtimes reset. JSXGraph 1.14.1 is bundled locally: agents can directly use JXG for responsive SVG geometry, function plots and sliders, without CDN access or new sandbox permissions. Validated image/audio/video files can be imported from ~/Documents/agent-canvas/media/{images,audio,video} into private card assets and referenced as card-asset://name; the sandbox has no external network or Canvas access. See docs/interactive-cards.md.
Use Start Card box agent once to create its New session, working in ~/Documents/agent-canvas/ (the card database directory when overridden). This agent is unique, has no delegation handles, and cannot be removed or replaced by another launch. Stop retains it; Resume reopens its conversation. Switching views does not stop or remount terminals. Canvas launches Pi in regular TUI mode so the embedded xterm owns scrollback instead of relying on Pi's fullscreen transcript viewport. Mode preference is remembered across reload; each view's viewport is retained while switching within the page. Restart restores previously running conversations using the existing exact-session/fallback rules, with no task replay. Card box agent instructions forbid directly editing SQLite/Canvas data files; this is guidance, not filesystem isolation. Links are automatically bidirectional: adding A → B adds B → A, and removing either end removes both, in one SQLite transaction. Links are shown as IDs and are clickable in Card box metadata to explore their complete connected component; tags remain flat rather than hierarchical. Up to 100 unique link IDs per card are allowed, including reverse links; a change that would exceed either card's limit is rejected without saving either side. New targets must exist, and self-links are rejected. Deleting a target retains its ID in referring cards as a historical reference; remove it manually if desired.
Both workspaces render LaTeX-style math locally with KaTeX. Use $E=mc^2$ for inline math, and put $$ on separate lines for display math:
$$
\frac{a}{b} + \sqrt{x}
$$A fenced math block also renders as display math; other code fences and inline code stay literal. Escape currency dollars as \$5. Edit shows the original source; Done renders it, without changing SQLite content or schema. Formula fonts scale with card text, and wide display formulas scroll horizontally without resizing the card. Malformed/unsupported formulas show source or red errors rather than breaking the card, and can still be saved as text. This is KaTeX's supported math syntax, not a full LaTeX document compiler. Trusted HTML, external links/images from TeX, and unbounded macro expansion are disabled; KaTeX fonts are served locally. The dependency override keeps the renderer/parser on the same patched KaTeX version.
Each card supports at most one image and one audio file, shared by both Canvas modes. Drag files onto a particular card to upload; dropping onto empty Canvas does nothing. There is no file-picker button, external URL registration, clipboard paste or recording in this version. A single drop may contain one file of each kind; duplicate kinds/unsupported files are rejected before uploading.
The front displays audio → image → Markdown text, including while editing text. Images display in full without cropping; click to open a larger preview (×, Escape or background click closes it). Audio offers play/pause, a seek bar and time; it never autoplays. One shared player continues across mode switches, and playing another card pauses the previous audio. Unlinking/replacing the currently playing file or deleting its card stops it.
Supported images: JPEG, PNG, WebP, GIF, maximum 10 MB each. Audio: MP3, M4A, WAV, OGG, maximum 50 MB each. Limits use 1024-based MB. Uploads validate actual file signatures, not browser MIME labels; browser codec support still determines playback. Back-face metadata lists original names and sizes, with Remove controls. Replacing an existing file requires confirmation; stale concurrent replacements return a conflict rather than overwriting a newer attachment. Attachment saves are independent of Markdown drafts and the 400-unit limit.
Files are copied (originals untouched) into images/ and audios/ alongside the card database, normally ~/Documents/agent-canvas/. UUID filenames avoid collisions; original names are stored in SQLite. Accepted files and their metadata are retained after unlinking, replacement or permanent card deletion, for a future manual audit. No audit/automatic cleanup is included now. Failed/uncommitted uploads are cleaned up. Two-file drops upload each file separately; a later failure does not undo an earlier successful upload.
Content is limited to 400 text units: CJK graphemes count individually and non-CJK words count using Unicode word segmentation. Mixed content is added together. Markdown syntax, link destinations, whitespace and punctuation do not count; ordinary code text counts. In math, source argument text, variables and numbers count with the same Unicode rules; dollar delimiters and backslash-letter command names (such as \frac or \text) do not. Text inside \text{...} still counts, so formulas do not bypass the 400-unit budget or 20,000-character source cap. Images embedded in Markdown and raw HTML are rejected; image/audio attachments use independent fields. UI and API share the same validator; over-limit drafts stay editable but are not saved, even after Done; the last valid stored content is preserved. Unsaved drafts exist only in the current page and are lost on reload. Existing cards above 400 units remain readable and deletable without truncation; further saves require shortening the content to the new limit. There is also a 20,000-character Markdown source limit. Tags: up to 30 unique strings, at most 60 characters each, no commas/newlines.
Cards live independently in SQLite at ~/Documents/agent-canvas/cards.sqlite (override with AGENT_CANVAS_CARDS_FILE). IDs use the server's local date and a daily non-reused sequence, e.g. 2026-10-03-0001. SQLite transactions protect ID allocation. SQLite automatically migrates older libraries to schema 5: schema 2 adds the Links column, and schema 3 fills missing reverse links between existing cards in one transaction, retaining references to deleted cards. Schema 4 adds attachment-file metadata and one image/audio reference per Markdown card. Schema 5 adds the interactive type, source revision and private-asset metadata without changing existing cards, content or links. If backfilling would exceed a card's 100-link limit, migration fails without partially adding reverse links. Older code supporting only earlier SQLite schemas cannot open the migrated database; use separate test data when checking out older commits. On startup an online SQLite backup is saved to cards.sqlite.bak; do not copy a running WAL database as a single file. For a manual full backup, stop the server and copy the Canvas JSON, SQLite file, and the complete images/, audios/ and interactive-assets/ directories (including unlinked ordinary attachments). These two stores are not a single atomic transaction: a card may survive a failed Canvas save, and missing-card placements are discarded on restore.
Canvas saves now use schema version 2 and read existing version-1 files. Older branches cannot load version-2 saves; when testing this branch separately, use a separate state file and card database. Reset clears Agent Canvas placements but preserves the card library, Card box layouts and its agent.
On the first default-path startup, if the new data directory is absent or empty, a stopped legacy ~/.agent-canvas/ is copied into ~/Documents/agent-canvas/ automatically. Original files remain as a recovery copy. Migration refuses to copy data owned by a live server, includes any remaining SQLite WAL/SHM files, and excludes process locks. Existing new-directory data is never overwritten or merged. Setting either storage-path environment override disables automatic migration. After migrating, the Documents directory is authoritative; the legacy copy is no longer updated.
Both workspace layouts are auto-saved to ~/Documents/agent-canvas/canvas.json (override with AGENT_CANVAS_STATE_FILE). Metadata changes are saved after 250 ms of inactivity with atomic replacement and a .bak previous-save backup. Normal shutdown flushes immediately. The file contains node metadata, notes, edges, layout, session identities and recent terminal output; treat it as private. Unsubmitted terminal input, active tools and Agent Canvas viewport are not saved. Card box view membership/layout/viewport and its active temporary view are saved with Canvas JSON. Pointer mode and the notification opt-in are remembered locally by the browser. Agent turn state is live only and is not saved.
On restart, previously running nodes automatically reopen their exact Pi conversation with no prompt sent. Missing or invalid sessions fall back to pi -r; a nonzero process exit before Pi reports a loaded session also falls back once. Missing workdirs leave a stopped node with an error. Previously stopped nodes stay stopped; use ▶ to reopen their conversation. User-installed Pi extensions still run normally on startup.
Reset Canvas in Agent Canvas asks for confirmation, stops only its Pi processes, and clears its nodes/notes/edges/placements. Card box agent and layouts are unaffected. It does not delete cards in the card box, Pi sessions or project files. Save failures appear in the browser. Corrupt/unsupported save files prevent server startup instead of being overwritten; inspect the original and .bak before manually recovering. A process lock prevents multiple servers from opening the same save file; stale locks from exited processes are recovered automatically.
- Architecture — processes, canvas state, PTY streaming, lifecycle and boundaries.
- Agent API Guide — copyable instructions for agents; also available in the browser via API Guide or
GET /api/docs. The web version fills in the correct explicitly allowed browser-facing URL. Canvas-launched Pi agents automatically receive the agent-canvas skill, which points them to the current full guide on the running server. - Development — installation, ports, tests and source map.
Use npm run dev for the current local/Tailscale workflow. Do not expose this unauthenticated service to the public internet or LAN; optional private tailnet access is described above.