pmx-canvas 0.11.0 → 0.12.1

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 (300) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +4 -1
  2. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  3. package/.github/extensions/pmx-canvas/tools.json +440 -18
  4. package/CHANGELOG.md +51 -0
  5. package/dist/amp-plugin/pmx-canvas/LICENSE +21 -0
  6. package/dist/amp-plugin/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  7. package/dist/amp-plugin/pmx-canvas/index.js +5445 -0
  8. package/dist/amp-plugin/pmx-canvas/runtime.json +3 -0
  9. package/dist/amp-plugin/pmx-canvas/shared-runtime.mjs +194 -0
  10. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/SKILL.md +267 -0
  11. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/evals.json +1168 -0
  12. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/jwt.ts +17 -0
  13. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/login.ts +12 -0
  14. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/middleware.ts +13 -0
  15. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/routes/auth.ts +13 -0
  16. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/investigation-board/src/handlers/users.ts +27 -0
  17. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/mcp.json +36 -0
  18. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/ax-html-control-surface.md +101 -0
  19. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/codex-app-adapter.md +151 -0
  20. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +150 -0
  21. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/full-reference.md +1839 -0
  22. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/github-copilot-app-adapter.md +223 -0
  23. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/html-primitives.md +139 -0
  24. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/importing-context.md +132 -0
  25. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/installing-pmx-canvas.md +301 -0
  26. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/tours-and-recording.md +117 -0
  27. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/workbench-workflows.md +908 -0
  28. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-orchestration/SKILL.md +141 -0
  29. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-testing/SKILL.md +266 -0
  30. package/dist/amp-plugin/pmx-canvas/start-mcp.mjs +14857 -0
  31. package/dist/amp-plugin/pmx-canvas/tools.json +10963 -0
  32. package/dist/canvas/chunks/{abnfDiagram-N423BO3Z-p8dtp6va.js → abnfDiagram-N423BO3Z-cwtsds5d.js} +1 -1
  33. package/dist/canvas/chunks/architecture-TIHT7OUA-x63cwejs.js +1 -0
  34. package/dist/canvas/chunks/{architectureDiagram-T3A2C74G-5jhmxrzf.js → architectureDiagram-T3A2C74G-b4xc9tpz.js} +3 -3
  35. package/dist/canvas/chunks/{blockDiagram-VBNYF7ZC-pw1fa9yb.js → blockDiagram-VBNYF7ZC-ahp3yxxs.js} +7 -7
  36. package/dist/canvas/chunks/{c4Diagram-5PPSVZJV-x07nq0tt.js → c4Diagram-5PPSVZJV-nappnay7.js} +4 -4
  37. package/dist/canvas/chunks/classDiagram-JCYQIIEL-k1j2xc1t.js +1 -0
  38. package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-33x08xf2.js +1 -0
  39. package/dist/canvas/chunks/{cose-bilkent-JH36ORCC-nm39npq0.js → cose-bilkent-JH36ORCC-a428syda.js} +1 -1
  40. package/dist/canvas/chunks/cynefin-VYW2F7L2-8v6jxfz8.js +1 -0
  41. package/dist/canvas/chunks/{cynefinDiagram-MW4NZA55-vdxrwahz.js → cynefinDiagram-MW4NZA55-nspsqebj.js} +1 -1
  42. package/dist/canvas/chunks/{dagre-VZM6K2ZE-bqdm7wpd.js → dagre-VZM6K2ZE-8y6177fh.js} +2 -2
  43. package/dist/canvas/chunks/{diagram-7IWD3JNH-gs5gpr93.js → diagram-7IWD3JNH-vym028wn.js} +4 -4
  44. package/dist/canvas/chunks/{diagram-B4RE2ZJO-cgwjg47s.js → diagram-B4RE2ZJO-jddqb8sy.js} +1 -1
  45. package/dist/canvas/chunks/{diagram-LBJQPF4R-w3vahpav.js → diagram-LBJQPF4R-bjwbpkrn.js} +1 -1
  46. package/dist/canvas/chunks/{diagram-Q27KOJAE-42837z6h.js → diagram-Q27KOJAE-j6ph6hcx.js} +2 -2
  47. package/dist/canvas/chunks/{diagram-UB23O5K3-4fz83q59.js → diagram-UB23O5K3-p09w8jej.js} +1 -1
  48. package/dist/canvas/chunks/{ebnfDiagram-BXEA7PRR-zphtgegj.js → ebnfDiagram-BXEA7PRR-j975ycq5.js} +1 -1
  49. package/dist/canvas/chunks/{erDiagram-JOGREHBK-zvbbdxp4.js → erDiagram-JOGREHBK-sxp6hjn8.js} +5 -5
  50. package/dist/canvas/chunks/eventmodeling-45OFAUF4-xzfgn6xg.js +1 -0
  51. package/dist/canvas/chunks/flowDiagram-UKHOOZJN-2nsmkskb.js +1 -0
  52. package/dist/canvas/chunks/{ganttDiagram-PKOTCBZU-282jzycm.js → ganttDiagram-PKOTCBZU-4tv230y2.js} +2 -2
  53. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-macbhand.js +1 -0
  54. package/dist/canvas/chunks/{gitGraphDiagram-DS77QQ5N-vqqy6p1b.js → gitGraphDiagram-DS77QQ5N-mqgmnw9g.js} +1 -1
  55. package/dist/canvas/chunks/{index-wgv7en21.js → index-037z5dz2.js} +2 -2
  56. package/dist/canvas/chunks/{index-w2mra17t.js → index-081n3har.js} +1 -1
  57. package/dist/canvas/chunks/{index-353m7b0p.js → index-5n03dmb9.js} +1 -1
  58. package/dist/canvas/chunks/{index-yw8h9vha.js → index-60pgzdqe.js} +1 -1
  59. package/dist/canvas/chunks/index-8g1vb57n.js +2 -0
  60. package/dist/canvas/chunks/{index-vv56h019.js → index-8rjes9r6.js} +1 -1
  61. package/dist/canvas/chunks/{index-nprm96xp.js → index-9022d1n9.js} +1 -1
  62. package/dist/canvas/chunks/{index-194m3hc3.js → index-9b02042t.js} +1 -1
  63. package/dist/canvas/chunks/{index-0rk4gp1g.js → index-ap4q4vez.js} +1 -1
  64. package/dist/canvas/chunks/{index-rh7nv68n.js → index-dyp2nhd3.js} +1 -1
  65. package/dist/canvas/chunks/index-frvta4tm.js +2 -0
  66. package/dist/canvas/chunks/{index-s4j678gj.js → index-h9ycj8n2.js} +1 -1
  67. package/dist/canvas/chunks/{index-fjtdggjn.js → index-mergmj97.js} +2 -2
  68. package/dist/canvas/chunks/index-nwsya6dj.js +301 -0
  69. package/dist/canvas/chunks/{index-3cy1bzwh.js → index-nz8qbd1p.js} +1 -1
  70. package/dist/canvas/chunks/{index-d3b3gqe9.js → index-qhfrazv3.js} +1 -1
  71. package/dist/canvas/chunks/{index-6qmsnxwy.js → index-v119nznk.js} +4 -4
  72. package/dist/canvas/chunks/{index-r2fyc9w1.js → index-vpt37m3d.js} +6 -6
  73. package/dist/canvas/chunks/{index-qj8fxwsf.js → index-y9ma6azy.js} +1 -1
  74. package/dist/canvas/chunks/{index-tb1wf525.js → index-zx0j3a2z.js} +26 -26
  75. package/dist/canvas/chunks/info-DKCQHKI2-a0ykzs9z.js +1 -0
  76. package/dist/canvas/chunks/{infoDiagram-6WML65LV-v1yvggqj.js → infoDiagram-6WML65LV-qv97qq6a.js} +2 -2
  77. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-p9306gh0.js +70 -0
  78. package/dist/canvas/chunks/{journeyDiagram-NVQOT4AX-zjcffveh.js → journeyDiagram-NVQOT4AX-sv67zats.js} +5 -5
  79. package/dist/canvas/chunks/{kanban-definition-27J2QSJJ-dakqtfp1.js → kanban-definition-27J2QSJJ-k2pfam6p.js} +1 -1
  80. package/dist/canvas/chunks/{mermaid.core-j2geswrd.js → mermaid.core-83r0fdce.js} +4 -4
  81. package/dist/canvas/chunks/{mindmap-definition-FAOFIHXS-142xhr3k.js → mindmap-definition-FAOFIHXS-vg2newm1.js} +1 -1
  82. package/dist/canvas/chunks/packet-7NZHBO7P-tqnabcdv.js +1 -0
  83. package/dist/canvas/chunks/{pegDiagram-VL7TDLO6-5wvk7vsd.js → pegDiagram-VL7TDLO6-k2h7zc5n.js} +1 -1
  84. package/dist/canvas/chunks/pie-RZYD4A2V-wke6gyhe.js +1 -0
  85. package/dist/canvas/chunks/{pieDiagram-7S7Q4E2Y-ztdhwycc.js → pieDiagram-7S7Q4E2Y-hg169p1v.js} +2 -2
  86. package/dist/canvas/chunks/{quadrantDiagram-CIZ2JOQS-c5p9gmg6.js → quadrantDiagram-CIZ2JOQS-bxtvsq0b.js} +5 -5
  87. package/dist/canvas/chunks/radar-I7S5WNFK-w7ka0drw.js +1 -0
  88. package/dist/canvas/chunks/railroad-3IZDKUUU-y3vg318x.js +1 -0
  89. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-6hqvcz3c.js +1 -0
  90. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-vcae260m.js +1 -0
  91. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-0dbyme23.js +1 -0
  92. package/dist/canvas/chunks/{railroadDiagram-AXF67PYL-xkmqkjwf.js → railroadDiagram-AXF67PYL-njyhcp4p.js} +1 -1
  93. package/dist/canvas/chunks/{requirementDiagram-LRYGKXZP-9wm2dyaw.js → requirementDiagram-LRYGKXZP-2z6v09gm.js} +4 -4
  94. package/dist/canvas/chunks/{sankeyDiagram-W5VNT64P-2w19etgb.js → sankeyDiagram-W5VNT64P-15mzam0e.js} +1 -1
  95. package/dist/canvas/chunks/{sequenceDiagram-SI44F4Z6-0jn0vwmz.js → sequenceDiagram-SI44F4Z6-rbshyd2c.js} +12 -12
  96. package/dist/canvas/chunks/{sizeCapture-X5ZJPWSS-x75epwdq.js → sizeCapture-X5ZJPWSS-sg26xwts.js} +1 -1
  97. package/dist/canvas/chunks/{stateDiagram-OKZ733FA-schjpy6d.js → stateDiagram-OKZ733FA-6eahp98e.js} +1 -1
  98. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-tty0ggn5.js +1 -0
  99. package/dist/canvas/chunks/{swimlanes-SLNWSIFB-gkw4xce3.js → swimlanes-SLNWSIFB-ze5db10t.js} +1 -1
  100. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-thze75gq.js +8 -0
  101. package/dist/canvas/chunks/{timeline-definition-Z64GVDOM-x7nkcy01.js → timeline-definition-Z64GVDOM-vnp7jyy8.js} +3 -3
  102. package/dist/canvas/chunks/treeView-QDETBFTQ-6ysffnm6.js +1 -0
  103. package/dist/canvas/chunks/treemap-6X3UGDF4-m3h9p066.js +1 -0
  104. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-0qrnb7mj.js +34 -0
  105. package/dist/canvas/chunks/wardley-OPB4EBWU-dp37drwx.js +1 -0
  106. package/dist/canvas/chunks/{wardleyDiagram-T6FBY63Y-t1j52t9r.js → wardleyDiagram-T6FBY63Y-8nxg8wqt.js} +4 -4
  107. package/dist/canvas/chunks/{xychartDiagram-ELKLHX3M-38t16pme.js → xychartDiagram-ELKLHX3M-nj921e7s.js} +5 -5
  108. package/dist/canvas/global.css +94 -1
  109. package/dist/canvas/index.js +154 -145
  110. package/dist/canvas/mcp-app.html +600 -498
  111. package/dist/json-render/index.css +1 -1
  112. package/dist/types/client/canvas/BoardMoveDialog.d.ts +2 -0
  113. package/dist/types/client/canvas/EdgeEditor.d.ts +19 -0
  114. package/dist/types/client/canvas/GroupNavigation.d.ts +2 -0
  115. package/dist/types/client/canvas/TextPrompt.d.ts +4 -0
  116. package/dist/types/client/nodes/PromptNode.d.ts +1 -0
  117. package/dist/types/client/nodes/ViewerFrame.d.ts +1 -1
  118. package/dist/types/client/nodes/WorkCard.d.ts +7 -0
  119. package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +1 -0
  120. package/dist/types/client/state/canvas-store.d.ts +15 -0
  121. package/dist/types/client/state/human-store.d.ts +2 -0
  122. package/dist/types/client/state/intent-bridge.d.ts +4 -2
  123. package/dist/types/client/state/relation-store.d.ts +11 -0
  124. package/dist/types/client/types.d.ts +1 -1
  125. package/dist/types/json-render/catalog.d.ts +2 -2
  126. package/dist/types/server/agent-presence.d.ts +1 -0
  127. package/dist/types/server/ax-interaction.d.ts +1 -0
  128. package/dist/types/server/ax-state-manager.d.ts +10 -2
  129. package/dist/types/server/board-local.d.ts +4 -0
  130. package/dist/types/server/board-move.d.ts +38 -0
  131. package/dist/types/server/canvas-db.d.ts +12 -2
  132. package/dist/types/server/canvas-operations.d.ts +5 -6
  133. package/dist/types/server/canvas-provenance.d.ts +1 -1
  134. package/dist/types/server/canvas-serialization.d.ts +1 -0
  135. package/dist/types/server/canvas-state.d.ts +84 -20
  136. package/dist/types/server/card-relocation.d.ts +6 -0
  137. package/dist/types/server/context-reads.d.ts +3 -1
  138. package/dist/types/server/human-presence.d.ts +1 -1
  139. package/dist/types/server/index.d.ts +16 -5
  140. package/dist/types/server/intent-registry.d.ts +8 -4
  141. package/dist/types/server/knowledge-lifecycle.d.ts +4 -2
  142. package/dist/types/server/mutation-history.d.ts +7 -5
  143. package/dist/types/server/operations/registry.d.ts +2 -0
  144. package/dist/types/server/trace-manager.d.ts +1 -0
  145. package/dist/types/shared/ax-kinds.d.ts +6 -0
  146. package/dist/types/shared/ax-surface-protocol.d.ts +8 -0
  147. package/dist/types/shared/human-presence.d.ts +1 -0
  148. package/dist/types/shared/lifecycle-status.d.ts +2 -0
  149. package/docs/RELEASE.md +21 -14
  150. package/docs/agent-plugin.md +22 -2
  151. package/docs/amp-plugin.md +270 -0
  152. package/docs/cli.md +42 -3
  153. package/docs/design/GuestName.dc.html +50 -0
  154. package/docs/design/PluginLogo.svg +7 -0
  155. package/docs/design/WorkCards.dc.html +39 -0
  156. package/docs/design/canvas.json +16 -0
  157. package/docs/design/plugin-logo.png +0 -0
  158. package/docs/design.md +111 -8
  159. package/docs/evals/agents-md-sections.md +38 -0
  160. package/docs/http-api.md +128 -10
  161. package/docs/mcp.md +61 -9
  162. package/docs/node-types.md +71 -1
  163. package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
  164. package/docs/plans/plan-019-post-0.9-roadmap.md +44 -20
  165. package/docs/plans/plan-020-wave-5-work-planning.md +553 -0
  166. package/docs/product-vision-2026-09.md +33 -17
  167. package/docs/sdk.md +18 -0
  168. package/package.json +6 -3
  169. package/plugins/pmx-canvas/assets/preview.png +0 -0
  170. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +4 -1
  171. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  172. package/plugins/pmx-canvas/plugin.json +2 -2
  173. package/plugins/pmx-canvas/runtime.json +1 -1
  174. package/plugins/pmx-canvas/tools.json +440 -18
  175. package/skills/data-analysis/SKILL.md +13 -26
  176. package/skills/frontend-design/LICENSE.txt +177 -0
  177. package/skills/frontend-design/SKILL.md +3 -0
  178. package/skills/pmx-canvas/SKILL.md +248 -842
  179. package/skills/pmx-canvas/evals/evals.json +110 -5
  180. package/skills/pmx-canvas/references/full-reference.md +142 -28
  181. package/skills/pmx-canvas/references/html-primitives.md +7 -0
  182. package/skills/pmx-canvas/references/installing-pmx-canvas.md +61 -11
  183. package/skills/pmx-canvas/references/tours-and-recording.md +39 -6
  184. package/skills/pmx-canvas/references/workbench-workflows.md +908 -0
  185. package/skills/pmx-canvas-testing/SKILL.md +7 -5
  186. package/skills/published-consumer-e2e/SKILL.md +25 -8
  187. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +51 -75
  188. package/skills/web-artifacts-builder/LICENSE.txt +202 -0
  189. package/skills/web-artifacts-builder/SKILL.md +6 -0
  190. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +2 -2
  191. package/skills/web-artifacts-builder/scripts/init-artifact.sh +8 -8
  192. package/src/cli/commands/apps.ts +1 -0
  193. package/src/cli/commands/boards.ts +30 -0
  194. package/src/client/App.tsx +20 -4
  195. package/src/client/canvas/AttentionHistory.tsx +10 -2
  196. package/src/client/canvas/BoardMoveDialog.tsx +225 -0
  197. package/src/client/canvas/CanvasNode.tsx +45 -2
  198. package/src/client/canvas/CanvasViewport.tsx +38 -9
  199. package/src/client/canvas/ContextMenu.tsx +34 -12
  200. package/src/client/canvas/EdgeEditor.tsx +149 -0
  201. package/src/client/canvas/EdgeLayer.tsx +87 -16
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +4 -0
  203. package/src/client/canvas/GroupNavigation.tsx +85 -0
  204. package/src/client/canvas/RelationsPanel.tsx +9 -0
  205. package/src/client/canvas/SelectionBar.tsx +21 -0
  206. package/src/client/canvas/SessionPanel.tsx +35 -1
  207. package/src/client/canvas/TextPrompt.tsx +18 -1
  208. package/src/client/canvas/TopBar.tsx +30 -0
  209. package/src/client/canvas/canvas-area.ts +1 -1
  210. package/src/client/nodes/ExtAppFrame.tsx +26 -10
  211. package/src/client/nodes/MarkdownNode.tsx +6 -7
  212. package/src/client/nodes/PromptNode.tsx +32 -3
  213. package/src/client/nodes/StatusNode.tsx +12 -0
  214. package/src/client/nodes/StatusSummary.tsx +11 -0
  215. package/src/client/nodes/ViewerFrame.tsx +21 -10
  216. package/src/client/nodes/WorkCard.tsx +175 -0
  217. package/src/client/nodes/use-ax-surface-bridge.ts +18 -14
  218. package/src/client/state/attention-bridge.ts +5 -0
  219. package/src/client/state/attention-store.ts +1 -0
  220. package/src/client/state/canvas-store.ts +185 -15
  221. package/src/client/state/human-store.ts +22 -2
  222. package/src/client/state/intent-bridge.ts +19 -5
  223. package/src/client/state/relation-store.ts +127 -1
  224. package/src/client/theme/global.css +94 -1
  225. package/src/client/types.ts +1 -0
  226. package/src/mcp/server.ts +13 -3
  227. package/src/server/agent-presence.ts +43 -23
  228. package/src/server/ax-flow-loop.ts +6 -0
  229. package/src/server/ax-gate-ttl.ts +8 -1
  230. package/src/server/ax-interaction.ts +16 -10
  231. package/src/server/ax-state-manager.ts +17 -4
  232. package/src/server/ax-wait.ts +20 -6
  233. package/src/server/board-export.ts +19 -6
  234. package/src/server/board-local.ts +20 -0
  235. package/src/server/board-move.ts +297 -0
  236. package/src/server/canvas-db.ts +262 -160
  237. package/src/server/canvas-operations.ts +139 -109
  238. package/src/server/canvas-schema.ts +21 -0
  239. package/src/server/canvas-serialization.ts +2 -0
  240. package/src/server/canvas-state.ts +792 -159
  241. package/src/server/card-relocation.ts +53 -0
  242. package/src/server/code-graph.ts +4 -3
  243. package/src/server/context-reads.ts +83 -55
  244. package/src/server/demo-state.json +35 -15
  245. package/src/server/file-watcher.ts +30 -10
  246. package/src/server/human-presence.ts +18 -5
  247. package/src/server/index.ts +35 -15
  248. package/src/server/intent-registry.ts +43 -5
  249. package/src/server/knowledge-lifecycle.ts +32 -9
  250. package/src/server/lifecycle-status.ts +29 -1
  251. package/src/server/mcp-app-runtime.ts +26 -7
  252. package/src/server/mutation-history.ts +37 -8
  253. package/src/server/operations/composites.ts +6 -3
  254. package/src/server/operations/mcp-contracts.ts +15 -0
  255. package/src/server/operations/mcp.ts +36 -9
  256. package/src/server/operations/ops/annotation.ts +22 -1
  257. package/src/server/operations/ops/ax-await.ts +1 -0
  258. package/src/server/operations/ops/ax-timeline.ts +28 -2
  259. package/src/server/operations/ops/batch.ts +25 -11
  260. package/src/server/operations/ops/boards.ts +44 -3
  261. package/src/server/operations/ops/nodes.ts +70 -0
  262. package/src/server/operations/ops/query.ts +32 -3
  263. package/src/server/operations/ops/snapshots.ts +1 -0
  264. package/src/server/operations/registry.ts +49 -4
  265. package/src/server/server.ts +62 -16
  266. package/src/server/tour-control.ts +37 -12
  267. package/src/server/trace-manager.ts +4 -0
  268. package/src/server/web-artifacts/scripts/bundle-artifact.sh +2 -2
  269. package/src/server/web-artifacts/scripts/init-artifact.sh +8 -8
  270. package/src/shared/agent-context.ts +7 -1
  271. package/src/shared/auto-arrange.ts +5 -4
  272. package/src/shared/ax-kinds.ts +13 -0
  273. package/src/shared/ax-surface-protocol.ts +22 -0
  274. package/src/shared/human-presence.ts +1 -0
  275. package/src/shared/lifecycle-status.ts +2 -0
  276. package/dist/canvas/chunks/architecture-TIHT7OUA-9tffvpsv.js +0 -1
  277. package/dist/canvas/chunks/classDiagram-JCYQIIEL-nc6g5x1r.js +0 -1
  278. package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-0qnbpwvh.js +0 -1
  279. package/dist/canvas/chunks/cynefin-VYW2F7L2-ys8p46ts.js +0 -1
  280. package/dist/canvas/chunks/eventmodeling-45OFAUF4-v9q6dn4c.js +0 -1
  281. package/dist/canvas/chunks/flowDiagram-UKHOOZJN-yrr5z8tc.js +0 -1
  282. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-kdwb8m0a.js +0 -1
  283. package/dist/canvas/chunks/index-1r72hdzb.js +0 -301
  284. package/dist/canvas/chunks/index-g3pwnqeb.js +0 -2
  285. package/dist/canvas/chunks/index-rft2hcz5.js +0 -2
  286. package/dist/canvas/chunks/info-DKCQHKI2-wpzs6sz3.js +0 -1
  287. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-5hnq1a9p.js +0 -70
  288. package/dist/canvas/chunks/packet-7NZHBO7P-xn439f0s.js +0 -1
  289. package/dist/canvas/chunks/pie-RZYD4A2V-0kf16rwh.js +0 -1
  290. package/dist/canvas/chunks/radar-I7S5WNFK-0g2pz36t.js +0 -1
  291. package/dist/canvas/chunks/railroad-3IZDKUUU-fcaran6j.js +0 -1
  292. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-wgdya41q.js +0 -1
  293. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-gy07a4ka.js +0 -1
  294. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-ewk6wtkh.js +0 -1
  295. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-k8sfp2e7.js +0 -1
  296. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-nqysw67j.js +0 -8
  297. package/dist/canvas/chunks/treeView-QDETBFTQ-cesw6gp0.js +0 -1
  298. package/dist/canvas/chunks/treemap-6X3UGDF4-zdv4knp9.js +0 -1
  299. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-x8dqn9yf.js +0 -34
  300. package/dist/canvas/chunks/wardley-OPB4EBWU-44pdddez.js +0 -1
