pmx-canvas 0.5.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/Readme.md +10 -7
  3. package/dist/canvas/global.css +101 -7
  4. package/dist/canvas/index.js +171 -128
  5. package/dist/canvas/mermaid-entry.js +1303 -1303
  6. package/dist/json-render/index.js +101 -102
  7. package/dist/types/client/canvas/use-pan-zoom.d.ts +2 -1
  8. package/docs/adr-001-bun-only-runtime.md +1 -1
  9. package/docs/bun-webview-integration.md +2 -2
  10. package/docs/node-types.md +1 -1
  11. package/docs/product-context-vision-2026-09.md +207 -0
  12. package/docs/product-review-2026-09.md +174 -0
  13. package/docs/product-vision-2026-09.md +200 -0
  14. package/docs/screenshot.png +0 -0
  15. package/package.json +2 -2
  16. package/skills/pmx-canvas/SKILL.md +62 -36
  17. package/skills/pmx-canvas/evals/evals.json +1 -1
  18. package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +5 -0
  19. package/skills/pmx-canvas/references/full-reference.md +5 -4
  20. package/skills/pmx-canvas/references/installing-pmx-canvas.md +126 -18
  21. package/skills/pmx-canvas-testing/SKILL.md +13 -4
  22. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +18 -8
  23. package/src/client/canvas/CanvasNode.tsx +9 -12
  24. package/src/client/canvas/CanvasViewport.tsx +20 -33
  25. package/src/client/canvas/ContextMenu.tsx +13 -0
  26. package/src/client/canvas/EdgeLayer.tsx +2 -2
  27. package/src/client/canvas/SessionPanel.tsx +23 -4
  28. package/src/client/canvas/ShortcutOverlay.tsx +2 -2
  29. package/src/client/canvas/TopBar.tsx +85 -45
  30. package/src/client/canvas/use-pan-zoom.ts +23 -3
  31. package/src/client/nodes/ExtAppFrame.tsx +97 -28
  32. package/src/client/state/canvas-store.ts +17 -5
  33. package/src/client/state/sse-bridge.ts +3 -1
  34. package/src/client/theme/global.css +101 -7
  35. package/src/server/canvas-operations.ts +2 -2
  36. package/src/server/operations/mcp.ts +45 -31
  37. package/src/server/operations/ops/ax-presence.ts +3 -0
  38. package/src/server/operations/ops/viewport.ts +6 -1
  39. package/src/server/scope-fence.ts +1 -1
  40. package/src/server/server.ts +9 -9
  41. package/src/server/web-artifacts/scripts/bundle-artifact.sh +29 -8
