PMX Canvas
PMX Canvas is a server-authoritative spatial workbench controlled through MCP, HTTP, or the CLI.
Humans curate agent context by pinning nodes; agents read that curation through
canvas://pinned-context. State survives browser refresh.
Runtime prerequisites
PMX Canvas 0.6.1 requires Bun >=1.4.2, including when installed through npm.
Check bun --version and ensure Bun is on the MCP host's PATH, not just your shell's;
use an absolute executable path if needed. Install/configure only when requested.
For pinned installation, MCP configuration, managed services, and disposable verification,
read Installing PMX Canvas.
Required Operating Sequence
- After a
pmx-canvas upgrade, refresh this skill first. Run
pmx-canvas skills sync --yes (--check to detect drift; exit 1 when stale) — it finds the skill
copies already installed in the workspace (whatever layout your agent uses) and replaces the
complete trees from the package, references/evals/fixtures included. Sync ONLY from the
installed package, never from a source checkout: this skill documents the runtime it ships
with, and a newer repo skill paired with an older installed runtime advertises commands and
themes the binary does not have (0.4.3 report skew finding). Also restart the HTTP daemon and
any --mcp processes; Bun does not hot-reload a running process.
- Open or focus the workbench before mutating. Reuse one visible canvas surface for the
session.
- Verify workspace identity at the actual target. Read
GET /health at the URL your
CLI/MCP/browser is using; its workspace must equal the intended canonical absolute root.
pmx-canvas serve status --port=<port> checks the local listener on that port, not an
arbitrary PMX_CANVAS_URL. A healthy listener may belong to another project.
- Read before write. Search with
canvas_query { action: "search", query } before creating
nodes. Read the full layout only when necessary. The MCP parameter is query — passing the
HTTP API's q is silently ignored and returns zero results.
- Snapshot before destructive changes. Use
canvas_snapshot { action: "save", name } before
clear, restore, or a major
reorganization.
- Show intent with the Ghost Cursor — by default. Signal with
canvas_intent { action: "signal", ... } before every meaningful create, move, connect, remove,
or edit, then pass the returned intent.id as intentId on the mutation so the ghost settles
into the result. Use it as much as possible to make your next move and your work visible: the
human watches intent form and can veto mid-thought. Skip it only for trivial in-place tweaks or
high-frequency batch churn. The default TTL (~8s) expires between agent turns: signal with
ttlMs: 30000 and settle by passing intentId on the mutation in the same or next call.
Since 0.4.5, an agent mutation WITHOUT a signal still shows a server-synthesized auto-ghost
(rendered lighter — dimmer, dotted, no veto — and settled instantly). That is a visibility
floor, not a replacement: only an explicit signal gives the human a real pre-mutation veto
window, your reasoning (reason), and staged multi-step previews. Batch and browser-human
actions never auto-ghost.
- Attach a session so the human can see you (0.4.8+). Start board work with
canvas_ax_state { action: "set-presence", attached: true, label: "<who you are>" } and end
it with { attached: false } — detaching hands the human a receipt. Everything the session
gives you and asks of you (cursor + phase chip, the session panel, steering, the scope fence's
403s, the human edit lock's 409s, unattended approvals) is in Sessions & the human below.
- Mutate through current composites. Prefer the 16 composite MCP tools below.
- Arrange and validate. After batch changes, use
canvas_view { action: "arrange" } when
appropriate and always finish with canvas_query { action: "validate" }.
7b. Show the human. After creating user-facing output, bring the camera to it: a single node
gets canvas_view { action: "focus", id } (pans by default), a small cluster gets
canvas_view { action: "fit", nodeIds: [...] } with exactly the new ids. See
In-View Placement & Sizing below — auto-placement is board-relative, not camera-relative.
- Verify context pins. Pin with
canvas_pin_nodes or the browser's Pin as context, then
read canvas://pinned-context.
- Clean up temporary nodes. Remove retry/test fixtures and restore the baseline snapshot when
the task requires leaving the board unchanged.
Workspace Safety
Before any create, update, remove, clear, restore, arrange, or smoke test, choose an
absolute project root and a dedicated free port. For a local target:
export PMX_CANVAS_WORKSPACE_ROOT=/absolute/path/to/project
export PMX_CANVAS_PORT=14313 # Example only: choose a free port for this workspace.
unset PMX_CANVAS_URL # Remove a stale URL before selecting a different local target.
curl --fail --silent --show-error "http://localhost:${PMX_CANVAS_PORT}/health"
pmx-canvas serve status --port="$PMX_CANVAS_PORT"
Compare the health field workspace (not workspaceRoot) to the intended root,
resolving symlinks before comparison. Stop on mismatch or failed health; do not write.
If deliberately using PMX_CANVAS_URL or --server-url, check /health at that exact
URL instead. Confirm the browser and MCP use the same verified target, especially
after fallback to another port. Do not print URLs containing credentials.
pmx-canvas smoke reports health/workspace/version information, checks MCP initialize,
creates/searches/removes a temporary node, and validates the board. It mutates state
and does not enforce your intended workspace identity; version skew is reported, not
necessarily a failing check. Use a disposable workspace, verify health first, and inspect
both the JSON details and exit status (1 when a check fails). It is not a read-only probe
or a guarantee of unchanged history.
- If
responsive: true but pidRunning: false, treat the listener as potentially stale.
- On mismatch, leave the other workspace's server alone. Start the intended workspace on
a different free port, target it explicitly, and re-check
/health before writing.
- On an ordinary local machine,
pmx-canvas serve --daemon --no-open --port=<free-port>
refuses a different-workspace occupant. In a managed environment, supervise foreground
pmx-canvas serve --no-open --port=<free-port> with the host's service manager instead.
PMX_CANVAS_PORT selects the CLI/MCP port. CLI target flags override environment values
(--server-url wins over --port); otherwise PMX_CANVAS_URL wins over PMX_CANVAS_PORT.
For server startup, explicit --port wins, then PMX_WEB_CANVAS_PORT, then
PMX_CANVAS_PORT; in Amp orbs PORT is a later fallback. URL targeting does not select
the server's bind port.
- MCP transport workspace resolution. Set
PMX_CANVAS_WORKSPACE_ROOT plus a dedicated
PMX_CANVAS_PORT in the host configuration. Without an explicit root, MCP may inherit a
different workspace from the preferred-port daemon; PMX_CANVAS_ALLOW_WORKSPACE_SPLIT=1
opts out of that heuristic. Current source honors an explicit root, but may bind a fallback
port if the preferred port is occupied. Root alone therefore does not guarantee CLI, MCP,
and browser target alignment; older installed runtimes can also differ. Inspect startup
output and the actual health endpoint, never assume the preferred port was bound.
Clear stale inherited PMX_CANVAS_URL values in the host configuration as well as the shell.
- CLI query/mutation commands are HTTP clients and do not start a server.
serve or --mcp
starts or attaches to one. Use the host's authenticated preview mechanism for remote viewing;
a loopback URL is only reachable inside the machine/orb running the service.
Choose the Smallest Useful Node Type
| Need |
Node/tool |
| Narrative, note, explanation |
markdown via canvas_node |
| Progress or current state |
status via canvas_node |
| Persistent context cards |
context via canvas_node |
| Event/check stream |
ledger or trace via canvas_node |
| Local source with live updates |
file via canvas_node |
Tabular data (.csv/.tsv), a PDF, or any other file on disk (0.4.6+) |
file via canvas_node — CSV/TSV render as tables, PDFs render inline, other binaries show a size placeholder. Never paste a CSV into a markdown fence. |
| Code review / unified diff (0.4.6+) |
diff via canvas_node (content = diff text; link to its file node with a references edge) |
| Flowchart / sequence / state diagram (0.4.6+) |
mermaid via canvas_node (content = mermaid source; renders client-side, no hosted app) |
| Image |
image via canvas_node |
| Cached URL content |
webpage via canvas_node |
| Structured UI |
json-render via canvas_render |
| Chart |
graph via canvas_render |
| Live work-item board (0.4.6+) |
canvas_render { action: "workboard" } — one board node, auto-refreshes on work-item changes |
| Generated communication surface |
HTML primitive via canvas_node |
| Give the agent tasks + watch progress + loop (0.4.7+) |
ax-board HTML primitive — a live AX control surface, created AX-enabled |
| The same, drawn as a task flow, and materializable to real nodes+edges (0.4.7+) |
ax-flow HTML primitive — Materialize to board lays the steps out as nodes joined by flow edges with a loop-back edge, each linked to a work item |
| Drive a flow WITHOUT the panel (0.4.7+) |
Materialized step nodes carry native Start/Done/Blocked controls, and the anchor adds Run loop/Stop + steer. The native loop runs server-side, so it survives a browser reload and keeps advancing while the tab is closed. |
| Self-contained HTML/JS |
html via canvas_node |
| Hosted interactive MCP app |
canvas_app { action: "open-mcp-app" } |
| Excalidraw diagram (interactive/human drawing) |
canvas_app { action: "diagram" } — prefer mermaid for agent-authored diagrams |
| Bundled React artifact |
canvas_app { action: "build-artifact" } |
Use the lightest tier that communicates the result. Do not build a web artifact when markdown,
json-render, a graph, or an HTML primitive is sufficient.
Current MCP Composites
The live MCP surface is 22 tools: the 16 composites below plus 6 standalones. This table is
kept in sync with the server's composite registry; tools/list on a fresh pmx-canvas --mcp is
always authoritative, independent of the installed release version.
| Composite |
Actions |
canvas_node |
add, get, update, remove |
canvas_render |
describe-schema, validate, add-json-render, stream-json-render, add-graph |
canvas_edge |
add, update, remove |
canvas_group |
create, add, ungroup |
canvas_history |
undo, redo |
canvas_view |
arrange, focus, fit, clear, remove-annotation |
canvas_query |
search, layout, validate |
canvas_webview |
status, start, stop, resize, evaluate |
canvas_app |
open-mcp-app, diagram, build-artifact |
canvas_ax_state |
get, set-focus, set-policy, report-capability, presence, set-presence |
canvas_ax_work |
add, update, annotate |
canvas_ax_gate |
request, resolve, await with approval, elicitation, or mode |
canvas_ax_timeline |
read, record-event, add-evidence, send-steering |
canvas_ax_delivery |
claim, mark |
canvas_snapshot |
save, list, restore, delete, gc, diff |
canvas_intent |
signal, update, clear |
Important routing:
- Basic nodes:
canvas_node { action: "add", type, ... }
- HTML:
canvas_node { action: "add", type: "html", html }
- HTML primitive:
canvas_node { action: "add", type: "html", primitive, data }
- Graph:
canvas_render { action: "add-graph", ... }
- JSON render:
canvas_render { action: "add-json-render", ... }
- MCP app:
canvas_app { action: "open-mcp-app", ... }
- Excalidraw:
canvas_app { action: "diagram", ... }
- Web artifact:
canvas_app { action: "build-artifact", ... }
As of v0.3.0, the 57 legacy single-purpose tools from the v0.2 compatibility window are removed.
The composites above plus the retained standalones are now the whole MCP surface: canvas_batch,
canvas_pin_nodes, canvas_screenshot, canvas_ax_interaction, canvas_ingest_activity, and
canvas_invoke_command. Snapshots are the canvas_snapshot composite (actions
save | list | restore | delete | gc | diff); the 6 legacy snapshot standalones were removed in
v0.4.0 after their deprecated 0.3.x window.
Spatial Rules
- Treat proximity as relatedness and top-left to bottom-right as reading order.
- Search before adding to avoid duplicate nodes.
- Extend the current board in place; do not evict prior nodes to add new material.
- Use groups only when the frame communicates meaningful containment.
- Keep related nodes 40–80 px apart and separate unrelated clusters by roughly 150–250 px.
- Use directed edges for actual relationships, not decoration.
- Edge types:
flow, depends-on, relation, references.
- After manual or batch layout changes, run
canvas_query { action: "validate" }.
In-View Placement & Sizing (required for user-facing nodes)
Auto-placement (omitting x/y) is board-relative, not camera-relative: it places right of
the last node or scans rows from the origin, ignoring where the human is looking. On a board with
distant nodes, an auto-placed node lands off-camera.
- Omit
x/y only on an empty or locally dense board. Otherwise place near the human's
attention: the pinned/focused neighborhood, or explicit coordinates beside the last
user-facing output (gap ≥ 24–48 px).
- After creating nodes the human should see, pan the camera (operating-sequence step 7b):
focus for one node, fit with exactly the new nodeIds for a cluster. Use noPan
(focus --no-pan) only when you must not steal the camera.
- Never fit the whole board to "show" new work — on a board with outliers that miniaturizes
everything. Always pass explicit
nodeIds to fit.
- Never leave user-facing output at far coordinates without a focus/fit.
- Do not hand-compute a whole board's coordinates. Create the nodes (omitting x/y), then
canvas_view { action: "arrange", layout: "grid" | "column" | "flow" } and finish with a
fit over the new ids. Manual pixel math is what produces long-line, unbalanced boards.
fit sizes itself to the connected browser window (0.4.6+) — you do not need to guess
width/height. Pass them only to fit for a window other than the human's.
- Every node is a canvas card. Nodes render on the canvas where you put them — there is
no docked/HUD placement and no
dockPosition (removed in the rail-chrome redesign); status
and context nodes are ordinary cards like the rest.
Size for content. Omitting width/height gives readable per-type defaults — prefer them:
markdown 640×420, status 360×200, file 520×360, diff 640×420, mermaid 640×460, html 720×640,
graph 760×520, mcp-app 960×600, web-artifact 960×720. A hosted app opened with
canvas_app { action: "open-mcp-app" | "diagram" } — including the Excalidraw diagram preset — is
the exception: request width: 720, height: 500 for its standard landscape preset instead of
relying on the 960×600 default for a directly created mcp-app node. After the app mounts, read
the node back with canvas_query { action: "layout" } and use its persisted width and height for
subsequent placement or layout; the mounted app may have changed the requested dimensions.
Since 0.4.6 the server clamps explicit creation sizes UP to per-type
readability floors (e.g. markdown 360×180, graph/json-render/html 420×280, mcp-app 480×320) —
a tiny probe size silently becomes the floor. strictSize: true is the only opt-out (a fixed
scrolling frame you genuinely want small). canvas_query { action: "validate" } additionally
reports any node below its floor as an advisory sizeWarnings entry — treat a non-empty list
as layout work left to do.
Token hygiene. For routine state checks use canvas_ax_state { action: "get" } WITHOUT
includeContext — the full AX context payload is ~10× larger; request it only when you are
actually consuming context.
Context Pins
Context pins are the primary human-to-agent bridge:
- Human pins nodes in the browser using Pin as context.
- Agent reads
canvas://pinned-context.
- The resource includes pinned nodes and nearby unpinned neighbors.
Do not confuse context pinning with Lock position, which only excludes a node from auto-arrange.
Every node type, including status, can be removed through canvas_node { action: "remove" }, the
title-bar × control, or the Close context-menu action.
Sessions & the human
The board has three modes, all gated on one fact — whether a session is attached:
- Quiet board (nobody attached, nobody writing): a plain canvas. Your writes still show as
ghosts, nothing else changes.
- External steering (you write with no session attached): the top bar shows a passive
writers indicator with an activity feed listing each write under your label, plus a
connected-writers sheet. Identify yourself (
PMX_CANVAS_AGENT_SOURCE, or x-pmx-source on
HTTP) so the feed names you, not the transport. Pending explicit intents carry an inline Veto
there.
- Focus session (attached): your cursor sits on the node you last touched with a phase chip
(
idle / thinking / tooling / waiting-approval); every MCP/HTTP write is attributed to
your session automatically (pass agentId only to keep a sub-agent separate); the human gets
the session panel (work items, approval gates, timeline of your tool runs, board writes,
evidence and steering), a command bar that posts steering you read on your next turn (with a target
picker when several agents are connected), a timeline filterable by kind (Updates / Steer /
Events / Evidence), and a context meter. That meter is the pinned-context payload estimated against a configured budget
("Pins") unless your host reports your real token usage on the presence
(set-presence { contextUsage: { used, total } }) — then it shows your actual window
("Context"). The Copilot extension reports it automatically; other hosts report it themselves. Report phase: "thinking" before a long reasoning stretch if your host
gives you a hook; tooling is derived from your writes. Hosts with adapters (the Copilot
extension) attach for you; the human can also start a session from the board's Start agent
session button — your writes (transport or PMX_CANVAS_AGENT_SOURCE label alike) are
attributed to it and it takes your name.
- No adapter? Use the pump. Any CLI agent becomes steer-reactive with one command in a
terminal:
pmx-canvas pump --consumer <your-key> --exec '<command>'. It long-polls your
delivery queue, runs the command once per steer (message on stdin plus the
PMX_STEER_MESSAGE/ID/SOURCE/TARGET/CREATED_AT env envelope), and marks
per-consumer only after the command exits 0 — a failed hand-off stays pending and the pump
exits non-zero. {message} in the template expands to a quoted env reference (never spliced;
refused on Windows — read stdin there). --parent <key> rolls you up under an orchestrator's
chip; --once for scripts. See pmx-canvas pump --help.
What the session asks of you:
- Detach explicitly (
attached: false or a session-end activity). Attaching over a
non-empty board saved a Before session · … snapshot; detaching emits the receipt (items
done / vetoed, a diff against that snapshot, one-click restore) — an idle timeout delays it.
- 403 = outside the scope fence. The human may fence you to a region (
policy.scope): writes
outside it are refused with a reason naming the node or position (a fenced group frame
grants its members too). Read policy.scope in canvas://ax-context, ask the human to widen
it, never retry blindly. The fence is the human's: set-policy does not take scope.
- 409 = a human is holding that node (dragging or editing it right now). Requeue the change
and retry in a moment. If you had signalled an intent on it, it was vetoed — a
yield timeline
event says who took over.
- A gate you do not answer auto-holds (
held, default TTL 5 min). held is a non-approval:
do not proceed; the human can reopen it from the panel. A rejection reaches you as steering.
- Your latest edit can be undone from the panel. One shared undo stack: when the human undoes
it you get steering ("Undid your edit: …"); treat the board as the truth, not your last write.
- Steering arrives as
steering-message rows in canvas://ax-timeline (or claim them via
canvas_ax_delivery). The command bar, gate rejections, undo and take-overs all speak through
it. Steering may be ADDRESSED: with several agents connected the human picks a recipient in
the composer, and a claim only returns broadcasts plus messages targeted at your consumer
label — so always claim as yourself (your PMX_CANVAS_AGENT_SOURCE label), or addressed
steering never reaches you.
- Steering is also the agent-to-agent mailbox. To coordinate with another agent on the
board, send
canvas_ax_timeline { action: "send-steering", message, target: "<their consumer label>" } — they claim it once on their next turn, you never receive your own, and
the human sees the exchange in the panel as "you → them · message". Read
canvas_ax_state { action: "presence" } for who is connected. Park shared state in work
items and evidence; split territory with group frames rather than editing the same node.
Browser Workflows
Use the visible workbench when the human is actively curating layout:
- Drag nodes to move them.
- Drag empty space to lasso-select (Select tool, the default); hold Space or pick the rail's Pan
tool to pan instead — the Pan tool pans even when the drag starts on a node.
- The selection bar (floating bottom-center) offers count, align left/top, distribute,
auto-arrange, Group (G), Connect, Pin as context, delete and clear; selected nodes show an
accent outline with corner handles.
- Double-left-click a node for context pinning, position locking, focus, collapse, connect, refresh,
open, close, and type-specific actions.
- Double-left-click empty canvas for the node-creation menu; hold and drag the right mouse button to pan.
- Drop files or URLs to create matching nodes; an empty board shows starter actions (new note,
pick files, paste a link, start an agent session).
- Use a markdown node's Edit button to edit its content.
- Groups are frames with the name pill and an action cluster (auto-arrange children, collapse,
⋯ rename / ungroup / pin all) on the top edge. Membership changes only on release while the
"release to add to " pill shows (Esc keeps it out); dragging a child fully out offers
"release to remove". A collapsed group is a chip that hides its children (edges to them draw
to the chip). G groups the selection, Shift+G ungroups. Ungroup dissolves the frame (children
stay, nested children move up one level, one undo step) — and your
canvas_group { action: "ungroup" } is the same operation with the same result.
- Edges: drag from a node port, or pick the rail's Connect tool (C) and drag from anywhere on
a node; the target lights up, Esc cancels, L asks for a label on release.
- The History drawer (rail camera button, or the receipt's Full log) lists snapshots and
agent sessions in one timeline; save a snapshot there before experiments and restore only after
confirmation. Ctrl/Cmd+Z and Shift+Z work the shared undo stack.
- The minimap (bottom-right of the canvas) is a true-scale map; hover magnifies it, click
jumps, drag pans. A banner under the top bar reports a dropped stream (reconnecting) or a
post-reconnect resync; edits still save over HTTP meanwhile.
- Other open tabs appear as green cursors with a name tag (
/workbench?name=mia sets yours);
a node you drag is locked for agents until you release it.
- The chrome is a persistent 52px left tool rail plus a slim 44px top bar (0.4.8+). The rail
carries the tools (Select V, Pan Space, Connect C), node creation (markdown M, image I, file
Shift+F, webpage W, HTML surface H, group G, annotate A — a popover with draw / text / eraser),
and utilities: search & commands (Cmd+K — actions with shortcuts, then jump-to-node), arrange,
trace, minimap, history, the theme picker (nine themes: dark, light, high-contrast, midnight,
sepia, arctic, ember, forest, volt), and shortcuts (?). The top bar holds the connection dot,
workspace title, the session chip / gate badge / context meter while a session is attached
(or the external-writers indicator and Start agent session otherwise), and the zoom cluster
(zoom out, % label = reset, zoom in, fit F). Hovering a rail button shows a tooltip with its
shortcut.
On viewports ≤1180px the top bar drops its meta text and the session panel becomes a drawer;
every control stays in the rail at any width — there is no separate mobile menu.
- Keyboard: nodes are focusable — arrow keys move to the nearest node, Enter opens it in focus
mode (a scrim + inset view with Open in tab), Esc closes the top-most overlay.
- Embedding hosts can open
/workbench?theme=<name> (or ?theme=auto to follow the host's
light/dark appearance) for a session-local default theme that never changes the server-global
theme other clients see; an explicit pick from the theme menu ends the override. The bundled
Copilot extension opens its panel with ?theme=light.
After changing files under src/client/, rebuild with bun run build before manual browser
verification.
AX Interactions
Node interactions request PMX AX primitives; they never execute arbitrary shell, tools, MCP calls,
or host actions.
DEFAULT_NODE_AX_CAPABILITIES is the per-node-type ceiling.
data.axCapabilities may enable or narrow capabilities but cannot escalate beyond the ceiling.
- Sandboxed surfaces are scoped to their own source node.
- HTML nodes must explicitly opt in.
- Use
window.PMX_AX.emit(type, payload) and await its result.
- Listen for
pmx-ax-update when an HTML control surface reflects live AX state.
- Steering is queued; claim with
canvas_ax_delivery, act, then mark delivered.
Read AX HTML control surfaces before building an
interactive AX-enabled HTML node.
Ready-made control surfaces (0.4.7+) — prefer these over hand-rolling
Two HTML primitives ship AX-enabled, so you do not need to author a control surface or pass
axCapabilities yourself. They are the ONLY primitives created with AX on; every other kind is
still a static document.
ax-board — task list: create tasks, watch status change live, steer, bounded loop.
ax-flow — the same controls drawn as a task flow with a loop-back rail, plus
Materialize to board.
ax.flow.materialize is the one interaction that creates canvas nodes, and it is deliberately
narrow: you supply TEXT ONLY (<= 12 steps, title <= 120 chars) and the server owns the result —
one markdown node per step, flow edges between them, a dashed references loop edge, and one
work item per step linked to its node. Re-materializing REPLACES the previous flow (the source
node keeps a manifest of what it created), so it is safe to call repeatedly.
The flow also works without the panel. Materialized step nodes carry data.axStep, and the
canvas renders native Start / Done / Blocked controls on them, with Run loop / Stop and a steer box
on the anchor (first) step. Those controls also follow the node into focus mode. The native loop
runs server-side: it advances when a step's work item completes, survives a browser reload, and
keeps going while the tab is closed — unlike the panel's loop, which dies with the iframe. Bounds
are the same either way: advances only while running, hard cap 20 runs, Stop persists immediately,
and blocked/cancelled halts it.
When you want the agent to work through a checklist the human can watch and steer, reach for
ax-flow + materialize rather than a markdown to-do list — the status chips and the loop come free.
Working a flow: keep the board honest as you go
A materialized flow is only useful if it tracks reality. While you work one:
- Move the step to
in-progress BEFORE you start it, and to done when it is actually done —
not in a batch at the end. The human is watching the chips to know where you are; a board that
only updates on completion tells them nothing while it matters.
- Pin the in-progress step as context (
canvas_pin_nodes with just that node, or
POST /api/canvas/context-pins mode set). This makes the active step the one thing in
canvas://pinned-context, so the board's "what am I working on" and your own context are the
same answer instead of drifting apart. Re-pin as you advance.
- Mark
blocked rather than going quiet when you are stuck — a stalled in-progress chip is
indistinguishable from a crashed agent.
- Read steering back with
canvas_ax_delivery as you go; a human watching a live flow steers
mid-run, and a steer you never claim is a correction you ignored.
Resources
Read the smallest resource that answers the question:
canvas://pinned-context — curated context plus neighborhoods
canvas://summary — compact board overview
canvas://layout — complete state
canvas://spatial-context — clusters and reading order
canvas://history — mutation history
canvas://code-graph — detected file dependencies
canvas://ax-context — compact AX context
canvas://ax-work — work items and gates
canvas://ax-timeline — events, evidence, steering
canvas://ax-pending-steering — adapterless delivery queue
canvas://skills and canvas://skills/<name> — bundled skills
Prefer canvas_query { action: "search" } over parsing the full layout.
Known Limitations
- Hosted MCP-app/ext-app nodes such as Excalidraw require the in-canvas host bridge and are not
standalone Open as site targets. URL-backed viewers and bundled web artifacts remain
openable.
- A standalone html surface (
/api/canvas/surface/:id opened as a site) is a VISUAL view: it
renders the same content and theme, but window.PMX_AX is not injected without the canvas
iframe's per-mount nonce, so AX buttons only work inside the in-canvas node (0.4.4 Codex note).
Do not tell a user a standalone tab's controls will steer the agent.
- A hosted ext-app (Excalidraw) node in a WebKit host panel (e.g. the GitHub Copilot app's
embedded WKWebView) historically could render as a black tile — a host compositor paint race
on the nested iframe, not a broken node (the session is healthy,
sessionStatus is
ready, and it renders fine in Chrome/Codex). Since 0.4.6 the canvas runs a paint oracle +
recovery ladder under WebKit: present-at-load ext-apps mount strictly one at a time (the
cold burst was the trigger), each frame answers a double-rAF paint probe after settle, and
on silence the ladder escalates — soft-expand cycle (the automatic analogue of the proven
enlarge+close) → serialized remount → an explicit "App surface failed to paint / Retry"
affordance. The connecting overlay stays up until paint is confirmed, so a black layer is
never presented as ready. The recovery trail
(GET /api/canvas/debug/ext-app-recovery / window.__PMX_EXTAPP_LOG) now records
mount-slot, paint-ok, paint-fail, soft-expand-cycle, recovery-exhausted, and
assume-visible-rearm — when diagnosing, trust paint-ok/paint-fail, and never assert
health from settled alone. Exception: in a host that reports the document hidden (the
GitHub Copilot panel does so continuously), paint-ok is recorded as
paint-ok (unverified: host hidden) — the app answered the paint probe, but nothing is
proven composited, so do not treat it as a verified paint. If a
tile still shows the Retry affordance, click Retry (fresh recovery budget) or expand-then-close;
attach the recovery trail when reporting.
- Ext-app frame documents live in server memory. Through 0.4.0, killing/restarting the daemon
while a panel stays open leaves ext-app tiles on dead frame URLs (
Frame document not found,
0.4.0 report Finding S) until a full workbench reload. Since 0.4.1 the browser revalidates its
frame documents on every reconnect and re-mints them against the new process automatically —
if a post-restart tile still looks blank on an older install, reload the workbench page.
- A hosted ext-app (Excalidraw) resized NARROW/TALL (e.g. 360x529) can show its diagram in the
upper region with the app's own dark fill below it — in every engine (0.3.4 report Finding Q).
This letterboxing lives inside the hosted app bundle's root container, not in PMX (a body-level
background override ships but cannot reach the app's inner root). Keep ext-app tiles landscape —
at or above the 720x500 they open at, or the same ratio — or expand the node: the fullscreen
overlay renders full-bleed. The durable fix is upstream in the excalidraw-mcp app.
- Behind proxies that buffer streaming responses (e.g. portal hosts), the workbench auto-falls
back from SSE to a polling transport within ~3s, so the board still boots and stays live.
Force a mode with
/workbench?transport=poll (or transport=sse) when diagnosing.
- Nested-iframe embeds (e.g. the Amp orb portal renders the canvas page inside an ampcode.com
iframe) can block child iframes from loading ANY
src URL, breaking every iframe-backed node
with a gray placeholder. The canvas probes this at boot and auto-falls back to fetching
same-origin surfaces and rendering them inline via srcdoc (HTML, graph, json-render, frame
documents). In Amp orbs specifically, the server sees AMP_ORB in its environment and adapts
end to end: it binds the portal-assigned $PORT automatically (no port flag in the service
command), stamps the page so the embedded client skips the (there-unreliable) probe, goes
straight to srcdoc, AND defaults to the polling transport (the portal proxy buffers SSE —
waiting out the watchdog could trip the boot modal); HTML surfaces also inline their theme
stylesheet so they render styled. External app
URLs cannot be inlined (cross-origin) and may stay blocked in such hosts. Force a mode with
/workbench?iframe-mode=srcdoc (or iframe-mode=src) when diagnosing.
- Graph and json-render standalone surfaces use
display=site and fill the browser viewport, and
reflow on a live window resize. Some single-tab host browsers historically didn't deliver
live-resize events; current Codex builds do — the 0.4.6 pass watched a standalone graph reflow
live (SVG 1550×783 → 850×483 on a 1600×900 → 900×600 window resize) with no reload. Do not
present a system browser as a workaround for stale resizing; it is only a preference for viewing
a full page beside the canvas.
- Some hosts cannot automate inside sandboxed workbench iframes. Verify those interactions in a
system browser or through server-side AX state.
pmx-canvas screenshot requires an active WebView. Start it with
canvas_webview { action: "start" }.
- The default server port is 4313, but it may fall back or be explicitly changed.
Persistence
State lives under .pmx-canvas/, primarily in canvas.db. It includes viewport, nodes, edges,
annotations, pins, snapshots, AX canvas state, and large-node blobs.
- Stop the server or close/flush the SDK before committing
canvas.db.
- History is session-scoped and is not persisted.
- Timeline AX data persists independently from canvas snapshots.
canvas_view { action: "clear" } clears canvas-bound state but not host/session diagnostics.
Detailed References
Load only the reference relevant to the task:
- Full MCP, HTTP, CLI, layout, and workflow reference
- Installing PMX Canvas
- HTML primitives
- Excalidraw diagram authoring
- AX HTML control surfaces
- GitHub Copilot adapter
- Codex app adapter
The authoritative current MCP inventory and legacy replacement table is
docs/mcp.md.
1---2name: pmx-canvas3description: Spatial canvas workbench for visual thinking — nodes, edges, groups on an infinite 2D canvas with pan/zoom, minimap, and real-time sync. Use this skill whenever you need to lay out information spatially: investigation boards, architecture diagrams, dependency maps, task plans, status dashboards, file relationship views, or any scenario where a flat list or text wall isn't enough. Also use when the user mentions "canvas", "board", "diagram", "spatial layout", "visual map", "node graph", or wants to see how things connect. The canvas is your extended working memory — pin nodes to curate context, read spatial arrangement to understand intent.4---56# PMX Canvas78PMX Canvas is a server-authoritative spatial workbench controlled through MCP, HTTP, or the CLI.9Humans curate agent context by pinning nodes; agents read that curation through10`canvas://pinned-context`. State survives browser refresh.1112## Runtime prerequisites1314PMX Canvas 0.6.1 requires **Bun >=1.4.2**, including when installed through npm.15Check `bun --version` and ensure Bun is on the MCP host's PATH, not just your shell's;16use an absolute executable path if needed. Install/configure only when requested.17For pinned installation, MCP configuration, managed services, and disposable verification,18read [Installing PMX Canvas](references/installing-pmx-canvas.md).1920## Required Operating Sequence21220. **After a `pmx-canvas` upgrade, refresh this skill first.** Run23 `pmx-canvas skills sync --yes` (`--check` to detect drift; exit 1 when stale) — it finds the skill24 copies already installed in the workspace (whatever layout your agent uses) and replaces the25 complete trees from the package, references/evals/fixtures included. Sync ONLY from the26 installed package, never from a source checkout: this skill documents the runtime it ships27 with, and a newer repo skill paired with an older installed runtime advertises commands and28 themes the binary does not have (0.4.3 report skew finding). Also restart the HTTP daemon and29 any `--mcp` processes; Bun does not hot-reload a running process.301. **Open or focus the workbench before mutating.** Reuse one visible canvas surface for the31 session.322. **Verify workspace identity at the actual target.** Read `GET /health` at the URL your33 CLI/MCP/browser is using; its `workspace` must equal the intended canonical absolute root.34 `pmx-canvas serve status --port=<port>` checks the local listener on that port, not an35 arbitrary `PMX_CANVAS_URL`. A healthy listener may belong to another project.363. **Read before write.** Search with `canvas_query { action: "search", query }` before creating37 nodes. Read the full layout only when necessary. The MCP parameter is `query` — passing the38 HTTP API's `q` is silently ignored and returns zero results.394. **Snapshot before destructive changes.** Use `canvas_snapshot { action: "save", name }` before40 clear, restore, or a major41 reorganization.425. **Show intent with the Ghost Cursor — by default.** Signal with43 `canvas_intent { action: "signal", ... }` before every meaningful create, move, connect, remove,44 or edit, then pass the returned `intent.id` as `intentId` on the mutation so the ghost settles45 into the result. Use it as much as possible to make your next move and your work visible: the46 human watches intent form and can veto mid-thought. Skip it only for trivial in-place tweaks or47 high-frequency batch churn. The default TTL (~8s) expires between agent turns: signal with48 `ttlMs: 30000` and settle by passing `intentId` on the mutation in the same or next call.49 Since 0.4.5, an agent mutation WITHOUT a signal still shows a server-synthesized **auto-ghost**50 (rendered lighter — dimmer, dotted, no veto — and settled instantly). That is a visibility51 floor, not a replacement: only an explicit signal gives the human a real pre-mutation veto52 window, your reasoning (`reason`), and staged multi-step previews. Batch and browser-human53 actions never auto-ghost.546. **Attach a session so the human can see you (0.4.8+).** Start board work with55 `canvas_ax_state { action: "set-presence", attached: true, label: "<who you are>" }` and end56 it with `{ attached: false }` — detaching hands the human a receipt. Everything the session57 gives you and asks of you (cursor + phase chip, the session panel, steering, the scope fence's58 403s, the human edit lock's 409s, unattended approvals) is in **Sessions & the human** below.597. **Mutate through current composites.** Prefer the 16 composite MCP tools below.607. **Arrange and validate.** After batch changes, use `canvas_view { action: "arrange" }` when61 appropriate and always finish with `canvas_query { action: "validate" }`.627b. **Show the human.** After creating user-facing output, bring the camera to it: a single node63 gets `canvas_view { action: "focus", id }` (pans by default), a small cluster gets64 `canvas_view { action: "fit", nodeIds: [...] }` with exactly the new ids. See65 **In-View Placement & Sizing** below — auto-placement is board-relative, not camera-relative.668. **Verify context pins.** Pin with `canvas_pin_nodes` or the browser's **Pin as context**, then67 read `canvas://pinned-context`.689. **Clean up temporary nodes.** Remove retry/test fixtures and restore the baseline snapshot when69 the task requires leaving the board unchanged.7071## Workspace Safety7273Before any create, update, remove, clear, restore, arrange, or smoke test, choose an74absolute project root and a dedicated free port. For a local target:7576```bash77export PMX_CANVAS_WORKSPACE_ROOT=/absolute/path/to/project78export PMX_CANVAS_PORT=14313 # Example only: choose a free port for this workspace.79unset PMX_CANVAS_URL # Remove a stale URL before selecting a different local target.80curl --fail --silent --show-error "http://localhost:${PMX_CANVAS_PORT}/health"81pmx-canvas serve status --port="$PMX_CANVAS_PORT"82```8384Compare the health field **`workspace`** (not `workspaceRoot`) to the intended root,85resolving symlinks before comparison. Stop on mismatch or failed health; do not write.86If deliberately using `PMX_CANVAS_URL` or `--server-url`, check `/health` at that exact87URL instead. Confirm the browser and MCP use the same verified target, especially88after fallback to another port. Do not print URLs containing credentials.8990`pmx-canvas smoke` reports health/workspace/version information, checks MCP initialize,91creates/searches/removes a temporary node, and validates the board. **It mutates state**92and does not enforce your intended workspace identity; version skew is reported, not93necessarily a failing check. Use a disposable workspace, verify health first, and inspect94both the JSON details and exit status (1 when a check fails). It is not a read-only probe95or a guarantee of unchanged history.9697- If `responsive: true` but `pidRunning: false`, treat the listener as potentially stale.98- On mismatch, leave the other workspace's server alone. Start the intended workspace on99 a different free port, target it explicitly, and re-check `/health` before writing.100- On an ordinary local machine, `pmx-canvas serve --daemon --no-open --port=<free-port>`101 refuses a different-workspace occupant. In a managed environment, supervise foreground102 `pmx-canvas serve --no-open --port=<free-port>` with the host's service manager instead.103- `PMX_CANVAS_PORT` selects the CLI/MCP port. CLI target flags override environment values104 (`--server-url` wins over `--port`); otherwise `PMX_CANVAS_URL` wins over `PMX_CANVAS_PORT`.105 For server startup, explicit `--port` wins, then `PMX_WEB_CANVAS_PORT`, then106 `PMX_CANVAS_PORT`; in Amp orbs `PORT` is a later fallback. URL targeting does not select107 the server's bind port.108- **MCP transport workspace resolution.** Set **`PMX_CANVAS_WORKSPACE_ROOT` plus a dedicated109 `PMX_CANVAS_PORT`** in the host configuration. Without an explicit root, MCP may inherit a110 different workspace from the preferred-port daemon; `PMX_CANVAS_ALLOW_WORKSPACE_SPLIT=1`111 opts out of that heuristic. Current source honors an explicit root, but may bind a fallback112 port if the preferred port is occupied. Root alone therefore does not guarantee CLI, MCP,113 and browser target alignment; older installed runtimes can also differ. Inspect startup114 output and the actual health endpoint, never assume the preferred port was bound.115 Clear stale inherited `PMX_CANVAS_URL` values in the host configuration as well as the shell.116- CLI query/mutation commands are HTTP clients and do not start a server. `serve` or `--mcp`117 starts or attaches to one. Use the host's authenticated preview mechanism for remote viewing;118 a loopback URL is only reachable inside the machine/orb running the service.119120## Choose the Smallest Useful Node Type121122| Need | Node/tool |123|------|-----------|124| Narrative, note, explanation | `markdown` via `canvas_node` |125| Progress or current state | `status` via `canvas_node` |126| Persistent context cards | `context` via `canvas_node` |127| Event/check stream | `ledger` or `trace` via `canvas_node` |128| Local source with live updates | `file` via `canvas_node` |129| Tabular data (`.csv`/`.tsv`), a PDF, or any other file on disk (0.4.6+) | `file` via `canvas_node` — CSV/TSV render as tables, PDFs render inline, other binaries show a size placeholder. Never paste a CSV into a markdown fence. |130| Code review / unified diff (0.4.6+) | `diff` via `canvas_node` (content = diff text; link to its file node with a `references` edge) |131| Flowchart / sequence / state diagram (0.4.6+) | `mermaid` via `canvas_node` (content = mermaid source; renders client-side, no hosted app) |132| Image | `image` via `canvas_node` |133| Cached URL content | `webpage` via `canvas_node` |134| Structured UI | `json-render` via `canvas_render` |135| Chart | `graph` via `canvas_render` |136| Live work-item board (0.4.6+) | `canvas_render { action: "workboard" }` — one board node, auto-refreshes on work-item changes |137| Generated communication surface | HTML primitive via `canvas_node` |138| Give the agent tasks + watch progress + loop (0.4.7+) | `ax-board` HTML primitive — a live AX control surface, created AX-enabled |139| The same, drawn as a task flow, and materializable to real nodes+edges (0.4.7+) | `ax-flow` HTML primitive — **Materialize to board** lays the steps out as nodes joined by `flow` edges with a loop-back edge, each linked to a work item |140| Drive a flow WITHOUT the panel (0.4.7+) | Materialized step nodes carry native Start/Done/Blocked controls, and the anchor adds Run loop/Stop + steer. The native loop runs server-side, so it survives a browser reload and keeps advancing while the tab is closed. |141| Self-contained HTML/JS | `html` via `canvas_node` |142| Hosted interactive MCP app | `canvas_app { action: "open-mcp-app" }` |143| Excalidraw diagram (interactive/human drawing) | `canvas_app { action: "diagram" }` — prefer `mermaid` for agent-authored diagrams |144| Bundled React artifact | `canvas_app { action: "build-artifact" }` |145146Use the lightest tier that communicates the result. Do not build a web artifact when markdown,147json-render, a graph, or an HTML primitive is sufficient.148149## Current MCP Composites150151The live MCP surface is **22 tools**: the 16 composites below plus 6 standalones. This table is152kept in sync with the server's composite registry; `tools/list` on a fresh `pmx-canvas --mcp` is153always authoritative, independent of the installed release version.154155| Composite | Actions |156|-----------|---------|157| `canvas_node` | `add`, `get`, `update`, `remove` |158| `canvas_render` | `describe-schema`, `validate`, `add-json-render`, `stream-json-render`, `add-graph` |159| `canvas_edge` | `add`, `update`, `remove` |160| `canvas_group` | `create`, `add`, `ungroup` |161| `canvas_history` | `undo`, `redo` |162| `canvas_view` | `arrange`, `focus`, `fit`, `clear`, `remove-annotation` |163| `canvas_query` | `search`, `layout`, `validate` |164| `canvas_webview` | `status`, `start`, `stop`, `resize`, `evaluate` |165| `canvas_app` | `open-mcp-app`, `diagram`, `build-artifact` |166| `canvas_ax_state` | `get`, `set-focus`, `set-policy`, `report-capability`, `presence`, `set-presence` |167| `canvas_ax_work` | `add`, `update`, `annotate` |168| `canvas_ax_gate` | `request`, `resolve`, `await` with `approval`, `elicitation`, or `mode` |169| `canvas_ax_timeline` | `read`, `record-event`, `add-evidence`, `send-steering` |170| `canvas_ax_delivery` | `claim`, `mark` |171| `canvas_snapshot` | `save`, `list`, `restore`, `delete`, `gc`, `diff` |172| `canvas_intent` | `signal`, `update`, `clear` |173174Important routing:175176- Basic nodes: `canvas_node { action: "add", type, ... }`177- HTML: `canvas_node { action: "add", type: "html", html }`178- HTML primitive: `canvas_node { action: "add", type: "html", primitive, data }`179- Graph: `canvas_render { action: "add-graph", ... }`180- JSON render: `canvas_render { action: "add-json-render", ... }`181- MCP app: `canvas_app { action: "open-mcp-app", ... }`182- Excalidraw: `canvas_app { action: "diagram", ... }`183- Web artifact: `canvas_app { action: "build-artifact", ... }`184185As of v0.3.0, the 57 legacy single-purpose tools from the v0.2 compatibility window are removed.186The composites above plus the retained standalones are now the whole MCP surface: `canvas_batch`,187`canvas_pin_nodes`, `canvas_screenshot`, `canvas_ax_interaction`, `canvas_ingest_activity`, and188`canvas_invoke_command`. Snapshots are the `canvas_snapshot` composite (actions189`save | list | restore | delete | gc | diff`); the 6 legacy snapshot standalones were removed in190v0.4.0 after their deprecated 0.3.x window.191192## Spatial Rules193194- Treat proximity as relatedness and top-left to bottom-right as reading order.195- Search before adding to avoid duplicate nodes.196- Extend the current board in place; do not evict prior nodes to add new material.197- Use groups only when the frame communicates meaningful containment.198- Keep related nodes 40–80 px apart and separate unrelated clusters by roughly 150–250 px.199- Use directed edges for actual relationships, not decoration.200- Edge types: `flow`, `depends-on`, `relation`, `references`.201- After manual or batch layout changes, run `canvas_query { action: "validate" }`.202203## In-View Placement & Sizing (required for user-facing nodes)204205Auto-placement (omitting `x`/`y`) is **board-relative, not camera-relative**: it places right of206the last node or scans rows from the origin, ignoring where the human is looking. On a board with207distant nodes, an auto-placed node lands off-camera.2082091. Omit `x`/`y` only on an empty or locally dense board. Otherwise place near the human's210 attention: the pinned/focused neighborhood, or explicit coordinates beside the last211 user-facing output (gap ≥ 24–48 px).2122. After creating nodes the human should see, pan the camera (operating-sequence step 7b):213 `focus` for one node, `fit` with exactly the new `nodeIds` for a cluster. Use `noPan`214 (`focus --no-pan`) only when you must not steal the camera.2153. Never fit the whole board to "show" new work — on a board with outliers that miniaturizes216 everything. Always pass explicit `nodeIds` to `fit`.2174. Never leave user-facing output at far coordinates without a focus/fit.2185. **Do not hand-compute a whole board's coordinates.** Create the nodes (omitting x/y), then219 `canvas_view { action: "arrange", layout: "grid" | "column" | "flow" }` and finish with a220 `fit` over the new ids. Manual pixel math is what produces long-line, unbalanced boards.2216. `fit` sizes itself to the connected browser window (0.4.6+) — you do not need to guess222 `width`/`height`. Pass them only to fit for a window other than the human's.2237. **Every node is a canvas card.** Nodes render on the canvas where you put them — there is224 no docked/HUD placement and no `dockPosition` (removed in the rail-chrome redesign); `status`225 and `context` nodes are ordinary cards like the rest.226227**Size for content.** Omitting `width`/`height` gives readable per-type defaults — prefer them:228markdown 640×420, status 360×200, file 520×360, diff 640×420, mermaid 640×460, html 720×640,229graph 760×520, mcp-app 960×600, web-artifact 960×720. A *hosted* app opened with230`canvas_app { action: "open-mcp-app" | "diagram" }` — including the Excalidraw diagram preset — is231the exception: request `width: 720, height: 500` for its standard landscape preset instead of232relying on the 960×600 default for a directly created `mcp-app` node. After the app mounts, read233the node back with `canvas_query { action: "layout" }` and use its persisted width and height for234subsequent placement or layout; the mounted app may have changed the requested dimensions.235Since 0.4.6 the server clamps explicit creation sizes UP to per-type236readability floors (e.g. markdown 360×180, graph/json-render/html 420×280, mcp-app 480×320) —237a tiny probe size silently becomes the floor. `strictSize: true` is the only opt-out (a fixed238scrolling frame you genuinely want small). `canvas_query { action: "validate" }` additionally239reports any node below its floor as an advisory `sizeWarnings` entry — treat a non-empty list240as layout work left to do.241242**Token hygiene.** For routine state checks use `canvas_ax_state { action: "get" }` WITHOUT243`includeContext` — the full AX context payload is ~10× larger; request it only when you are244actually consuming context.245246## Context Pins247248Context pins are the primary human-to-agent bridge:2492501. Human pins nodes in the browser using **Pin as context**.2512. Agent reads `canvas://pinned-context`.2523. The resource includes pinned nodes and nearby unpinned neighbors.253254Do not confuse context pinning with **Lock position**, which only excludes a node from auto-arrange.255Every node type, including `status`, can be removed through `canvas_node { action: "remove" }`, the256title-bar × control, or the **Close** context-menu action.257258## Sessions & the human259260The board has three modes, all gated on one fact — whether a session is attached:261262- **Quiet board** (nobody attached, nobody writing): a plain canvas. Your writes still show as263 ghosts, nothing else changes.264- **External steering** (you write with no session attached): the top bar shows a passive265 writers indicator with an activity feed listing each write under your label, plus a266 connected-writers sheet. Identify yourself (`PMX_CANVAS_AGENT_SOURCE`, or `x-pmx-source` on267 HTTP) so the feed names you, not the transport. Pending explicit intents carry an inline Veto268 there.269- **Focus session** (attached): your cursor sits on the node you last touched with a phase chip270 (`idle` / `thinking` / `tooling` / `waiting-approval`); every MCP/HTTP write is attributed to271 your session automatically (pass `agentId` only to keep a sub-agent separate); the human gets272 the session panel (work items, approval gates, timeline of your tool runs, board writes,273 evidence and steering), a command bar that posts steering you read on your next turn (with a target274 picker when several agents are connected), a timeline filterable by kind (Updates / Steer /275 Events / Evidence), and a context meter. That meter is the pinned-context payload estimated against a configured budget276 ("Pins") unless your host reports your real token usage on the presence277 (`set-presence { contextUsage: { used, total } }`) — then it shows your actual window278 ("Context"). The Copilot extension reports it automatically; other hosts report it themselves. Report `phase: "thinking"` before a long reasoning stretch if your host279 gives you a hook; `tooling` is derived from your writes. Hosts with adapters (the Copilot280 extension) attach for you; the human can also start a session from the board's *Start agent281 session* button — your writes (transport or `PMX_CANVAS_AGENT_SOURCE` label alike) are282 attributed to it and it takes your name.283- **No adapter? Use the pump.** Any CLI agent becomes steer-reactive with one command in a284 terminal: `pmx-canvas pump --consumer <your-key> --exec '<command>'`. It long-polls your285 delivery queue, runs the command once per steer (message on stdin plus the286 `PMX_STEER_MESSAGE`/`ID`/`SOURCE`/`TARGET`/`CREATED_AT` env envelope), and marks287 per-consumer only after the command exits 0 — a failed hand-off stays pending and the pump288 exits non-zero. `{message}` in the template expands to a quoted env reference (never spliced;289 refused on Windows — read stdin there). `--parent <key>` rolls you up under an orchestrator's290 chip; `--once` for scripts. See `pmx-canvas pump --help`.291292What the session asks of you:293294- **Detach explicitly** (`attached: false` or a `session-end` activity). Attaching over a295 non-empty board saved a `Before session · …` snapshot; detaching emits the receipt (items296 done / vetoed, a diff against that snapshot, one-click restore) — an idle timeout delays it.297- **403 = outside the scope fence.** The human may fence you to a region (`policy.scope`): writes298 outside it are refused with a reason naming the node or position (a fenced group frame299 grants its members too). Read `policy.scope` in `canvas://ax-context`, ask the human to widen300 it, never retry blindly. The fence is the human's: `set-policy` does not take `scope`.301- **409 = a human is holding that node** (dragging or editing it right now). Requeue the change302 and retry in a moment. If you had signalled an intent on it, it was vetoed — a `yield` timeline303 event says who took over.304- **A gate you do not answer auto-holds** (`held`, default TTL 5 min). `held` is a non-approval:305 do not proceed; the human can reopen it from the panel. A rejection reaches you as steering.306- **Your latest edit can be undone from the panel.** One shared undo stack: when the human undoes307 it you get steering ("Undid your edit: …"); treat the board as the truth, not your last write.308- **Steering arrives as `steering-message` rows** in `canvas://ax-timeline` (or claim them via309 `canvas_ax_delivery`). The command bar, gate rejections, undo and take-overs all speak through310 it. Steering may be ADDRESSED: with several agents connected the human picks a recipient in311 the composer, and a claim only returns broadcasts plus messages targeted at your `consumer`312 label — so always claim as yourself (your `PMX_CANVAS_AGENT_SOURCE` label), or addressed313 steering never reaches you.314- **Steering is also the agent-to-agent mailbox.** To coordinate with another agent on the315 board, send `canvas_ax_timeline { action: "send-steering", message, target: "<their316 consumer label>" }` — they claim it once on their next turn, you never receive your own, and317 the human sees the exchange in the panel as "you → them · message". Read318 `canvas_ax_state { action: "presence" }` for who is connected. Park shared state in work319 items and evidence; split territory with group frames rather than editing the same node.320321## Browser Workflows322323Use the visible workbench when the human is actively curating layout:324325- Drag nodes to move them.326- Drag empty space to lasso-select (Select tool, the default); hold Space or pick the rail's Pan327 tool to pan instead — the Pan tool pans even when the drag starts on a node.328- The selection bar (floating bottom-center) offers count, align left/top, distribute,329 auto-arrange, Group (G), Connect, Pin as context, delete and clear; selected nodes show an330 accent outline with corner handles.331- Double-left-click a node for context pinning, position locking, focus, collapse, connect, refresh,332 open, close, and type-specific actions.333- Double-left-click empty canvas for the node-creation menu; hold and drag the right mouse button to pan.334- Drop files or URLs to create matching nodes; an empty board shows starter actions (new note,335 pick files, paste a link, start an agent session).336- Use a markdown node's Edit button to edit its content.337- **Groups** are frames with the name pill and an action cluster (auto-arrange children, collapse,338 ⋯ rename / ungroup / pin all) on the top edge. Membership changes only on release while the339 "release to add to <group>" pill shows (Esc keeps it out); dragging a child fully out offers340 "release to remove". A collapsed group is a chip that hides its children (edges to them draw341 to the chip). G groups the selection, Shift+G ungroups. Ungroup *dissolves* the frame (children342 stay, nested children move up one level, one undo step) — and your343 `canvas_group { action: "ungroup" }` is the same operation with the same result.344- **Edges**: drag from a node port, or pick the rail's Connect tool (C) and drag from anywhere on345 a node; the target lights up, Esc cancels, L asks for a label on release.346- The **History** drawer (rail camera button, or the receipt's Full log) lists snapshots and347 agent sessions in one timeline; save a snapshot there before experiments and restore only after348 confirmation. Ctrl/Cmd+Z and Shift+Z work the shared undo stack.349- The **minimap** (bottom-right of the canvas) is a true-scale map; hover magnifies it, click350 jumps, drag pans. A banner under the top bar reports a dropped stream (reconnecting) or a351 post-reconnect resync; edits still save over HTTP meanwhile.352- Other open tabs appear as green cursors with a name tag (`/workbench?name=mia` sets yours);353 a node you drag is locked for agents until you release it.354- The chrome is a persistent 52px left tool rail plus a slim 44px top bar (0.4.8+). The rail355 carries the tools (Select V, Pan Space, Connect C), node creation (markdown M, image I, file356 Shift+F, webpage W, HTML surface H, group G, annotate A — a popover with draw / text / eraser),357 and utilities: search & commands (Cmd+K — actions with shortcuts, then jump-to-node), arrange,358 trace, minimap, history, the theme picker (nine themes: dark, light, high-contrast, midnight,359 sepia, arctic, ember, forest, volt), and shortcuts (?). The top bar holds the connection dot,360 workspace title, the session chip / gate badge / context meter while a session is attached361 (or the external-writers indicator and *Start agent session* otherwise), and the zoom cluster362 (zoom out, % label = reset, zoom in, fit F). Hovering a rail button shows a tooltip with its363 shortcut.364 On viewports ≤1180px the top bar drops its meta text and the session panel becomes a drawer;365 every control stays in the rail at any width — there is no separate mobile menu.366- Keyboard: nodes are focusable — arrow keys move to the nearest node, Enter opens it in focus367 mode (a scrim + inset view with Open in tab), Esc closes the top-most overlay.368- Embedding hosts can open `/workbench?theme=<name>` (or `?theme=auto` to follow the host's369 light/dark appearance) for a session-local default theme that never changes the server-global370 theme other clients see; an explicit pick from the theme menu ends the override. The bundled371 Copilot extension opens its panel with `?theme=light`.372373After changing files under `src/client/`, rebuild with `bun run build` before manual browser374verification.375376## AX Interactions377378Node interactions request PMX AX primitives; they never execute arbitrary shell, tools, MCP calls,379or host actions.380381- `DEFAULT_NODE_AX_CAPABILITIES` is the per-node-type ceiling.382- `data.axCapabilities` may enable or narrow capabilities but cannot escalate beyond the ceiling.383- Sandboxed surfaces are scoped to their own source node.384- HTML nodes must explicitly opt in.385- Use `window.PMX_AX.emit(type, payload)` and await its result.386- Listen for `pmx-ax-update` when an HTML control surface reflects live AX state.387- Steering is queued; claim with `canvas_ax_delivery`, act, then mark delivered.388389Read [AX HTML control surfaces](references/ax-html-control-surface.md) before building an390interactive AX-enabled HTML node.391392### Ready-made control surfaces (0.4.7+) — prefer these over hand-rolling393394Two HTML primitives ship AX-enabled, so you do not need to author a control surface or pass395`axCapabilities` yourself. They are the ONLY primitives created with AX on; every other kind is396still a static document.397398- **`ax-board`** — task list: create tasks, watch status change live, steer, bounded loop.399- **`ax-flow`** — the same controls drawn as a task flow with a loop-back rail, plus400 **Materialize to board**.401402**`ax.flow.materialize`** is the one interaction that creates canvas nodes, and it is deliberately403narrow: you supply TEXT ONLY (<= 12 steps, title <= 120 chars) and the server owns the result —404one `markdown` node per step, `flow` edges between them, a dashed `references` loop edge, and one405work item per step linked to its node. Re-materializing REPLACES the previous flow (the source406node keeps a manifest of what it created), so it is safe to call repeatedly.407408**The flow also works without the panel.** Materialized step nodes carry `data.axStep`, and the409canvas renders native Start / Done / Blocked controls on them, with Run loop / Stop and a steer box410on the anchor (first) step. Those controls also follow the node into focus mode. The native loop411runs **server-side**: it advances when a step's work item completes, survives a browser reload, and412keeps going while the tab is closed — unlike the panel's loop, which dies with the iframe. Bounds413are the same either way: advances only while running, hard cap 20 runs, Stop persists immediately,414and `blocked`/`cancelled` halts it.415416When you want the agent to work through a checklist the human can watch and steer, reach for417`ax-flow` + materialize rather than a markdown to-do list — the status chips and the loop come free.418419### Working a flow: keep the board honest as you go420421A materialized flow is only useful if it tracks reality. While you work one:4224231. **Move the step to `in-progress` BEFORE you start it, and to `done` when it is actually done** —424 not in a batch at the end. The human is watching the chips to know where you are; a board that425 only updates on completion tells them nothing while it matters.4262. **Pin the in-progress step as context** (`canvas_pin_nodes` with just that node, or427 `POST /api/canvas/context-pins` mode `set`). This makes the active step the one thing in428 `canvas://pinned-context`, so the board's "what am I working on" and your own context are the429 same answer instead of drifting apart. Re-pin as you advance.4303. **Mark `blocked` rather than going quiet** when you are stuck — a stalled `in-progress` chip is431 indistinguishable from a crashed agent.4324. Read steering back with `canvas_ax_delivery` as you go; a human watching a live flow steers433 mid-run, and a steer you never claim is a correction you ignored.434435## Resources436437Read the smallest resource that answers the question:438439- `canvas://pinned-context` — curated context plus neighborhoods440- `canvas://summary` — compact board overview441- `canvas://layout` — complete state442- `canvas://spatial-context` — clusters and reading order443- `canvas://history` — mutation history444- `canvas://code-graph` — detected file dependencies445- `canvas://ax-context` — compact AX context446- `canvas://ax-work` — work items and gates447- `canvas://ax-timeline` — events, evidence, steering448- `canvas://ax-pending-steering` — adapterless delivery queue449- `canvas://skills` and `canvas://skills/<name>` — bundled skills450451Prefer `canvas_query { action: "search" }` over parsing the full layout.452453## Known Limitations454455- Hosted MCP-app/ext-app nodes such as Excalidraw require the in-canvas host bridge and are not456 standalone **Open as site** targets. URL-backed viewers and bundled web artifacts remain457 openable.458- A standalone html surface (`/api/canvas/surface/:id` opened as a site) is a VISUAL view: it459 renders the same content and theme, but `window.PMX_AX` is not injected without the canvas460 iframe's per-mount nonce, so AX buttons only work inside the in-canvas node (0.4.4 Codex note).461 Do not tell a user a standalone tab's controls will steer the agent.462- A hosted ext-app (Excalidraw) node in a **WebKit** host panel (e.g. the GitHub Copilot app's463 embedded WKWebView) historically could render as a black tile — a host compositor paint race464 on the nested iframe, **not** a broken node (the session is healthy, `sessionStatus` is465 `ready`, and it renders fine in Chrome/Codex). Since 0.4.6 the canvas runs a **paint oracle +466 recovery ladder** under WebKit: present-at-load ext-apps mount strictly one at a time (the467 cold burst was the trigger), each frame answers a double-rAF **paint probe** after settle, and468 on silence the ladder escalates — soft-expand cycle (the automatic analogue of the proven469 enlarge+close) → serialized remount → an explicit "App surface failed to paint / Retry"470 affordance. The connecting overlay stays up until paint is confirmed, so a black layer is471 never presented as ready. The recovery trail472 (`GET /api/canvas/debug/ext-app-recovery` / `window.__PMX_EXTAPP_LOG`) now records473 `mount-slot`, `paint-ok`, `paint-fail`, `soft-expand-cycle`, `recovery-exhausted`, and474 `assume-visible-rearm` — when diagnosing, trust `paint-ok`/`paint-fail`, and never assert475 health from `settled` alone. Exception: in a host that reports the document hidden (the476 GitHub Copilot panel does so continuously), `paint-ok` is recorded as477 `paint-ok (unverified: host hidden)` — the app answered the paint probe, but nothing is478 proven composited, so do not treat it as a verified paint. If a479 tile still shows the Retry affordance, click Retry (fresh recovery budget) or expand-then-close;480 attach the recovery trail when reporting.481- Ext-app frame documents live in server memory. Through 0.4.0, killing/restarting the daemon482 while a panel stays open leaves ext-app tiles on dead frame URLs (`Frame document not found`,483 0.4.0 report Finding S) until a full workbench reload. Since 0.4.1 the browser revalidates its484 frame documents on every reconnect and re-mints them against the new process automatically —485 if a post-restart tile still looks blank on an older install, reload the workbench page.486- A hosted ext-app (Excalidraw) resized NARROW/TALL (e.g. 360x529) can show its diagram in the487 upper region with the app's own dark fill below it — in every engine (0.3.4 report Finding Q).488 This letterboxing lives inside the hosted app bundle's root container, not in PMX (a body-level489 background override ships but cannot reach the app's inner root). Keep ext-app tiles landscape —490 at or above the 720x500 they open at, or the same ratio — or expand the node: the fullscreen491 overlay renders full-bleed. The durable fix is upstream in the excalidraw-mcp app.492- Behind proxies that buffer streaming responses (e.g. portal hosts), the workbench auto-falls493 back from SSE to a polling transport within ~3s, so the board still boots and stays live.494 Force a mode with `/workbench?transport=poll` (or `transport=sse`) when diagnosing.495- Nested-iframe embeds (e.g. the Amp orb portal renders the canvas page inside an ampcode.com496 iframe) can block child iframes from loading ANY `src` URL, breaking every iframe-backed node497 with a gray placeholder. The canvas probes this at boot and auto-falls back to fetching498 same-origin surfaces and rendering them inline via `srcdoc` (HTML, graph, json-render, frame499 documents). In Amp orbs specifically, the server sees `AMP_ORB` in its environment and adapts500 end to end: it binds the portal-assigned `$PORT` automatically (no port flag in the service501 command), stamps the page so the embedded client skips the (there-unreliable) probe, goes502 straight to `srcdoc`, AND defaults to the polling transport (the portal proxy buffers SSE —503 waiting out the watchdog could trip the boot modal); HTML surfaces also inline their theme504 stylesheet so they render styled. External app505 URLs cannot be inlined (cross-origin) and may stay blocked in such hosts. Force a mode with506 `/workbench?iframe-mode=srcdoc` (or `iframe-mode=src`) when diagnosing.507- Graph and json-render standalone surfaces use `display=site` and fill the browser viewport, and508 reflow on a live window resize. Some single-tab host browsers historically didn't deliver509 live-resize events; current Codex builds do — the 0.4.6 pass watched a standalone graph reflow510 live (SVG 1550×783 → 850×483 on a 1600×900 → 900×600 window resize) with no reload. Do not511 present a system browser as a workaround for stale resizing; it is only a preference for viewing512 a full page beside the canvas.513- Some hosts cannot automate inside sandboxed workbench iframes. Verify those interactions in a514 system browser or through server-side AX state.515- `pmx-canvas screenshot` requires an active WebView. Start it with516 `canvas_webview { action: "start" }`.517- The default server port is 4313, but it may fall back or be explicitly changed.518519## Persistence520521State lives under `.pmx-canvas/`, primarily in `canvas.db`. It includes viewport, nodes, edges,522annotations, pins, snapshots, AX canvas state, and large-node blobs.523524- Stop the server or close/flush the SDK before committing `canvas.db`.525- History is session-scoped and is not persisted.526- Timeline AX data persists independently from canvas snapshots.527- `canvas_view { action: "clear" }` clears canvas-bound state but not host/session diagnostics.528529## Detailed References530531Load only the reference relevant to the task:532533- [Full MCP, HTTP, CLI, layout, and workflow reference](references/full-reference.md)534- [Installing PMX Canvas](references/installing-pmx-canvas.md)535- [HTML primitives](references/html-primitives.md)536- [Excalidraw diagram authoring](references/excalidraw-diagram-authoring.md)537- [AX HTML control surfaces](references/ax-html-control-surface.md)538- [GitHub Copilot adapter](references/github-copilot-app-adapter.md)539- [Codex app adapter](references/codex-app-adapter.md)540541The authoritative current MCP inventory and legacy replacement table is542[`docs/mcp.md`](../../docs/mcp.md).