@@ -1,4 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import type { BoardMoveInput, BoardMovePreview, BoardMoveResult } from './board-move.js';
3
+ export type { BoardMoveInput, BoardMovePreview, BoardMoveResult } from './board-move.js';
2
4
  import type { Camera, Tour, TourPresenter, TourStop } from '../shared/tour.js';
3
5
  import {
4
6
  currentTour,
@@ -508,21 +510,19 @@ export class PmxCanvas extends EventEmitter {
508
510
  return (await executeOperation('edge.update', { id, ...patch }, { source: 'sdk' })) as CanvasEdge;
509
511
  }
510
512
 
511
- addAnnotation(input: Omit<CanvasAnnotation, 'id' | 'createdAt'> & { id?: string; createdAt?: string }): string {
512
- const id = input.id ?? `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
513
- canvasState.addAnnotation({
514
- ...input,
515
- id,
516
- createdAt: input.createdAt ?? new Date().toISOString(),
517
- });
518
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
519
- return id;
513
+ async addAnnotation(
514
+ input: Pick<CanvasAnnotation, 'points'> &
515
+ Partial<Pick<CanvasAnnotation, 'id' | 'type' | 'color' | 'width' | 'text' | 'label'>>,
516
+ ): Promise<string> {
517
+ const result = (await executeOperation('annotation.add', input, { source: 'sdk' })) as {
518
+ annotation: { id: string };
519
+ };
520
+ return result.annotation.id;
520
521
  }
521
522
 
522
- removeAnnotation(id: string): boolean {
523
- const removed = canvasState.removeAnnotation(id);
524
- if (removed) emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
525
- return removed;
523
+ async removeAnnotation(id: string): Promise<boolean> {
524
+ const result = (await executeOperation('annotation.remove', { id }, { source: 'sdk' })) as { ok: boolean };
525
+ return result.ok;
526
526
  }
527
527
 
528
528
  removeEdge(id: string): void {
@@ -764,7 +764,12 @@ export class PmxCanvas extends EventEmitter {
764
764
  }
765
765
 
766
766
  /** Undelivered steering for a consumer (loop-safe; excludes consumer-originated). */
767
- getPendingSteering(options?: { consumer?: string; limit?: number }): PmxAxSteeringMessage[] {
767
+ getPendingSteering(options?: {
768
+ consumer?: string;
769
+ limit?: number;
770
+ addressedOnly?: boolean;
771
+ offset?: number;
772
+ }): PmxAxSteeringMessage[] {
768
773
  return canvasState.getPendingSteering(options ?? {});
769
774
  }
770
775
 
@@ -954,6 +959,15 @@ export class PmxCanvas extends EventEmitter {
954
959
  return (await executeOperation('board.create-from', input, { source: 'sdk' })) as Record<string, unknown>;
955
960
  }
956
961
 
962
+ /** Relocate identities atomically; preview is read-only and retries reuse the original receipt. */
963
+ async moveSelectionToNewBoard(
964
+ input: BoardMoveInput,
965
+ ): Promise<{ ok: true; preview: BoardMovePreview } | (BoardMoveResult & { ok: true; replayed: boolean })> {
966
+ return (await executeOperation('board.move-selection', input, { source: 'sdk' })) as
967
+ | { ok: true; preview: BoardMovePreview }
968
+ | (BoardMoveResult & { ok: true; replayed: boolean });
969
+ }
970
+
957
971
  async searchLibrary(query: string, limit?: number): Promise<Record<string, unknown>> {
958
972
  return (await executeOperation('search', { q: query, scope: 'library', limit }, { source: 'sdk' })) as Record<
959
973
  string,
@@ -970,7 +984,13 @@ export class PmxCanvas extends EventEmitter {
970
984
 
971
985
  /** Budgeted cross-board brief. Budget is measured in UTF-16 characters. */
972
986
  async getContextBrief(
973
- options: { consumer?: string; since?: number; budget?: number } = {},
987
+ options: {
988
+ consumer?: string;
989
+ since?: number;
990
+ budget?: number;
991
+ contextDelivery?: boolean;
992
+ includeSteering?: boolean;
993
+ } = {},
974
994
  ): Promise<Record<string, unknown>> {
975
995
  return (await executeOperation('context.get', options, { source: 'sdk' })) as Record<string, unknown>;
976
996
  }
@@ -25,6 +25,8 @@ import {
25
25
  type PmxAxIntentKind,
26
26
  } from '../shared/ax-intent.js';
27
27
  import { OperationError } from './operations/types.js';
28
+ import { createBoardLocal } from './board-local.js';
29
+ import { canvasState } from './canvas-state.js';
28
30
 
29
31
  type IntentEmitter = (event: string, payload: Record<string, unknown>) => void;
30
32
  const MAX_VETO_TOMBSTONES = 128;
@@ -103,11 +105,33 @@ function nextIntentId(): string {
103
105
  }
104
106
 
105
107
  export class IntentRegistry {
106
- private readonly intents = new Map<string, PmxAxIntent>();
107
- private readonly vetoedIntentIds = new Map<string, number>();
108
- private readonly committingIntentIds = new Set<string>();
108
+ private readonly board = createBoardLocal(
109
+ () => ({
110
+ intents: new Map<string, PmxAxIntent>(),
111
+ vetoedIntentIds: new Map<string, number>(),
112
+ committingIntentIds: new Set<string>(),
113
+ sweepTimer: null as ReturnType<typeof setInterval> | null,
114
+ }),
115
+ (state) => {
116
+ if (state.sweepTimer) clearInterval(state.sweepTimer);
117
+ },
118
+ );
119
+ private get intents() {
120
+ return this.board.current.intents;
121
+ }
122
+ private get vetoedIntentIds() {
123
+ return this.board.current.vetoedIntentIds;
124
+ }
125
+ private get committingIntentIds() {
126
+ return this.board.current.committingIntentIds;
127
+ }
109
128
  private emit: IntentEmitter = () => {};
110
- private sweepTimer: ReturnType<typeof setInterval> | null = null;
129
+ private get sweepTimer() {
130
+ return this.board.current.sweepTimer;
131
+ }
132
+ private set sweepTimer(value: ReturnType<typeof setInterval> | null) {
133
+ this.board.current.sweepTimer = value;
134
+ }
111
135
 
112
136
  /** Inject the workbench SSE emitter (server.ts wires this at module load). */
113
137
  setEmitter(emitter: IntentEmitter | null): void {
@@ -118,6 +142,17 @@ export class IntentRegistry {
118
142
  return [...this.intents.values()];
119
143
  }
120
144
 
145
+ /** The first write from Home adopts its ghosts and vetoes without opening on signal. */
146
+ moveHomeIntentsToBoard(boardId: string): void {
147
+ const home = this.board.current;
148
+ canvasState.withBoard(boardId, () => {
149
+ for (const [id, intent] of home.intents) this.intents.set(id, intent);
150
+ for (const [id, expires] of home.vetoedIntentIds) this.vetoedIntentIds.set(id, expires);
151
+ this.ensureSweeper();
152
+ });
153
+ this.reset();
154
+ }
155
+
121
156
  /** Signal a new (or replace an existing) intent. Returns the stored envelope. */
122
157
  signal(raw: unknown): PmxAxIntent {
123
158
  const input = parseOrThrow(intentSignalSchema, raw, 'intent');
@@ -335,7 +370,10 @@ export class IntentRegistry {
335
370
 
336
371
  private ensureSweeper(): void {
337
372
  if (this.sweepTimer || this.intents.size === 0) return;
338
- this.sweepTimer = setInterval(() => this.sweep(), 1000);
373
+ this.sweepTimer = setInterval(
374
+ canvasState.bindBoard(() => this.sweep()),
375
+ 1000,
376
+ );
339
377
  // Don't keep the process (or a test runner) alive just for ghost expiry.
340
378
  (this.sweepTimer as { unref?: () => void }).unref?.();
341
379
  }
@@ -4,6 +4,7 @@ import { normalizeBoardFolder } from '../shared/boards.js';
4
4
  import { memoryKey, type MemoryTarget } from '../shared/memory-graph.js';
5
5
  import { currentActor, type ActorAttribution } from './attribution.js';
6
6
  import { OperationError } from './operations/types.js';
7
+ import { CARD_HISTORY_CTE } from './card-relocation.js';
7
8
  import type { LifecycleQualification } from '../shared/lifecycle-status.js';
8
9
  import type { CanvasNodeState } from './canvas-state.js';
9
10
  import { RELATION_PRESETS, relationPresetSchema, type RelationPreset } from '../shared/relations.js';
@@ -281,15 +282,19 @@ export class KnowledgeLifecycle {
281
282
  if (!targets.length) return new Map();
282
283
  const rows = this.indexedDb()
283
284
  .query<{ boardId: string; nodeId: string; validation: string | null; retirement: string | null }, [string]>(
284
- `WITH targets AS (
285
- SELECT json_extract(value, '$.boardId') AS boardId, json_extract(value, '$.cardId') AS nodeId
286
- FROM json_each(?)
287
- ) SELECT boardId, nodeId,
288
- (SELECT value FROM review_records WHERE board_id = boardId AND node_id = nodeId
285
+ `${CARD_HISTORY_CTE}
286
+ SELECT t.current_board AS boardId, t.node_id AS nodeId,
287
+ (SELECT value FROM review_records r JOIN history h
288
+ ON r.board_id = h.board_id AND r.node_id = h.node_id
289
+ AND r.sequence > h.review_after AND r.sequence <= h.review_through
290
+ WHERE h.current_board = t.current_board AND h.node_id = t.node_id
289
291
  AND ${validationPredicate} ORDER BY sequence DESC LIMIT 1) AS validation,
290
- (SELECT json_extract(value, '$.outcome') FROM review_records WHERE board_id = boardId AND node_id = nodeId
292
+ (SELECT json_extract(value, '$.outcome') FROM review_records r JOIN history h
293
+ ON r.board_id = h.board_id AND r.node_id = h.node_id
294
+ AND r.sequence > h.review_after AND r.sequence <= h.review_through
295
+ WHERE h.current_board = t.current_board AND h.node_id = t.node_id
291
296
  AND ${retirementPredicate} ORDER BY sequence DESC LIMIT 1) AS retirement
292
- FROM targets`,
297
+ FROM targets t`,
293
298
  )
294
299
  .all(JSON.stringify(targets));
295
300
  return new Map(
@@ -306,11 +311,29 @@ export class KnowledgeLifecycle {
306
311
  );
307
312
  }
308
313
 
314
+ /** Resolve a current card's location when evidence was recorded, without rewriting that evidence. */
315
+ locationAtReview(review: ReviewRecord, target: MemoryTarget): MemoryTarget {
316
+ const sequence = this.db()
317
+ .query<{ sequence: number }, [string, string, string]>(
318
+ "SELECT sequence FROM review_records WHERE board_id = ? AND node_id = ? AND json_extract(value, '$.id') = ?",
319
+ )
320
+ .get(review.boardId, review.nodeId, review.id)?.sequence;
321
+ if (sequence === undefined) return target;
322
+ const location = this.db()
323
+ .query<{ board_id: string }, [string, number, number]>(
324
+ `${CARD_HISTORY_CTE} SELECT board_id FROM history WHERE review_after < ? AND review_through >= ? LIMIT 1`,
325
+ )
326
+ .get(JSON.stringify([target]), sequence, sequence);
327
+ return location ? { ...target, boardId: location.board_id } : target;
328
+ }
329
+
309
330
  reviews(boardId: string, nodeId: string) {
310
331
  const records = (
311
332
  this.db()
312
- .query('SELECT value FROM review_records WHERE board_id = ? AND node_id = ? ORDER BY sequence')
313
- .all(boardId, nodeId) as { value: string }[]
333
+ .query(`${CARD_HISTORY_CTE} SELECT value FROM review_records r JOIN history h
334
+ ON r.board_id = h.board_id AND r.node_id = h.node_id
335
+ AND r.sequence > h.review_after AND r.sequence <= h.review_through ORDER BY sequence`)
336
+ .all(JSON.stringify([{ boardId, cardId: nodeId }])) as { value: string }[]
314
337
  ).map((r): ReviewRecord => {
315
338
  const record = JSON.parse(r.value) as ReviewRecord;
316
339
  return { ...record, contentRevision: record.contentRevision ?? null };
@@ -11,6 +11,7 @@ const processStartedAt = Date.now();
11
11
  interface Source {
12
12
  key: string;
13
13
  target: MemoryTarget;
14
+ edge?: { owner: MemoryTarget; id: string };
14
15
  baseline?: number;
15
16
  kind: string;
16
17
  observedAt?: string;
@@ -179,6 +180,7 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [],
179
180
  existing.push({
180
181
  key: JSON.stringify([board.id, edge.id, kind, memoryKey(target)]),
181
182
  target,
183
+ edge: { owner: from, id: edge.id },
182
184
  kind,
183
185
  baseline: edge.sourceRevision,
184
186
  });
@@ -219,13 +221,30 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [],
219
221
  const current = cards.get(memoryKey(source.target));
220
222
  const revision = source.availability === 'unavailable' ? null : (current?.contentRevision ?? null);
221
223
  status.observations[source.key] = revision;
222
- const observed = validation ? validation.sourceRevisions?.[source.key] : source.baseline;
224
+ let observed = validation ? validation.sourceRevisions?.[source.key] : source.baseline;
225
+ if (validation?.sourceRevisions && observed === undefined) {
226
+ const target = canvasState.knowledge.locationAtReview(validation, source.target);
227
+ const historicalKey = source.edge
228
+ ? JSON.stringify([
229
+ canvasState.knowledge.locationAtReview(validation, source.edge.owner).boardId,
230
+ source.edge.id,
231
+ source.kind,
232
+ memoryKey(target),
233
+ ])
234
+ : `provenance:${memoryKey(target)}`;
235
+ observed = validation.sourceRevisions[historicalKey];
236
+ }
223
237
  const title = current ? String(current.data.title ?? current.id) : 'Unavailable source';
238
+ const before = status.reasons.length;
224
239
  if (!current) status.reasons.push('A dependency source is unavailable; no fresh check.');
225
240
  else if (source.availability === 'unavailable')
226
241
  status.reasons.push(`${title}: source unavailable; cached content is not a fresh check.`);
227
242
  else if (observed !== revision || (!validation && ['contradicts', 'supersedes'].includes(source.kind)))
228
243
  status.reasons.push(`${source.kind}: ${title}${observed === undefined ? ' needs review' : ' changed'}.`);
244
+ if (status.reasons.length > before) {
245
+ status.sourceWarnings ??= {};
246
+ status.sourceWarnings[source.key] = status.reasons.slice(before);
247
+ }
229
248
  }
230
249
  }
231
250
  // Fixed point, not recursive traversal: cycles cannot overflow and unrelated links never propagate.
@@ -246,6 +265,15 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [],
246
265
  }
247
266
  }
248
267
  for (const [key, status] of Object.entries(result)) {
268
+ for (const source of sources.get(key) ?? []) {
269
+ const upstream = result[memoryKey(source.target)];
270
+ if (upstream?.due || upstream?.retired) {
271
+ status.sourceWarnings ??= {};
272
+ status.sourceWarnings[source.key] ??= [
273
+ upstream.retired ? 'Source is retired.' : 'An explicit source needs review.',
274
+ ];
275
+ }
276
+ }
249
277
  const references: Source[] = [...(sources.get(key) ?? [])];
250
278
  const boardId = portals.get(key);
251
279
  if (boardId) {
@@ -18,6 +18,7 @@ import { EXTENSION_ID, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps
18
18
  import type { McpUiClientCapabilities, McpUiResourceCsp, McpUiResourceMeta } from '@modelcontextprotocol/ext-apps';
19
19
  import { getToolUiResourceUri } from '@modelcontextprotocol/ext-apps/app-bridge';
20
20
  import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
21
+ import { canvasState } from './canvas-state.js';
21
22
 
22
23
  // A restriction follows async rehydration spawned by an embedded operation,
23
24
  // without disabling independent requests from the local workbench.
@@ -71,10 +72,12 @@ type RuntimeTransport = StdioClientTransport | StreamableHTTPClientTransport;
71
72
 
72
73
  interface McpAppSession {
73
74
  id: string;
75
+ board: object;
74
76
  serverName: string;
75
77
  client: Client;
76
78
  transport: RuntimeTransport;
77
79
  tools: Tool[];
80
+ unregisterBoardDispose: () => void;
78
81
  }
79
82
 
80
83
  const uiCapabilities: McpUiClientCapabilities = {
@@ -223,21 +226,35 @@ async function createSession(
223
226
  }
224
227
  const transport = buildTransport(transportConfig);
225
228
  const client = new Client({ name: 'pmx-canvas-app-host', version: '0.1.0' }, { capabilities: clientCapabilities });
226
- await client.connect(transport, requestOptions(timeoutMs));
227
-
228
- const toolList = await client.listTools(undefined, requestOptions(timeoutMs));
229
+ const id = randomId('mcp-app-session');
230
+ let disposed = false;
229
231
  const session: McpAppSession = {
230
- id: randomId('mcp-app-session'),
232
+ id,
233
+ board: canvasState.boardIdentity,
231
234
  serverName: normalizeServerName(serverName, transportConfig),
232
235
  client,
233
236
  transport,
234
- tools: toolList.tools,
237
+ tools: [],
238
+ unregisterBoardDispose: canvasState.onBoardDispose(() => {
239
+ disposed = true;
240
+ closeMcpAppSession(id);
241
+ }),
235
242
  };
236
243
  sessions.set(session.id, session);
237
- return session;
244
+ try {
245
+ await client.connect(transport, requestOptions(timeoutMs));
246
+ const toolList = await client.listTools(undefined, requestOptions(timeoutMs));
247
+ if (disposed) throw new Error('The app board was deleted while connecting.');
248
+ session.tools = toolList.tools;
249
+ return session;
250
+ } catch (error) {
251
+ await closeSession(session);
252
+ throw error;
253
+ }
238
254
  }
239
255
 
240
256
  async function closeSession(session: McpAppSession): Promise<void> {
257
+ session.unregisterBoardDispose();
241
258
  sessions.delete(session.id);
242
259
  await session.transport.close();
243
260
  }
@@ -468,5 +485,7 @@ export function hasMcpAppSession(sessionId: string): boolean {
468
485
  }
469
486
 
470
487
  export function listMcpAppSessionIds(): string[] {
471
- return [...sessions.keys()];
488
+ return [...sessions.values()]
489
+ .filter((session) => session.board === canvasState.boardIdentity)
490
+ .map((session) => session.id);
472
491
  }
@@ -14,6 +14,7 @@
14
14
 
15
15
  import type { CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
16
16
  import { currentActor as getActor, setCurrentActor } from './attribution.js';
17
+ import { createBoardLocal } from './board-local.js';
17
18
 
18
19
  // ── Types ────────────────────────────────────────────────────────────
19
20
 
@@ -88,11 +89,25 @@ export interface SnapshotDiffResult {
88
89
  removedEdges: { id: string; from: string; to: string; toBoardId?: string; type: string }[];
89
90
  }
90
91
 
91
- function comparableNodeData(data: Record<string, unknown>): Record<string, unknown> {
92
+ function comparableNodeData(data: Record<string, unknown>): string {
92
93
  const comparable = { ...data };
93
94
  delete comparable.title;
94
95
  delete comparable.content;
95
- return comparable;
96
+ // Match content-revision semantics: fetching the same page again is not a
97
+ // content change. Keep authored timestamps elsewhere, including nested data.
98
+ delete comparable.fetchedAt;
99
+ if (comparable.provenance && typeof comparable.provenance === 'object') {
100
+ const provenance = { ...(comparable.provenance as Record<string, unknown>) };
101
+ delete provenance.syncedAt;
102
+ comparable.provenance = provenance;
103
+ }
104
+ // Canonical JSON preserves array order and the persisted JSON semantics for
105
+ // undefined properties, while ignoring object-key insertion order at any depth.
106
+ return JSON.stringify(comparable, (_key, value: unknown) =>
107
+ value && typeof value === 'object' && !Array.isArray(value)
108
+ ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
109
+ : value,
110
+ );
96
111
  }
97
112
 
98
113
  // ── Ring Buffer ──────────────────────────────────────────────────────
@@ -100,11 +115,25 @@ function comparableNodeData(data: Record<string, unknown>): Record<string, unkno
100
115
  const MAX_ENTRIES = 200;
101
116
 
102
117
  class MutationHistory {
103
- private entries: MutationEntry[] = [];
104
- /** Index of the last applied mutation. -1 means nothing applied / all undone. */
105
- private cursor = -1;
106
- /** When true, mutations triggered by undo/redo are not recorded. */
107
- private _replaying = false;
118
+ private readonly board = createBoardLocal(() => ({ entries: [] as MutationEntry[], cursor: -1, replaying: false }));
119
+ private get entries() {
120
+ return this.board.current.entries;
121
+ }
122
+ private set entries(value: MutationEntry[]) {
123
+ this.board.current.entries = value;
124
+ }
125
+ private get cursor() {
126
+ return this.board.current.cursor;
127
+ }
128
+ private set cursor(value: number) {
129
+ this.board.current.cursor = value;
130
+ }
131
+ private get _replaying() {
132
+ return this.board.current.replaying;
133
+ }
134
+ private set _replaying(value: boolean) {
135
+ this.board.current.replaying = value;
136
+ }
108
137
 
109
138
  get isReplaying(): boolean {
110
139
  return this._replaying;
@@ -315,7 +344,7 @@ export function diffLayouts(
315
344
  changes.push(`content changed (${lenDiff >= 0 ? '+' : ''}${lenDiff} chars)`);
316
345
  }
317
346
 
318
- if (JSON.stringify(comparableNodeData(snapNode.data)) !== JSON.stringify(comparableNodeData(curNode.data))) {
347
+ if (comparableNodeData(snapNode.data) !== comparableNodeData(curNode.data)) {
319
348
  changes.push('data changed');
320
349
  }
321
350
 
@@ -152,15 +152,17 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
152
152
  {
153
153
  toolName: 'canvas_board',
154
154
  description:
155
+ '"move-selection" previews or atomically relocates selected cards and group descendants to a new board, preserves identity and leaves a portal. Supply sourceBoardId, nodeIds, name, expectedRevision and requestId; preview:true reports blockers and the Undo/snapshot boundary. Retry identical input with the same requestId. It never opens the destination. ' +
155
156
  'Knowledge data: knowledge-list reads categories/folders/shared relation presets; category-set replaces category (id/name/glyph/decayType/rate/triggers/revalidationMethod); category-remove deletes an unused category by id (explicit folder assignments, including empty folders, refuse deletion); relation-preset-set saves preset (id/name/vocabulary); relation-preset-remove deletes a template without changing existing board vocabularies; folder-set replaces path/categoryId/lifecycleOverride (null inherits). These workspace actions never open a board and are refused under agent scope fences. ' +
156
157
  'Boards in this workspace — the library of the human\'s and your working memory, filed in folders on Home. Your writes always go to the open board (activeBoardId); on Home (no board open) your first write opens the most recent board, or a new one when there are none. Action "list" returns every board (with its folder), most recently opened first; "get" reads one (default: the open board); "create" makes a new empty board WITHOUT opening it (optional folder); "open" explicitly switches the server active board to id (null or omitted opens Home), changing the subsequent write target. Humans browsing Map/Graph stay in the library until they explicitly open a board/card; do not infer activeBoardId from their visible library. After creating a board, open its returned id and verify activeBoardId before writing; another participant can switch the board later. "update" renames a board, files it under a folder (folder "" removes it), or designates its README via readmeNodeId. As part of normal board authoring, read the board metadata; if readmeNodeId is null, choose or create a concise markdown introduction explaining its purpose and key context, then designate that node with "update". Preserve an existing README unless the human asks to change it; humans can override the designation manually. "pin" adds a board to the working set (its README and pinned cards reach your brief whichever board is open; optional reason), "unpin" removes it; a pinned board is read-only context, never a write target. Only the human deletes boards.',
157
158
  actionSummary:
158
- 'list | get | create | create-from | open | update | pin | unpin | knowledge-list | category-set | category-remove | relation-preset-set | relation-preset-remove | folder-set',
159
+ 'list | get | create | create-from | move-selection | open | update | pin | unpin | knowledge-list | category-set | category-remove | relation-preset-set | relation-preset-remove | folder-set',
159
160
  actions: {
160
161
  list: 'board.list',
161
162
  get: 'board.get',
162
163
  create: 'board.create',
163
164
  'create-from': 'board.create-from',
165
+ 'move-selection': 'board.move-selection',
164
166
  open: 'board.open',
165
167
  update: 'board.update',
166
168
  'knowledge-list': 'knowledge.list',
@@ -239,9 +241,9 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
239
241
  {
240
242
  toolName: 'canvas_view',
241
243
  description:
242
- 'Canvas viewport and layout control. Action "arrange" auto-lays-out nodes (grid/columns/etc.); "focus" pans/zooms the viewport to a node; "fit" zooms to fit all nodes in view; "clear" removes every node and edge from the canvas; "remove-annotation" deletes a human-drawn annotation by id; "get-tour"/"set-tour" read or persist the presentation tour; "tour-go" presents a stop (stop index or step next/previous); "tour-exit" leaves presentation; "move" eases the camera to a nodeId, world rect or viewport over a duration. Camera actions never enter undo history.',
244
+ 'Canvas viewport and layout control. Action "arrange" auto-lays-out nodes (grid/columns/etc.); "focus" pans/zooms the viewport to a node; "fit" zooms to fit all nodes in view; "clear" removes every node and edge from the canvas; "add-annotation" draws freehand points or text in world coordinates; "remove-annotation" erases a whole mark by id; "get-tour"/"set-tour" read or persist the presentation tour; "tour-go" presents a stop (stop index or step next/previous); "tour-exit" leaves presentation; "move" eases the camera to a nodeId, world rect or viewport over a duration. Camera actions never enter undo history.',
243
245
  actionSummary:
244
- 'arrange | focus | fit | clear | remove-annotation | get-tour | set-tour | tour-go | tour-exit | move',
246
+ 'arrange | focus | fit | clear | add-annotation | remove-annotation | get-tour | set-tour | tour-go | tour-exit | move',
245
247
  actions: {
246
248
  'get-tour': 'tour.get',
247
249
  'set-tour': 'tour.set',
@@ -252,6 +254,7 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
252
254
  focus: 'node.focus',
253
255
  fit: 'view.fit',
254
256
  clear: 'canvas.clear',
257
+ 'add-annotation': 'annotation.add',
255
258
  'remove-annotation': 'annotation.remove',
256
259
  },
257
260
  },
@@ -86,6 +86,20 @@ const payloads: Record<string, ZodType> = {
86
86
  board,
87
87
  z.looseObject({ ok: z.boolean(), sourceBoardId: id, cards: z.array(node), edges: z.array(edge) }),
88
88
  ]),
89
+ 'board.move-selection': z.union([
90
+ z.looseObject({
91
+ ok: z.boolean(),
92
+ preview: z.looseObject({ sourceBoardId: id, nodeIds: z.array(id), blockers: z.array(z.string()) }),
93
+ }),
94
+ z.looseObject({
95
+ ok: z.boolean(),
96
+ sourceBoardId: id,
97
+ destinationBoardId: id,
98
+ portalId: id,
99
+ nodeIds: z.array(id),
100
+ replayed: z.boolean(),
101
+ }),
102
+ ]),
89
103
  'board.open': boards,
90
104
  'board.update': boards,
91
105
  'board.pin': boards,
@@ -145,6 +159,7 @@ const payloads: Record<string, ZodType> = {
145
159
  'node.focus': z.looseObject({ ok: z.boolean(), focused: id, panned: z.boolean() }),
146
160
  'view.fit': z.looseObject({ ok: z.boolean(), viewport }),
147
161
  'canvas.clear': z.looseObject({ ok: z.boolean(), cleared: z.literal(true) }),
162
+ 'annotation.add': entity('annotation'),
148
163
  'annotation.remove': ok,
149
164
  'context.get': z.looseObject({ version: z.literal(1), budgetUnit: z.literal('utf16-code-units') }),
150
165
  'memory-graph.get': z.looseObject({
@@ -36,6 +36,17 @@ export interface OperationToolHost extends OperationMcpToolHost {
36
36
  recordContextRead(read: ContextReadInput): Promise<void>;
37
37
  }
38
38
 
39
+ const routingShape = {
40
+ targetBoardId: z
41
+ .string()
42
+ .min(1)
43
+ .optional()
44
+ .describe(
45
+ 'Execute on this board without changing the shared workbench view. Nested operations retain this target.',
46
+ ),
47
+ agentId: z.string().min(1).optional().describe('Stable agent/thread identity for attribution and scope fences.'),
48
+ };
49
+
39
50
  function executionOptions(extra: RequestHandlerExtra<ServerRequest, ServerNotification>): OperationExecutionOptions {
40
51
  const progressToken = extra._meta?.progressToken;
41
52
  return {
@@ -58,9 +69,23 @@ async function invokeMcpOperation(
58
69
  options: OperationExecutionOptions,
59
70
  ) {
60
71
  options.signal?.throwIfAborted();
61
- const board = typeof opInput.board === 'string' && opInput.board.trim() ? opInput.board.trim() : undefined;
62
- const readTarget = CONTEXT_READ_OPS.has(op.name) ? await host.prepareContextRead(board) : null;
63
- if (readTarget?.boardId && 'board' in op.inputShape) {
72
+ // buildInput may project operation-specific fields; routing belongs to the
73
+ // invocation and must also cover formatter follow-up reads and accounting.
74
+ const routing = {
75
+ ...(formatInput.targetBoardId !== undefined ? { targetBoardId: formatInput.targetBoardId } : {}),
76
+ ...(formatInput.agentId !== undefined ? { agentId: formatInput.agentId } : {}),
77
+ };
78
+ opInput = { ...opInput, ...routing };
79
+ if (typeof routing.agentId === 'string') consumer = routing.agentId;
80
+ const board =
81
+ typeof routing.targetBoardId === 'string'
82
+ ? routing.targetBoardId
83
+ : typeof opInput.board === 'string' && opInput.board.trim()
84
+ ? opInput.board.trim()
85
+ : undefined;
86
+ const preparesContext = op.name === 'context.get' && opInput.contextDelivery === false;
87
+ const readTarget = CONTEXT_READ_OPS.has(op.name) && !preparesContext ? await host.prepareContextRead(board) : null;
88
+ if (readTarget?.boardId && 'board' in op.inputShape && opInput.board === undefined) {
64
89
  opInput = { ...opInput, board: readTarget.boardId };
65
90
  formatInput = { ...formatInput, board: readTarget.boardId };
66
91
  }
@@ -70,8 +95,10 @@ async function invokeMcpOperation(
70
95
  // Preserve the host receiver, and bind options to formatter follow-up reads
71
96
  // (history flags, snapshot summary, streaming node metadata).
72
97
  const formatterHost: OperationMcpToolHost = {
73
- getPinnedNodeIds: (target) => host.getPinnedNodeIds(target),
74
- invoker: () => ({ invoke: (name, input, followUp) => invoker.invoke(name, input, { ...options, ...followUp }) }),
98
+ getPinnedNodeIds: (target) => host.getPinnedNodeIds(target ?? board),
99
+ invoker: () => ({
100
+ invoke: (name, input, followUp) => invoker.invoke(name, { ...input, ...routing }, { ...options, ...followUp }),
101
+ }),
75
102
  };
76
103
  const formatted = structuredOperationResult(
77
104
  op.name,
@@ -151,7 +178,7 @@ export function registerOperationTools(server: McpServer, getHost: () => Promise
151
178
  // successful over MCP while the same HTTP call 400'd (0.5.1 Amp
152
179
  // finding A). Loose parsing forwards unknown keys to the op layer,
153
180
  // the single authority on dead-field rejection, HTTP and MCP alike.
154
- inputSchema: z.looseObject({ ...op.inputShape, ...(tool.extraShape ?? {}) }),
181
+ inputSchema: z.looseObject({ ...op.inputShape, ...(tool.extraShape ?? {}), ...routingShape }),
155
182
  outputSchema: operationOutputSchema([op.name]),
156
183
  annotations: operationToolAnnotations(tool.toolName),
157
184
  },
@@ -376,8 +403,8 @@ export function registerOperationHelpTool(server: McpServer): void {
376
403
  const composite = compositeToolDefinitions.find((def) => compositeMemberOps(def).includes(name));
377
404
  const publicName = (field: string) =>
378
405
  Object.entries(composite?.fieldRemap ?? {}).find(([, original]) => original === field)?.[0] ?? field;
379
- const shape = Object.fromEntries(
380
- Object.entries({ ...op.inputShape, ...op.mcp?.extraShape }).map(([field, schema]) => [
406
+ const shape: Record<string, ZodTypeAny> = Object.fromEntries(
407
+ Object.entries({ ...op.inputShape, ...op.mcp?.extraShape, ...routingShape }).map(([field, schema]) => [
381
408
  publicName(field),
382
409
  schema,
383
410
  ]),
@@ -444,7 +471,7 @@ export function registerCompositeTools(
444
471
  def.toolName,
445
472
  {
446
473
  description: def.description,
447
- inputSchema: z.looseObject(buildCompositeShape(def)),
474
+ inputSchema: z.looseObject({ ...buildCompositeShape(def), ...routingShape }),
448
475
  outputSchema: operationOutputSchema(compositeMemberOps(def)),
449
476
  annotations: operationToolAnnotations(def.toolName),
450
477
  },
@@ -13,6 +13,7 @@
13
13
  *
14
14
  * This module must never import server.ts or index.ts.
15
15
  */
16
+ import { isDeepStrictEqual } from 'node:util';
16
17
  import { z } from 'zod';
17
18
  import { canvasState, type CanvasAnnotation } from '../../canvas-state.js';
18
19
  import { summarizeCanvasAnnotation } from '../../canvas-serialization.js';
@@ -35,7 +36,7 @@ const annotationRemoveOperation = defineOperation<z.infer<typeof annotationRemov
35
36
  },
36
37
  mcp: {
37
38
  toolName: 'canvas_remove_annotation',
38
- description: 'Remove a human-drawn canvas annotation by ID.',
39
+ description: 'Erase a whole canvas mark by ID, whether drawn by a human or agent.',
39
40
  extraShape: {
40
41
  id: z.string().describe('Annotation ID to remove'),
41
42
  },
@@ -119,6 +120,19 @@ const annotationAddOperation = defineOperation<z.infer<typeof annotationAddSchem
119
120
  method: 'POST',
120
121
  path: '/api/canvas/annotation',
121
122
  },
123
+ mcp: {
124
+ toolName: 'canvas_add_annotation',
125
+ description: 'Draw canvas ink or write a text note in world coordinates. No session required.',
126
+ extraShape: {
127
+ type: z.enum(['freehand', 'text']).optional(),
128
+ points: z.array(z.object({ x: z.number(), y: z.number() })).min(1),
129
+ width: z.number().optional(),
130
+ color: z.string().optional(),
131
+ label: z.string().optional(),
132
+ text: z.string().optional(),
133
+ id: z.string().optional(),
134
+ },
135
+ },
122
136
  handler: (input) => {
123
137
  const body: Record<string, unknown> = input;
124
138
  const type = body.type === 'text' ? 'text' : 'freehand';
@@ -164,6 +178,13 @@ const annotationAddOperation = defineOperation<z.infer<typeof annotationAddSchem
164
178
  createdAt: new Date().toISOString(),
165
179
  };
166
180
 
181
+ const existing = canvasState.getAnnotations().find((entry) => entry.id === id);
182
+ if (existing) {
183
+ if (!isDeepStrictEqual(existing, { ...annotation, createdAt: existing.createdAt })) {
184
+ throw new OperationError(`Annotation "${id}" already exists with different content.`, 409);
185
+ }
186
+ return { ok: true, annotation: summarizeCanvasAnnotation(existing) };
187
+ }
167
188
  canvasState.addAnnotation(annotation);
168
189
  return { ok: true, annotation: summarizeCanvasAnnotation(annotation) };
169
190
  },
@@ -69,6 +69,7 @@ function clampTimeoutMs(value: unknown): number {
69
69
  /** Accept the operation's timeoutMs as well as the direct HTTP waitMs spelling. */
70
70
  function readWaitMsInput(_req: Request, params: Record<string, string>, url: URL): Record<string, unknown> {
71
71
  return {
72
+ ...Object.fromEntries(url.searchParams),
72
73
  ...params,
73
74
  timeoutMs: clampTimeoutMs(url.searchParams.get('timeoutMs') ?? url.searchParams.get('waitMs') ?? ''),
74
75
  };