@@ -0,0 +1,200 @@
1
+ # Product Vision — September 2026
2
+
3
+ **Status:** Proposed
4
+ **Date:** 2026-09-05
5
+ **Scope:** Where `pmx-canvas` should go, what must be fixed now, what to add, what to delete, and what is architecturally wrong. Written against `main` `e17776f6` (clean tree).
6
+ **Method:** My own position, drafted first, then stress-tested by a 54-agent panel: four fact-finders, six independent visions from different angles (context engineering, systems, product strategy, rendering, developer experience, minimalism), a merge into 14 moves, three adversarial refuters per move (evidence, feasibility, value), and a completeness critic. Where the panel refuted me, this document says so. Companion: [`product-review-2026-09.md`](product-review-2026-09.md) (the audit).
7
+
8
+ ## The one-line vision
9
+
10
+ **The board is the agent's working memory, and the human's attention is the compiler.** Explicit steering directs agents; annotations, pins, connections, and grouping inform relevance, while spatial layout and human camera attention can contribute weaker cues. The server compiles one budgeted brief for every agent turn. Everything the agent has to say to the human is a card on the board, never a row in a side table. The board owns its own store and its own history, and reaches remote agents through its own authenticated network mode. One journal records every write by either side, so time on the board can be scrubbed like a video. In the long run the board is hosted, and a team and its agents share it as one working memory.
11
+
12
+ **Purpose clarification, 2026-09-06:** pmx-canvas is a visual workspace for **planning, discovery, analysis, and coordination with agents**. This is its intended purpose, not merely an initial audience experiment. Its promise is to let people shape the work spatially, carry useful understanding into the next task, and see known changes and uncertainty in the context they use. The [companion vision](product-context-vision-2026-09.md#position) develops these promises; personal and shared knowledge support the working surface rather than replace it. Attention does not grant approval or sharing permission.
13
+
14
+ ## What the real boards show
15
+
16
+ Two corrections from the maintainer shaped this section. First, boards inside the `pmx-canvas` repository are test boards by design. Second, the real boards are mixed in with test boards on the board the globally installed MCP server writes to. That board lives in the directory the host spawns the MCP server from, plus a worktree copy. Its live state today is a fixture set (`F Graph`, `Excal A` to `E`), but its snapshot history holds every board that came before, and that is where the real work is. Read-only, titles only, snapshot ids decoded to dates:
17
+
18
+ | Date | Real board (from snapshot history) | Content |
19
+ |---|---|---|
20
+ | 2026-04-15 | A quarterly OKR and developer-metrics board | 8 objective cards, 8 metric charts, 1 dashboard app |
21
+ | 2026-05-11 | A workshop deck on working with agents | 1 HTML deck, 15 slides |
22
+ | 2026-05-27 | Vibe cluster feedback, agent dev environment | 2 HTML surfaces |
23
+ | 2026-06-22 | A product discovery board | 17 markdown cards in 4 groups, 1 double-diamond HTML |
24
+ | 2026-06-25 to 07-08 | Vibe coding cluster model, agent kanban | 1 Excalidraw app, 2 HTML surfaces |
25
+ | 2026-07-07 | Two training tracks for a workplace rollout | 2 markdown outlines, 2 HTML slide decks, 1 file |
26
+ | 2026-08-03 | A metrics read on delivery cycle times | 1 markdown, 5 charts, 1 file |
27
+ | 2026-08-07 | A department-wide planning overview | about 30 markdown cards, a status card, a six-step flow |
28
+ | 2026-08-10 to 08-28 | Work board | 1 json-render surface, the longest-lived real node |
29
+
30
+ Three things follow, and they change the vision more than anything the panel produced.
31
+
32
+ 1. **The product is used for real, by a PM, for thinking and presenting with agents:** OKR boards, metric charts, discovery boards, org overviews, slide decks. Nine sessions over four and a half months, almost all agent-written from the maintainer's prompts.
33
+ 2. **Every real board was destroyed by the next session.** One board per workspace means the planning overview, the discovery board and the OKR board exist today only as snapshot rows under test fixtures. The product's own persistence model is what makes real use invisible. It is the first thing to fix, and nobody on the panel saw it because nobody looked in the snapshot tables.
34
+ 3. **The rich surfaces are real-use surfaces.** Thirteen real chart nodes, five real HTML decks and surfaces, two real Excalidraw apps, one long-lived json-render board. My earlier cut of `graph` and json-render was wrong on the evidence, and the maintainer's broader point stands: his own usage is not evidence that any type is unused. No node type is cut anywhere in this document; move 5 changes only how they render.
35
+
36
+ What still holds from the test boards: steering is the most exercised human verb (84 to 89 browser-sent steers per board), pins are rare (1 or 2 live), and snapshots accumulate without bound (273 and 337 on boards of 14 and 16 live nodes, 24 MB each), which is now doubly telling, since snapshots are the only place the real work survived.
37
+
38
+ ## Part 1: The foundation
39
+
40
+ Move 0 comes first, so real boards stop being overwritten. Then three changes that everything in Part 2 stands on. None of them is speculative; each is worth shipping on its own.
41
+
42
+ 1. **One read.** `canvas://context?budget=N` is the only thing an agent reads per turn: pinned nodes first, then what changed since this consumer's last read (a stable seq cursor), then human-authored nodes, then open asks and undelivered steers for this consumer. Nothing done, delivered or system-generated ever enters it. Everything else stays reachable by explicit pull. It folds 10 of 14 resources into the one read and turns spatial analysis (clusters, neighborhoods, reading order) into an input to the ranker rather than a separate resource.
43
+ 2. **The board fills itself.** Nobody on the panel proposed this and it is the cheapest radical move available: stop requiring the agent to remember to call `canvas_node`. A Claude Code hook, the Copilot extension and the Codex heartbeat project the session onto the board, meaning files touched, diffs, test output and the plan, into a session lane the human then curates from abundance. Today a node exists only when an agent decides to write one.
44
+ 3. **Author on every node.** The `nodes` table has no writer column, and provenance lives only in an in-memory ring that dies on restart. Add `author`, human or agent id. The compiler ranks human-written above agent-written, the board can show who wrote what, and hosted multiplayer needs it later anyway.
45
+
46
+ ## Part 2: The shape I believe in
47
+
48
+ Twelve moves, numbered from the one the evidence demands first. Move 3 was rewritten and move 6 split after the maintainer's decisions on 2026-09-06. Each carries the panel's strongest objection and my answer, because several of them changed the move.
49
+
50
+ ### 0. Many boards per workspace (S)
51
+
52
+ The move the evidence demands before any other. A workspace holds many named boards, switchable from the top bar and addressable from every transport (`board` on the MCP session, `--board` on the CLI, one journal per board once move 4 lands). A new session starts on a new or chosen board; test runs get their own boards; nothing is overwritten. Recovery for the maintainer today: the nine real boards above can be restored from snapshot history into named boards in an afternoon. Deletes nothing. Objection: none, the panel did not see it.
53
+
54
+ ### 1. One read, one brief (M)
55
+
56
+ Above. Objection: ranking by "the human's camera" rests on a signal the product does not have; the server holds one global viewport that agents also write via `view.fit` and `view.focus`, so a camera ranker would rank the agent's own last focus. Accepted. The camera rides the per-tab human-presence heartbeat, viewport writes get an actor like every other write, and the camera is a weak tiebreak, never the primary axis. Primary axes: pins, delta since last read, author, recency.
57
+
58
+ ### 2. The session projects itself onto the board (M)
59
+
60
+ Above. Objection: none raised; the panel missed it. Risk: noise. Mitigation: the projection lands in its own lane at low rank; the compiler's tiers keep it out of the brief unless pinned or recent.
61
+
62
+ ### 3. The agent's output moves onto the board (M)
63
+
64
+ **Decided with the maintainer, 2026-09-06: the fleet features stay.** They are new, deliberate capability, and the same rule that protects the node types protects them. Scope fences, consumer keys, the `pump`, parent rollups, census chips, addressed versus broadcast steering and per-consumer delivery are all kept as they are. What follows is a relocation, not a cut.
65
+
66
+ The problem this move solves is one surface too many. When the agent opens a work item, raises a gate, asks a question or files evidence, it lands in a side table that the canvas cannot show. The human reads it in a panel while the board, the surface built for exactly this, stays blank about it. Two side effects follow: deleting a node forces the server to re-anchor orphaned items, and the panel and the board have different lifetimes, since canvas-bound state rides snapshots while the timeline does not.
67
+
68
+ So the agent's output becomes nodes. A work item is a `status` node carrying the work state. Approval gates, elicitations and mode requests become one new `ask` node with a typed answer, human-only (move 7). Evidence is a node or a file node. Every gesture that already exists then applies to agent output: drag it beside the file it concerns, group it, pin it, connect it to the diff that raised it. Nothing to re-anchor, because the item is the node. One lifetime, because there is one surface.
69
+
70
+ The evidence is the maintainer's own board. When three agents reviewed the product for him, they wrote markdown cards and patched each other's sections rather than filing work items. The card won because it was visible and spatial. This move makes the winner the design.
71
+
72
+ Deletes: the duplicate storage only. The three timeline tables fold into the journal in move 4, which was already happening.
73
+
74
+ **Three primitives I would still question, as open questions rather than proposals.** The `policy` singleton, `host-capability`, and the command registry (`pmx.plan`, `pmx.review`, and the rest) each carry a schema, a route, an MCP action and a doc section, and I cannot find a use for them on any board or in any field report. I no longer claim the evidence to delete them, and the maintainer's rule is that my not finding a use is not proof. They are worth a deliberate decision, not a default one. Separately, ghost intents: keep the capability, but change the bundled skill so signalling one is opt-in rather than required before every mutation. That is a default, not a feature.
75
+
76
+ Objection (value lens): steer is the one verb the human demonstrably uses, so any move that touched it would remove the thing that works. Accepted, and now moot: steering, delivery and the fleet around it are untouched. Objection (feasibility): the presence cursors are the presence registry, so they cannot be separated. Accepted, and also moot: presence is untouched.
77
+
78
+ ### 4. One journal is the truth; the app owns its history and its store (L, then XL)
79
+
80
+ Beside the SQLite store the product already owns, one append-only journal (seq, actor, op, inverse patch) written by the single write path. Snapshots become named bookmarks on the journal; the History drawer becomes a time scrubber over it; SSE becomes journal tailing with `Last-Event-ID` (today there is no cursor, and every reconnect replays the full board); `watch` becomes "entries since seq"; undo survives restart. No external system is involved: the app is its own version control. The maintainer's databases make the case: 4.2 MB of which 3.1 MB is 196 full-copy snapshots of a 25-node board, and 273 to 337 snapshots on the boards where the real work lived.
81
+
82
+ A folder export (one markdown file per note plus a board file in the product's own schema) is worth having so a human or an agent can read a board without the server, but it is an export, not the store. With Obsidian and git both out of scope, the folder loses two of its three reasons to be canonical, and a canonical file that any process can write would make every trust guarantee advisory. The store stays SQLite; the journal is the truth for who did what.
83
+
84
+ Objection (feasibility, refuted as written): "a log written by `executeOperation` and nothing else" does not describe this codebase (487 `canvasState.` references in 40 files; the SDK makes 87 direct calls). Accepted: the XL half (state as a fold of the journal) comes after move 6 collapses the SDK onto the registry.
85
+
86
+ ### 5. Keep every node type; unify how they render (M)
87
+
88
+ The maintainer's rule, and it is the right one: his own usage is not evidence that a node type is unused, and a node that renders any MCP app belongs in the product. So no node type is removed, and the generic `mcp-app` node stays generic. What changes is the machinery under the iframe-backed kinds (`html`, `mcp-app`, `json-render`, `graph`, `webpage`, `mermaid`), which today is spread over six components with three copies of the AX bridge and one 1,585-line frame component whose recovery ladder produced nine rounds of black tiles.
89
+
90
+ One frame host for every iframe-backed kind: mounted lazily when the node is in or near the viewport, counted against a live-frame budget, given one paint deadline, and shown with one visible fallback and a retry when it misses, instead of an escalating oracle. Mermaid renders inline and lazily, with no per-node 3.5 MB document. The React chart bundle stays behind `graph` and `json-render` as types, loads only when such a node is on screen, and whether it is later replaced by a lighter renderer is a cost decision, not a usage one. The 21 HTML primitives, the web-artifact builder and every other surface stay as capabilities. One observation, not a cut: the web-artifact builder is the only feature that fetches a toolchain from the network at build time, which sits oddly beside the no-externals rule and deserves a deliberate look.
91
+
92
+ Deletes: the `ExtAppFrame` recovery machinery, two of three AX-bridge copies, five per-type iframe implementations. Capabilities removed: none. Every deletion the earlier version of this move argued from "zero real uses" was withdrawn once the real boards surfaced; what remains is argued from cost and incident count only.
93
+
94
+ ### 6a. Make the tool surface cheap (S) — ready to ship
95
+
96
+ Measured on this tree, `tools/list` is 49,222 bytes across 22 tools, about 12,300 tokens that every agent pays on every session before it does anything. Two tools account for a fifth of it: one advertises 51 optional properties, another 49, because a composite flattens four actions' fields into one schema. Per-action requirements exist only in prose, so a call missing a required field passes validation and fails in the handler.
97
+
98
+ Nothing structural is needed to fix most of that. Ship per-action schemas where hosts support them and a `canvas_help { op }` that returns one operation's schema on demand, so an agent loads what it uses instead of the whole surface. Add `session.start`, returning workspace, brief and inbox in one round trip, so a useful first session is about five calls. Fix the two live descriptions that are wrong today, and add the test that an action summary always equals its own action list. Put a token budget on every agent read, which is Part 1's one read.
99
+
100
+ This is the half the maintainer has already endorsed in direction. It removes no capability, renames nothing, and is worth roughly 10,000 tokens per session per agent.
101
+
102
+ ### 6b. Generate every surface from the registry (L, two releases) — pending the maintainer's decision
103
+
104
+ **Status: undecided as of 2026-09-06.** The maintainer likes the simplification and the token saving, and wants more time on the restructuring. Nothing below should start before that decision.
105
+
106
+ The proposal: `listOperations()` becomes the single published vocabulary. Five MCP tools (`canvas_read`, `canvas_write` taking a list so batch is just a longer list, `canvas_ask`, `canvas_inbox`, `canvas_help`) replace the 22. The CLI becomes one generic dispatcher over operation names plus eight human commands, and the SDK becomes a typed facade over the same invoker. The API document is rendered from the registry, replacing 23,000 words of hand-written prose that has drifted at least seven times.
107
+
108
+ The case for it: four naming conventions for one operation (`set-focus`, `ax focus`, a path, `setAxFocus`) mean an agent cannot transfer knowledge between transports, and about 14 operations exist in three surfaces but not in MCP with no error explaining the gap.
109
+
110
+ The case against, which is why it is worth thinking about: 22 named tools are self-documenting in a way that five generic ones are not, a host that renders tool names to the user shows something meaningful today, and a single write tool keyed on operation names needs a discriminated union to catch wrong-field arguments, which not every host supports yet. Generated CLI help is also usually worse than hand-written help.
111
+
112
+ If the answer is no, 6a still stands on its own, and the one piece worth taking regardless is routing the SDK through the registry, since that is a trust fix rather than a surface change: the SDK makes 87 direct state calls today and 4 registry calls, so an SDK write skips the human edit lock, the scope fence and the activity feed.
113
+
114
+ Objection (feasibility, refuted as bundled): the previous registry campaign took four pull requests plus a month of follow-up slices, so five refactors in six weeks will not land. Accepted: two releases, SDK first. Objection (evidence): "41 operations unreachable from MCP" is a field-count artifact; the real gap is about 14. Accepted.
115
+
116
+ ### 7. Trust that is not a label (S, then M)
117
+
118
+ The workbench marker is a plain header any agent with `curl` can set, so "human-only gate resolution keyed on the marker" is still theater. The server mints a secret at boot, serves it only in the workbench HTML, and human-only ops require it. Then: `ask` answers are human-only; file nodes are confined by default to the workspace plus explicit allow-roots (persistence hygiene, since every host already gives the agent `cat`); the webpage fetcher refuses loopback, private, and link-local addresses on every redirect hop; the client bridge checks `res.ok` and shows the toast on 400 and 500. One reservation I am not settled on: the real boards carry file nodes for documents (weekly prioritization reports) that may live outside any repository, and default confinement would have refused them; the default should probably be a per-board allow-root list that includes the folders a PM board actually reads, not the workspace alone.
119
+
120
+ The same mechanism gives the product its own remote story, without relying on anything external. A loopback-only server means Claude Code cloud, Codex cloud and the Copilot coding agent, the agents that run longest unattended, cannot reach the board at all. With per-writer tokens minted by the server (human tokens served only to the workbench, agent tokens issued per session), the server can bind beyond loopback deliberately (`--listen` plus a token), and a remote agent is just another authenticated writer. No relay, no sync service, no version-control detour.
121
+
122
+ Correction to my own review: a human's write cannot hit the 403 fence or the 409 lock, both sit behind `if (!meta.fromWorkbench)`; the silent-failure defect is real but smaller than I wrote.
123
+
124
+ ### 8. Ship a binary, one-screen README, net-negative releases (S)
125
+
126
+ `bun build --compile` works today: 0.5 s, 66 MB, `bun:sqlite` unmodified, verified by the panel. Release binaries per platform and an npm package that is an installer. The 227 tracked `dist/` files stop being committed. The README becomes the thesis, one scenario showing both directions, Quick start, `smoke`, and a two-minute GIF. For 60 days a release may ship only if it deletes more lines than it adds, or is one of the trust fixes, or is move 0. Move 3 and move 6a are exempt too, since both add before they remove.
127
+
128
+ ### 9. Render for the pane you live in (M)
129
+
130
+ The home is a 500 to 600 px panel inside Claude Code, Codex, and Copilot. Viewport culling, `content-visibility`, lazy iframe mount, and a live-frame budget. Objection: the thumbnail-eviction pipeline I wanted does not exist (no clip support, capture is macOS-only). Accepted: no thumbnails; culling, lazy mount and the frame budget are move 5's frame host doing its job.
131
+
132
+ ### 10. CLAUDE.md and AGENTS.md become one contract, the incidents become checks (S)
133
+
134
+ Both files stay, because different agents read different files (Claude Code reads CLAUDE.md; Codex, Copilot and Amp read AGENTS.md), and they stay byte-identical under the CI gate that already enforces it. Each becomes the same 120-line contract; the AX design reference moves to `docs/ax.md` and is linked from both; each "this shipped N times" paragraph becomes a test or a Biome rule where an oracle exists. Objection: the nine-time black-tile class has no executable oracle (Playwright is Chromium-only). True, and move 5 replaces the oracle ladder with a paint deadline and a visible fallback, which a test can assert; a WebKit Playwright project would cover the rest.
135
+
136
+ ### The payoff nobody asked for
137
+
138
+ With one journal, a **time scrubber** replaces snapshots, the History drawer, and diff: drag a slider and watch the agent's work unfold on the board. With one brief, an **attention heat** on the board shows the human exactly what the agent will read next, so looking and pinning become visible tuning. Those two are the demo. They are also the two-way surface at its most literal: you see what the agent sees, and you see what it did.
139
+
140
+ ## Part 3: Long term, hosted multiplayer
141
+
142
+ **Discussion extension, 2026-09-06:** [Boards, personal wiki, and workspace context](product-context-vision-2026-09.md) reviews this plan and proposes app-owned personal and shared knowledge built from board evidence. It includes oracle feedback, publication and permission boundaries, corrections to the trust/provenance assumptions below, and a small validation experiment. It is a proposal, not a replacement for the maintainer's settled decisions or authorization to start hosted work.
143
+
144
+ The maintainer's long-term direction, and the natural end of the two-way surface: the board is hosted, people log in, and several humans and their agents work on the same canvas. My position is that this is the right destination and that almost everything in Part 2 is its prerequisite, which is the strongest argument for Part 2.
145
+
146
+ **What Part 2 already buys.** The journal (move 4) is the sync model: an ordered log with sequence numbers, per-consumer cursors and inverse patches is exactly what multiple clients need, and node-granular edits with compare-and-set on the node version resolve concurrent human edits without operational transforms or CRDTs. Per-writer tokens (move 7) become per-user credentials; the listen mode becomes the hosted server; author on every node (Part 1) becomes attribution; presence is already multi-writer with identity colors; many boards (move 0) become the team's board list; the ask node with a human-only answer becomes an ask addressed to a person or a role.
147
+
148
+ **What is genuinely new.** Identity and authorization: accounts, per-board roles (read, write, answer asks), and an audit trail, which the journal already is. Hosting: the same Bun binary run with `--listen` behind TLS, one server per team first (single-tenant, one store per team), multi-tenant only if a second team ever needs the same server. Uploads instead of file nodes: a hosted server has no workspace on disk, so a file node becomes an uploaded document with its watcher replaced by re-upload or a connector. Per-person attention on a shared board: the brief an agent reads is compiled from the attention of the people in that agent's session, not from everyone's, so two people can work the same board with different agents without steering each other's context.
149
+
150
+ **The vision it enables.** A team's working memory: a quarterly OKR board with the PM, the team leads and each of their agents on it, every card attributed, every ask answered by the person it was addressed to, and the time scrubber showing how the plan changed and who changed it. That is a product nobody has, and it is the same product as Part 2 with a login screen.
151
+
152
+ **The constraints that hold.** No externals: identity is the app's own (passkeys and email, with federation optional later), and history and sync are the journal, not a third-party service. Trust stops being optional: everything in move 7 is mandatory before a single port is opened to the internet, and MCP-app hosting of third-party apps needs a per-board allowlist once viewers are not all the owner. Scope: this is 2.0 work for a solo maintainer. It should not start before 1.0, and it should not shape 0.6 to 0.8 beyond the choices above that cost nothing now.
153
+
154
+ ## What is architecturally wrong today
155
+
156
+ 1. **One write lands in four logs and none is durable.** Mutation history (200 closures, in-memory), the SSE ring (500, in-memory), presence activity (50), and three AX tables with independent retention. Undo is empty after every restart. SSE has an `id:` field nobody reads.
157
+ 2. **The human marker is a label, not a gate.** Actor attribution, the fence, and the lock all key on an unauthenticated header.
158
+ 3. **Three write paths with three policies.** The registry (fence, lock, actor), the SDK (87 direct state calls, none of those), and 21 hand-written routes in a 3,471-line server file.
159
+ 4. **Node type is not the renderer.** Five rendering tiers, three node types funnelled through one iframe component, two UI frameworks, two CSS toolchains.
160
+ 5. **Single-slot listeners and import-order wiring.** `onMutation` and the work-items listener hold one callback each; module-level timers outlive the server.
161
+ 6. **The knowledge that keeps the product working lives in prose.** Rules 3, 7, and 9 in CLAUDE.md and its byte-identical AGENTS.md twin have each grown a paragraph per recurrence.
162
+
163
+ ## What must not be touched
164
+
165
+ The operation registry and its dispatcher. State in the server, browser as renderer. The human/agent distinction as a first-class concept (to be made real, not removed). Context pins and `canvas://pinned-context`. Six of six iframes sandboxed without `allow-same-origin`. `smoke`, the e2e gate, the changelog discipline. The velocity: 110 commits, +64,444 and −13,026 lines in the last six weeks, which is what makes a deletion-heavy plan credible.
166
+
167
+ ## Sequence
168
+
169
+ | Release | Weeks | Content |
170
+ |---|---|---|
171
+ | 0.6 | 2–3 | Many boards (move 0), trust (move 7), the cheap tool surface (6a), the frame-host consolidation of move 5, the binary (8). |
172
+ | 0.7 | 2 | The foundation (Part 1): one read, session projection, author on every node. |
173
+ | 0.8 | 4–6 | The journal and bookmarks (4), the agent's output onto the board (3), the SDK through the registry. |
174
+ | 1.0 | | Generated surfaces (6b, if decided), state as a fold of the journal, time scrubber, attention heat, culling and frame budget (9). |
175
+ | 2.0 | | Hosted multiplayer (Part 3): accounts and per-board roles, listen mode behind TLS, uploads instead of file nodes, per-person attention on a shared board. |
176
+
177
+ ## Where I disagree with the panel
178
+
179
+ - The minimalist and the strategist delete AX. Wrong target, and after the maintainer's decision the target is narrower still: the agent's output moves onto the board, and every AX mechanism including the whole fleet layer stays.
180
+ - The minimalist deletes the registry. Refuted three to zero: the browser alone calls 55 API paths.
181
+ - The rendering architect deletes the tool rail and top bar. Design for 600 px first; the rail is the good part of the chrome.
182
+ - Every vision, and my own first draft, proposed cutting node types on the strength of the repo's test board. The real boards refute the chart cut outright, and the maintainer's rule closes the rest: usage on one machine is not evidence of non-use. No node type is cut; consolidation is confined to how nodes render.
183
+ - Four of six visions leaned on Obsidian, and five on git, as the format, the snapshot system, or the remote transport. The maintainer's rule is that this is its own app with no reliance on externals, and I agree: the app owns its history (the journal), its store (SQLite), and its remote reach (tokens and a listen mode). What survives of the folder idea is an export.
184
+ - Several visions invented constants (700 chars, six frames, 14k lines) with the same confidence they mocked the repo's. So did I. Every such number in this document is a starting value to tune in use, not a claim.
185
+
186
+ ## Evidence
187
+
188
+ | Claim | Source |
189
+ |---|---|
190
+ | Nine real boards Apr–Aug 2026 in the snapshot history of the board the global MCP install writes to (13 chart, 5+ HTML, 2 Excalidraw, 1 json-render, about 60 markdown real nodes); live state is fixtures; 84–89 browser steers; 273–337 snapshots on 14–16 live nodes | read-only `sqlite3` over `snapshot_nodes`, titles only, snapshot ids decoded to dates, 2026-09-05 |
191
+ | 3.1 MB of 4.2 MB is snapshots; 196 snapshots, 2,206 rows | `dbstat`, `snapshot_meta`, `snapshot_nodes` counts, 2026-09-05 |
192
+ | Rendering tier 11,298 lines, 10 of 17 deps, 5.6 MB dist | fact-finder `rendering`, `wc -l`, `package.json` |
193
+ | SDK bypass 87 direct calls vs 4 registry calls | `grep -c "canvasState\." src/server/index.ts` |
194
+ | File nodes unconfined at create | `src/server/canvas-operations.ts:760`; confinement exists only in the byte-serving route |
195
+ | Webpage fetcher follows redirects, no host filter | `src/server/webpage-node.ts:216,245-246` |
196
+ | Marker is a plain header | `src/server/operations/http.ts:91`; no auth in `server.ts` |
197
+ | SSE has no resume cursor | fact-finder `logs`; `Last-Event-ID` unused |
198
+ | `bun build --compile` works | panel feasibility refuter: 0.5 s, 66 MB, state route 200 |
199
+ | Velocity | `git log --since=2026-07-24 --shortstat`: 110 commits, +64,444 −13,026 |
200
+ | Loopback-only server | `server.ts:89` |
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.5.1",
3
+ "version": "0.6.2",
4
4
  "description": "Spatial canvas workbench for coding agents \u2014 infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
5
5
  "type": "module",
6
6
  "main": "./src/server/index.ts",
@@ -117,6 +117,6 @@
117
117
  },
118
118
  "homepage": "https://github.com/pskoett/pmx-canvas#readme",
119
119
  "engines": {
120
- "bun": ">=1.3.14"
120
+ "bun": ">=1.4.2"
121
121
  }
122
122
  }
@@ -16,6 +16,14 @@ PMX Canvas is a server-authoritative spatial workbench controlled through MCP, H
16
16
  Humans curate agent context by pinning nodes; agents read that curation through
17
17
  `canvas://pinned-context`. State survives browser refresh.
18
18
 
19
+ ## Runtime prerequisites
20
+
21
+ PMX Canvas 0.6.1 requires **Bun >=1.4.2**, including when installed through npm.
22
+ Check `bun --version` and ensure Bun is on the MCP host's PATH, not just your shell's;
23
+ use an absolute executable path if needed. Install/configure only when requested.
24
+ For pinned installation, MCP configuration, managed services, and disposable verification,
25
+ read [Installing PMX Canvas](references/installing-pmx-canvas.md).
26
+
19
27
  ## Required Operating Sequence
20
28
 
21
29
  0. **After a `pmx-canvas` upgrade, refresh this skill first.** Run
@@ -28,9 +36,10 @@ Humans curate agent context by pinning nodes; agents read that curation through
28
36
  any `--mcp` processes; Bun does not hot-reload a running process.
29
37
  1. **Open or focus the workbench before mutating.** Reuse one visible canvas surface for the
30
38
  session.
31
- 2. **Verify workspace identity.** Read `GET /health` or `pmx-canvas serve status`; the returned
32
- `workspace` must equal the intended absolute workspace root. A healthy listener on port 4313
33
- may belong to another project.
39
+ 2. **Verify workspace identity at the actual target.** Read `GET /health` at the URL your
40
+ CLI/MCP/browser is using; its `workspace` must equal the intended canonical absolute root.
41
+ `pmx-canvas serve status --port=<port>` checks the local listener on that port, not an
42
+ arbitrary `PMX_CANVAS_URL`. A healthy listener may belong to another project.
34
43
  3. **Read before write.** Search with `canvas_query { action: "search", query }` before creating
35
44
  nodes. Read the full layout only when necessary. The MCP parameter is `query` — passing the
36
45
  HTTP API's `q` is silently ignored and returns zero results.
@@ -68,38 +77,52 @@ Humans curate agent context by pinning nodes; agents read that curation through
68
77
 
69
78
  ## Workspace Safety
70
79
 
71
- Before any create, update, remove, clear, restore, or arrange:
80
+ Before any create, update, remove, clear, restore, arrange, or smoke test, choose an
81
+ absolute project root and a dedicated free port. For a local target:
72
82
 
73
83
  ```bash
74
- curl -sS http://localhost:4313/health
75
- pmx-canvas serve status
84
+ export PMX_CANVAS_WORKSPACE_ROOT=/absolute/path/to/project
85
+ export PMX_CANVAS_PORT=14313 # Example only: choose a free port for this workspace.
86
+ unset PMX_CANVAS_URL # Remove a stale URL before selecting a different local target.
87
+ curl --fail --silent --show-error "http://localhost:${PMX_CANVAS_PORT}/health"
88
+ pmx-canvas serve status --port="$PMX_CANVAS_PORT"
76
89
  ```
77
90
 
78
- Both surfaces report `workspace`. It must match the intended workspace root.
79
- For a full environment check in one command (health + workspace, CLI/server
80
- version skew, MCP initialize handshake, temp-node create/search/remove
81
- round-trip, board validation), run `pmx-canvas smoke` (0.4.6+) — JSON report,
82
- exit 1 on failure.
91
+ Compare the health field **`workspace`** (not `workspaceRoot`) to the intended root,
92
+ resolving symlinks before comparison. Stop on mismatch or failed health; do not write.
93
+ If deliberately using `PMX_CANVAS_URL` or `--server-url`, check `/health` at that exact
94
+ URL instead. Confirm the browser and MCP use the same verified target, especially
95
+ after fallback to another port. Do not print URLs containing credentials.
96
+
97
+ `pmx-canvas smoke` reports health/workspace/version information, checks MCP initialize,
98
+ creates/searches/removes a temporary node, and validates the board. **It mutates state**
99
+ and does not enforce your intended workspace identity; version skew is reported, not
100
+ necessarily a failing check. Use a disposable workspace, verify health first, and inspect
101
+ both the JSON details and exit status (1 when a check fails). It is not a read-only probe
102
+ or a guarantee of unchanged history.
83
103
 
84
104
  - If `responsive: true` but `pidRunning: false`, treat the listener as potentially stale.
85
- - On mismatch, do not mutate. Start the intended workspace on an explicit free port:
86
- `pmx-canvas serve --daemon --no-open --port=<free-port>`. (`serve --daemon` enforces this
87
- itself: pointed at a port owned by another workspace, it refuses with the owner named instead
88
- of reporting "already running".)
89
- - Target that port and re-check `/health`.
90
- - `PMX_CANVAS_PORT` is the agent CLI target; the server's startup port is controlled by `--port`
91
- or `PMX_WEB_CANVAS_PORT`.
92
- - **MCP transport workspace resolution.** An MCP server (`pmx-canvas --mcp`) holds its own in-memory
93
- canvas. To avoid the old "wrong-workspace split" (a `--mcp` launched from an incidental dir, e.g.
94
- `~/.copilot`, silently binding a fallback port the panel never renders): when the preferred port is
95
- held by a healthy daemon serving a *different* workspace, the MCP server now **attaches** to it
96
- (inherits its workspace) so writes are visible where the panel renders; and if it launched from an
97
- incidental host/agent config dir on a *free* port, it still binds but emits a loud stderr warning
98
- instead of silently adopting that cwd. To pin the intended workspace deterministically set
99
- **`PMX_CANVAS_WORKSPACE_ROOT=<abs project root>`** (recommended for host adapters); for a genuinely
100
- separate canvas set `PMX_CANVAS_ALLOW_WORKSPACE_SPLIT=1` or a distinct `PMX_CANVAS_PORT`. The CLI's
101
- query/mutation commands are a thin HTTP client and never start a server of their own (only `serve` /
102
- `--mcp` spawn a process).
105
+ - On mismatch, leave the other workspace's server alone. Start the intended workspace on
106
+ a different free port, target it explicitly, and re-check `/health` before writing.
107
+ - On an ordinary local machine, `pmx-canvas serve --daemon --no-open --port=<free-port>`
108
+ refuses a different-workspace occupant. In a managed environment, supervise foreground
109
+ `pmx-canvas serve --no-open --port=<free-port>` with the host's service manager instead.
110
+ - `PMX_CANVAS_PORT` selects the CLI/MCP port. CLI target flags override environment values
111
+ (`--server-url` wins over `--port`); otherwise `PMX_CANVAS_URL` wins over `PMX_CANVAS_PORT`.
112
+ For server startup, explicit `--port` wins, then `PMX_WEB_CANVAS_PORT`, then
113
+ `PMX_CANVAS_PORT`; in Amp orbs `PORT` is a later fallback. URL targeting does not select
114
+ the server's bind port.
115
+ - **MCP transport workspace resolution.** Set **`PMX_CANVAS_WORKSPACE_ROOT` plus a dedicated
116
+ `PMX_CANVAS_PORT`** in the host configuration. Without an explicit root, MCP may inherit a
117
+ different workspace from the preferred-port daemon; `PMX_CANVAS_ALLOW_WORKSPACE_SPLIT=1`
118
+ opts out of that heuristic. Current source honors an explicit root, but may bind a fallback
119
+ port if the preferred port is occupied. Root alone therefore does not guarantee CLI, MCP,
120
+ and browser target alignment; older installed runtimes can also differ. Inspect startup
121
+ output and the actual health endpoint, never assume the preferred port was bound.
122
+ Clear stale inherited `PMX_CANVAS_URL` values in the host configuration as well as the shell.
123
+ - CLI query/mutation commands are HTTP clients and do not start a server. `serve` or `--mcp`
124
+ starts or attaches to one. Use the host's authenticated preview mechanism for remote viewing;
125
+ a loopback URL is only reachable inside the machine/orb running the service.
103
126
 
104
127
  ## Choose the Smallest Useful Node Type
105
128
 
@@ -132,9 +155,9 @@ json-render, a graph, or an HTML primitive is sufficient.
132
155
 
133
156
  ## Current MCP Composites
134
157
 
135
- The live MCP surface is **22 tools**: the 16 composites below plus 6 standalones (v0.4.x —
136
- this table is kept in sync with the server's composite registry; `tools/list` on a fresh
137
- `pmx-canvas --mcp` is always authoritative).
158
+ The live MCP surface is **22 tools**: the 16 composites below plus 6 standalones. This table is
159
+ kept in sync with the server's composite registry; `tools/list` on a fresh `pmx-canvas --mcp` is
160
+ always authoritative, independent of the installed release version.
138
161
 
139
162
  | Composite | Actions |
140
163
  |-----------|---------|
@@ -212,8 +235,10 @@ distant nodes, an auto-placed node lands off-camera.
212
235
  markdown 640×420, status 360×200, file 520×360, diff 640×420, mermaid 640×460, html 720×640,
213
236
  graph 760×520, mcp-app 960×600, web-artifact 960×720. A *hosted* app opened with
214
237
  `canvas_app { action: "open-mcp-app" | "diagram" }` — including the Excalidraw diagram preset — is
215
- the exception: it opens at 720×500, not 960×600 (960×600 is the default for an `mcp-app` node you
216
- create directly). Pass `width`/`height` to that action when you want a bigger diagram tile.
238
+ the exception: request `width: 720, height: 500` for its standard landscape preset instead of
239
+ relying on the 960×600 default for a directly created `mcp-app` node. After the app mounts, read
240
+ the node back with `canvas_query { action: "layout" }` and use its persisted width and height for
241
+ subsequent placement or layout; the mounted app may have changed the requested dimensions.
217
242
  Since 0.4.6 the server clamps explicit creation sizes UP to per-type
218
243
  readability floors (e.g. markdown 360×180, graph/json-render/html 420×280, mcp-app 480×320) —
219
244
  a tiny probe size silently becomes the floor. `strictSize: true` is the only opt-out (a fixed
@@ -310,11 +335,12 @@ Use the visible workbench when the human is actively curating layout:
310
335
  - The selection bar (floating bottom-center) offers count, align left/top, distribute,
311
336
  auto-arrange, Group (G), Connect, Pin as context, delete and clear; selected nodes show an
312
337
  accent outline with corner handles.
313
- - Right-click a node for context pinning, position locking, focus, collapse, connect, refresh,
338
+ - Double-left-click a node for context pinning, position locking, focus, collapse, connect, refresh,
314
339
  open, close, and type-specific actions.
340
+ - Double-left-click empty canvas for the node-creation menu; hold and drag the right mouse button to pan.
315
341
  - Drop files or URLs to create matching nodes; an empty board shows starter actions (new note,
316
342
  pick files, paste a link, start an agent session).
317
- - Double-click markdown to edit inline.
343
+ - Use a markdown node's Edit button to edit its content.
318
344
  - **Groups** are frames with the name pill and an action cluster (auto-arrange children, collapse,
319
345
  ⋯ rename / ungroup / pin all) on the top edge. Membership changes only on release while the
320
346
  "release to add to <group>" pill shows (Esc keeps it out); dragging a child fully out offers
@@ -274,7 +274,7 @@
274
274
  "id": 11,
275
275
  "name": "context-pin-workflow",
276
276
  "prompt": "Pin the auth design node so I (the agent) keep it in context, then confirm it's pinned.",
277
- "expected_output": "Pins via a verified path (canvas_pin_nodes, the CLI pin, or browser/right-click 'Pin as context'), then reads canvas://pinned-context to confirm; does not claim a broken UI action succeeded.",
277
+ "expected_output": "Pins via a verified path (canvas_pin_nodes, the CLI pin, or browser/double-left-click menu 'Pin as context'), then reads canvas://pinned-context to confirm; does not claim a broken UI action succeeded.",
278
278
  "assertions": [
279
279
  {
280
280
  "name": "pins-via-verified-path",
@@ -3,6 +3,11 @@
3
3
  Use this guide when creating diagrams through PMX Canvas with `canvas_app { action: "diagram" }` or
4
4
  `pmx-canvas external-app add --kind excalidraw`.
5
5
 
6
+ Request `width: 720, height: 500` when opening the standard landscape diagram tile. Treat those as
7
+ requested preset dimensions, not authoritative geometry: after the app mounts, query
8
+ `canvas_query { action: "layout" }` and use the node's persisted width and height for placement,
9
+ overlap checks, and any later layout. A hosted app can resize during mount.
10
+
6
11
  ## Why Text Can Still Drift
7
12
 
8
13
  PMX normalizes canonical Excalidraw bound text (`containerId` / `boundElements`) into the hosted
@@ -82,7 +82,7 @@ section below.
82
82
  6. **Arrange + validate** — `canvas_view { action: "arrange" }` after batch adds, then
83
83
  `canvas_query { action: "validate" }` to catch collisions / dangling edges before finishing.
84
84
  7. **Pin through a verified path** — `canvas_pin_nodes` (or the browser "Pin as context" /
85
- right-click "Pin as context"), then read `canvas://pinned-context` to confirm.
85
+ double-click menu's "Pin as context"), then read `canvas://pinned-context` to confirm.
86
86
  8. **Clean up** — remove retry/test fixtures (`canvas_node { action: "remove" }` — works for
87
87
  every type, including `status`), and restore the baseline snapshot.
88
88
 
@@ -192,16 +192,17 @@ running". `serve stop` reads the pid file, verifies the pid still looks like a c
192
192
  The browser is not just a passive view. Human interactions on the canvas persist back to the
193
193
  server and become part of the authoritative canvas state.
194
194
 
195
- - Double-click empty canvas — create a markdown note at that position
195
+ - Double-left-click empty canvas — open the node-creation menu at that position
196
+ - Right-button hold and drag — pan the board, including over node content
196
197
  - Shift+drag on empty canvas — lasso-select multiple nodes
197
198
  - Selection bar actions — when nodes are selected, the browser exposes `Pin as context`,
198
199
  `Group`, `Connect`, and `Clear`
199
- - Right-click a node — open the node context menu. **`Pin as context`** adds the node to the
200
+ - Double-left-click a node — open the node context menu. **`Pin as context`** adds the node to the
200
201
  human-curated agent context set (updates the context count + the node's context-pin indicator);
201
202
  `Lock position` is the separate arrange-lock. Plus focus, collapse, connect, refresh/open, close,
202
203
  and type-specific actions. (Every node type, **including `status`, has a remove `×`** in its title
203
204
  bar and a `Close` menu item.)
204
- - Right-click a group node — recolor the group using preset swatches or a custom color picker,
205
+ - Double-left-click a group node — recolor the group using preset swatches or a custom color picker,
205
206
  or ungroup it (dissolves the frame; children stay)
206
207
  - Drag-and-drop files or URLs — add file, image, markdown, or webpage nodes directly
207
208
  - Paste URLs — create webpage nodes from the clipboard