pmx-canvas 0.10.0 → 0.12.0

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 (343) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +3 -1
  2. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  3. package/.github/extensions/pmx-canvas/tools.json +688 -128
  4. package/CHANGELOG.md +69 -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 +251 -0
  11. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/evals.json +1086 -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 +1785 -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 +251 -0
  26. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/tours-and-recording.md +111 -0
  27. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/workbench-workflows.md +900 -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-cwtsds5d.js +1 -0
  33. package/dist/canvas/chunks/architecture-TIHT7OUA-x63cwejs.js +1 -0
  34. package/dist/canvas/chunks/architectureDiagram-T3A2C74G-b4xc9tpz.js +36 -0
  35. package/dist/canvas/chunks/blockDiagram-VBNYF7ZC-ahp3yxxs.js +132 -0
  36. package/dist/canvas/chunks/c4Diagram-5PPSVZJV-nappnay7.js +10 -0
  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-a428syda.js +1 -0
  40. package/dist/canvas/chunks/cynefin-VYW2F7L2-8v6jxfz8.js +1 -0
  41. package/dist/canvas/chunks/cynefinDiagram-MW4NZA55-nspsqebj.js +62 -0
  42. package/dist/canvas/chunks/dagre-VZM6K2ZE-8y6177fh.js +4 -0
  43. package/dist/canvas/chunks/diagram-7IWD3JNH-vym028wn.js +30 -0
  44. package/dist/canvas/chunks/diagram-B4RE2ZJO-jddqb8sy.js +3 -0
  45. package/dist/canvas/chunks/diagram-LBJQPF4R-bjwbpkrn.js +24 -0
  46. package/dist/canvas/chunks/diagram-Q27KOJAE-j6ph6hcx.js +24 -0
  47. package/dist/canvas/chunks/diagram-UB23O5K3-p09w8jej.js +41 -0
  48. package/dist/canvas/chunks/ebnfDiagram-BXEA7PRR-j975ycq5.js +1 -0
  49. package/dist/canvas/chunks/erDiagram-JOGREHBK-sxp6hjn8.js +85 -0
  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-4tv230y2.js +292 -0
  53. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-macbhand.js +1 -0
  54. package/dist/canvas/chunks/gitGraphDiagram-DS77QQ5N-mqgmnw9g.js +106 -0
  55. package/dist/canvas/chunks/index-037z5dz2.js +232 -0
  56. package/dist/canvas/chunks/index-081n3har.js +207 -0
  57. package/dist/canvas/chunks/index-5n03dmb9.js +2 -0
  58. package/dist/canvas/chunks/index-5tv8v1hk.js +1 -0
  59. package/dist/canvas/chunks/index-60pgzdqe.js +157 -0
  60. package/dist/canvas/chunks/index-6ceya556.js +332 -0
  61. package/dist/canvas/chunks/index-8g1vb57n.js +2 -0
  62. package/dist/canvas/chunks/index-8rjes9r6.js +2 -0
  63. package/dist/canvas/chunks/index-9022d1n9.js +16 -0
  64. package/dist/canvas/chunks/index-9b02042t.js +2 -0
  65. package/dist/canvas/chunks/index-ap4q4vez.js +2 -0
  66. package/dist/canvas/chunks/index-dyp2nhd3.js +2 -0
  67. package/dist/canvas/chunks/index-frvta4tm.js +2 -0
  68. package/dist/canvas/chunks/index-gf4e0et4.js +2 -0
  69. package/dist/canvas/chunks/index-h9ycj8n2.js +2 -0
  70. package/dist/canvas/chunks/index-hj99nz1f.js +1 -0
  71. package/dist/canvas/chunks/index-mergmj97.js +2 -0
  72. package/dist/canvas/chunks/index-nwsya6dj.js +301 -0
  73. package/dist/canvas/chunks/index-nz8qbd1p.js +2 -0
  74. package/dist/canvas/chunks/index-qhfrazv3.js +2 -0
  75. package/dist/canvas/chunks/index-v119nznk.js +4 -0
  76. package/dist/canvas/chunks/index-vpt37m3d.js +89 -0
  77. package/dist/canvas/chunks/index-y9ma6azy.js +2 -0
  78. package/dist/canvas/chunks/index-zx0j3a2z.js +179 -0
  79. package/dist/canvas/chunks/info-DKCQHKI2-a0ykzs9z.js +1 -0
  80. package/dist/canvas/chunks/infoDiagram-6WML65LV-qv97qq6a.js +2 -0
  81. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-p9306gh0.js +70 -0
  82. package/dist/canvas/chunks/journeyDiagram-NVQOT4AX-sv67zats.js +139 -0
  83. package/dist/canvas/chunks/kanban-definition-27J2QSJJ-k2pfam6p.js +89 -0
  84. package/dist/canvas/chunks/katex-dmg8k4zm.js +257 -0
  85. package/dist/canvas/chunks/mermaid.core-83r0fdce.js +12 -0
  86. package/dist/canvas/chunks/mindmap-definition-FAOFIHXS-vg2newm1.js +96 -0
  87. package/dist/canvas/chunks/packet-7NZHBO7P-tqnabcdv.js +1 -0
  88. package/dist/canvas/chunks/pegDiagram-VL7TDLO6-k2h7zc5n.js +1 -0
  89. package/dist/canvas/chunks/pie-RZYD4A2V-wke6gyhe.js +1 -0
  90. package/dist/canvas/chunks/pieDiagram-7S7Q4E2Y-hg169p1v.js +39 -0
  91. package/dist/canvas/chunks/quadrantDiagram-CIZ2JOQS-bxtvsq0b.js +7 -0
  92. package/dist/canvas/chunks/radar-I7S5WNFK-w7ka0drw.js +1 -0
  93. package/dist/canvas/chunks/railroad-3IZDKUUU-y3vg318x.js +1 -0
  94. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-6hqvcz3c.js +1 -0
  95. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-vcae260m.js +1 -0
  96. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-0dbyme23.js +1 -0
  97. package/dist/canvas/chunks/railroadDiagram-AXF67PYL-njyhcp4p.js +1 -0
  98. package/dist/canvas/chunks/requirementDiagram-LRYGKXZP-2z6v09gm.js +84 -0
  99. package/dist/canvas/chunks/sankeyDiagram-W5VNT64P-15mzam0e.js +40 -0
  100. package/dist/canvas/chunks/sequenceDiagram-SI44F4Z6-rbshyd2c.js +162 -0
  101. package/dist/canvas/chunks/sizeCapture-X5ZJPWSS-sg26xwts.js +1 -0
  102. package/dist/canvas/chunks/stateDiagram-OKZ733FA-6eahp98e.js +1 -0
  103. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-tty0ggn5.js +1 -0
  104. package/dist/canvas/chunks/swimlanes-SLNWSIFB-ze5db10t.js +1 -0
  105. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-thze75gq.js +8 -0
  106. package/dist/canvas/chunks/timeline-definition-Z64GVDOM-vnp7jyy8.js +120 -0
  107. package/dist/canvas/chunks/treeView-QDETBFTQ-6ysffnm6.js +1 -0
  108. package/dist/canvas/chunks/treemap-6X3UGDF4-m3h9p066.js +1 -0
  109. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-0qrnb7mj.js +34 -0
  110. package/dist/canvas/chunks/wardley-OPB4EBWU-dp37drwx.js +1 -0
  111. package/dist/canvas/chunks/wardleyDiagram-T6FBY63Y-8nxg8wqt.js +78 -0
  112. package/dist/canvas/chunks/xychartDiagram-ELKLHX3M-nj921e7s.js +7 -0
  113. package/dist/canvas/global.css +483 -230
  114. package/dist/canvas/index.js +158 -219
  115. package/dist/canvas/mcp-app.html +4301 -548
  116. package/dist/canvas/mermaid-entry.js +338 -338
  117. package/dist/canvas/surface-theme.css +6 -6
  118. package/dist/json-render/index.css +1 -1
  119. package/dist/json-render/index.js +49 -49
  120. package/dist/types/client/canvas/BarHint.d.ts +5 -3
  121. package/dist/types/client/canvas/BoardMoveDialog.d.ts +2 -0
  122. package/dist/types/client/canvas/CanvasNode.d.ts +0 -11
  123. package/dist/types/client/canvas/EdgeEditor.d.ts +19 -0
  124. package/dist/types/client/canvas/EdgeLayer.d.ts +1 -2
  125. package/dist/types/client/canvas/GroupNavigation.d.ts +2 -0
  126. package/dist/types/client/canvas/TextPrompt.d.ts +4 -0
  127. package/dist/types/client/canvas/auto-fit.d.ts +4 -5
  128. package/dist/types/client/mermaid-entry.d.ts +1 -2
  129. package/dist/types/client/mermaid-render.d.ts +1 -0
  130. package/dist/types/client/nodes/BoardNode.d.ts +1 -1
  131. package/dist/types/client/nodes/ExtAppFrame.d.ts +7 -90
  132. package/dist/types/client/nodes/McpAppNode.d.ts +1 -25
  133. package/dist/types/client/nodes/MermaidNode.d.ts +1 -8
  134. package/dist/types/client/nodes/PromptNode.d.ts +1 -0
  135. package/dist/types/client/nodes/ViewerFrame.d.ts +29 -0
  136. package/dist/types/client/nodes/ViewerHost.d.ts +6 -0
  137. package/dist/types/client/nodes/WorkCard.d.ts +7 -0
  138. package/dist/types/client/nodes/iframe-document-url.d.ts +3 -9
  139. package/dist/types/client/nodes/primitive-copy.d.ts +5 -0
  140. package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +1 -0
  141. package/dist/types/client/nodes/use-iframe-content-height.d.ts +1 -1
  142. package/dist/types/client/nodes/use-surface-frame.d.ts +4 -6
  143. package/dist/types/client/nodes/viewer-runtime-state.d.ts +2 -1
  144. package/dist/types/client/state/canvas-store.d.ts +18 -1
  145. package/dist/types/client/state/human-store.d.ts +2 -0
  146. package/dist/types/client/state/intent-bridge.d.ts +5 -2
  147. package/dist/types/client/state/presentation.d.ts +17 -6
  148. package/dist/types/client/state/relation-store.d.ts +11 -0
  149. package/dist/types/client/types.d.ts +1 -1
  150. package/dist/types/json-render/catalog.d.ts +2 -2
  151. package/dist/types/json-render/server.d.ts +2 -1
  152. package/dist/types/server/agent-presence.d.ts +1 -0
  153. package/dist/types/server/ax-interaction.d.ts +1 -0
  154. package/dist/types/server/ax-state-manager.d.ts +10 -2
  155. package/dist/types/server/board-local.d.ts +4 -0
  156. package/dist/types/server/board-move.d.ts +38 -0
  157. package/dist/types/server/canvas-db.d.ts +12 -2
  158. package/dist/types/server/canvas-operations.d.ts +5 -6
  159. package/dist/types/server/canvas-provenance.d.ts +1 -1
  160. package/dist/types/server/canvas-serialization.d.ts +1 -0
  161. package/dist/types/server/canvas-state.d.ts +86 -20
  162. package/dist/types/server/card-relocation.d.ts +6 -0
  163. package/dist/types/server/context-reads.d.ts +3 -1
  164. package/dist/types/server/human-presence.d.ts +1 -1
  165. package/dist/types/server/index.d.ts +26 -10
  166. package/dist/types/server/intent-registry.d.ts +8 -4
  167. package/dist/types/server/knowledge-lifecycle.d.ts +4 -2
  168. package/dist/types/server/mutation-history.d.ts +7 -5
  169. package/dist/types/server/operations/ops/boards.d.ts +0 -1
  170. package/dist/types/server/operations/registry.d.ts +2 -0
  171. package/dist/types/server/tour-control.d.ts +13 -3
  172. package/dist/types/server/trace-manager.d.ts +1 -0
  173. package/dist/types/shared/ax-kinds.d.ts +6 -0
  174. package/dist/types/shared/ax-surface-protocol.d.ts +8 -9
  175. package/dist/types/shared/human-presence.d.ts +1 -0
  176. package/dist/types/shared/lifecycle-status.d.ts +2 -0
  177. package/dist/types/shared/tour.d.ts +8 -0
  178. package/docs/RELEASE.md +21 -14
  179. package/docs/agent-plugin.md +16 -2
  180. package/docs/amp-plugin.md +271 -0
  181. package/docs/cli.md +42 -3
  182. package/docs/design/Chrome.dc.html +32 -6
  183. package/docs/design/GuestName.dc.html +50 -0
  184. package/docs/design/PluginLogo.svg +7 -0
  185. package/docs/design/Types1.dc.html +3 -2
  186. package/docs/design/Types2.dc.html +1 -1
  187. package/docs/design/Types3.dc.html +1 -1
  188. package/docs/design/WorkCards.dc.html +39 -0
  189. package/docs/design/Zoom.dc.html +37 -60
  190. package/docs/design/canvas.json +17 -1
  191. package/docs/design/plugin-logo.png +0 -0
  192. package/docs/design.md +280 -12
  193. package/docs/evals/agents-md-sections.md +38 -0
  194. package/docs/http-api.md +179 -11
  195. package/docs/mcp.md +69 -9
  196. package/docs/node-types.md +109 -4
  197. package/docs/plans/plan-009-tech-debt-backlog.md +3 -3
  198. package/docs/plans/plan-019-post-0.9-roadmap.md +327 -31
  199. package/docs/plans/plan-020-wave-5-work-planning.md +553 -0
  200. package/docs/product-vision-2026-09.md +42 -17
  201. package/docs/sdk.md +18 -0
  202. package/package.json +7 -4
  203. package/plugins/pmx-canvas/assets/preview.png +0 -0
  204. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +3 -1
  205. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  206. package/plugins/pmx-canvas/plugin.json +2 -2
  207. package/plugins/pmx-canvas/runtime.json +1 -1
  208. package/plugins/pmx-canvas/tools.json +688 -128
  209. package/skills/data-analysis/SKILL.md +13 -26
  210. package/skills/frontend-design/LICENSE.txt +177 -0
  211. package/skills/frontend-design/SKILL.md +3 -0
  212. package/skills/pmx-canvas/SKILL.md +232 -849
  213. package/skills/pmx-canvas/evals/evals.json +26 -3
  214. package/skills/pmx-canvas/references/full-reference.md +89 -24
  215. package/skills/pmx-canvas/references/html-primitives.md +7 -0
  216. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  217. package/skills/pmx-canvas/references/tours-and-recording.md +29 -2
  218. package/skills/pmx-canvas/references/workbench-workflows.md +900 -0
  219. package/skills/pmx-canvas-testing/SKILL.md +7 -5
  220. package/skills/published-consumer-e2e/SKILL.md +25 -8
  221. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +51 -77
  222. package/skills/web-artifacts-builder/LICENSE.txt +202 -0
  223. package/skills/web-artifacts-builder/SKILL.md +6 -0
  224. package/skills/web-artifacts-builder/scripts/init-artifact.sh +2 -2
  225. package/src/cli/commands/apps.ts +1 -0
  226. package/src/cli/commands/boards.ts +30 -0
  227. package/src/client/App.tsx +20 -4
  228. package/src/client/canvas/AttentionHistory.tsx +10 -2
  229. package/src/client/canvas/BarHint.tsx +6 -3
  230. package/src/client/canvas/BoardMoveDialog.tsx +225 -0
  231. package/src/client/canvas/CanvasNode.tsx +79 -49
  232. package/src/client/canvas/CanvasViewport.tsx +42 -14
  233. package/src/client/canvas/ContextMenu.tsx +97 -15
  234. package/src/client/canvas/EdgeEditor.tsx +149 -0
  235. package/src/client/canvas/EdgeLayer.tsx +441 -174
  236. package/src/client/canvas/ExpandedNodeOverlay.tsx +5 -2
  237. package/src/client/canvas/GroupNavigation.tsx +85 -0
  238. package/src/client/canvas/Presentation.tsx +53 -3
  239. package/src/client/canvas/RelationsPanel.tsx +9 -0
  240. package/src/client/canvas/SelectionBar.tsx +21 -0
  241. package/src/client/canvas/SessionPanel.tsx +35 -1
  242. package/src/client/canvas/TextPrompt.tsx +18 -1
  243. package/src/client/canvas/TopBar.tsx +54 -9
  244. package/src/client/canvas/auto-fit.ts +9 -11
  245. package/src/client/canvas/canvas-area.ts +2 -2
  246. package/src/client/canvas/use-focus-trap.ts +6 -1
  247. package/src/client/mermaid-entry.ts +6 -20
  248. package/src/client/mermaid-render.ts +39 -0
  249. package/src/client/nodes/AxStepControls.tsx +45 -69
  250. package/src/client/nodes/BoardNode.tsx +65 -9
  251. package/src/client/nodes/ContextNode.tsx +88 -396
  252. package/src/client/nodes/ExtAppFrame.tsx +302 -1465
  253. package/src/client/nodes/FileNode.tsx +92 -81
  254. package/src/client/nodes/HtmlNode.tsx +8 -36
  255. package/src/client/nodes/LedgerNode.tsx +31 -17
  256. package/src/client/nodes/MarkdownNode.tsx +14 -24
  257. package/src/client/nodes/McpAppNode.tsx +48 -154
  258. package/src/client/nodes/MermaidNode.tsx +80 -69
  259. package/src/client/nodes/PromptNode.tsx +44 -17
  260. package/src/client/nodes/ResponseNode.tsx +8 -6
  261. package/src/client/nodes/StatusNode.tsx +34 -7
  262. package/src/client/nodes/StatusSummary.tsx +16 -3
  263. package/src/client/nodes/TraceNode.tsx +8 -8
  264. package/src/client/nodes/ViewerFrame.tsx +273 -0
  265. package/src/client/nodes/ViewerHost.tsx +41 -0
  266. package/src/client/nodes/WebpageNode.tsx +22 -24
  267. package/src/client/nodes/WorkCard.tsx +175 -0
  268. package/src/client/nodes/iframe-document-url.ts +20 -27
  269. package/src/client/nodes/primitive-copy.ts +97 -0
  270. package/src/client/nodes/use-ax-surface-bridge.ts +18 -14
  271. package/src/client/nodes/use-iframe-content-height.ts +34 -10
  272. package/src/client/nodes/use-surface-frame.ts +22 -11
  273. package/src/client/nodes/viewer-runtime-state.ts +41 -35
  274. package/src/client/state/attention-bridge.ts +5 -0
  275. package/src/client/state/attention-store.ts +1 -0
  276. package/src/client/state/canvas-store.ts +202 -26
  277. package/src/client/state/human-store.ts +22 -2
  278. package/src/client/state/intent-bridge.ts +20 -5
  279. package/src/client/state/presentation.ts +135 -15
  280. package/src/client/state/relation-store.ts +127 -1
  281. package/src/client/state/sse-bridge.ts +18 -19
  282. package/src/client/theme/global.css +483 -230
  283. package/src/client/theme/surface-theme.css +6 -6
  284. package/src/client/types.ts +1 -0
  285. package/src/json-render/charts/tufte-components.tsx +44 -2
  286. package/src/json-render/renderer/index.css +62 -2
  287. package/src/json-render/renderer/index.tsx +31 -7
  288. package/src/json-render/server.ts +3 -2
  289. package/src/mcp/server.ts +13 -3
  290. package/src/server/agent-presence.ts +43 -23
  291. package/src/server/ax-flow-loop.ts +6 -0
  292. package/src/server/ax-gate-ttl.ts +8 -1
  293. package/src/server/ax-interaction.ts +16 -10
  294. package/src/server/ax-state-manager.ts +17 -4
  295. package/src/server/ax-wait.ts +20 -6
  296. package/src/server/board-export.ts +54 -30
  297. package/src/server/board-local.ts +20 -0
  298. package/src/server/board-move.ts +297 -0
  299. package/src/server/canvas-db.ts +249 -158
  300. package/src/server/canvas-geometry.ts +1 -0
  301. package/src/server/canvas-operations.ts +139 -109
  302. package/src/server/canvas-schema.ts +21 -0
  303. package/src/server/canvas-serialization.ts +2 -0
  304. package/src/server/canvas-state.ts +797 -159
  305. package/src/server/card-relocation.ts +53 -0
  306. package/src/server/code-graph.ts +4 -3
  307. package/src/server/context-reads.ts +83 -55
  308. package/src/server/demo-state.json +140 -114
  309. package/src/server/file-watcher.ts +30 -10
  310. package/src/server/html-primitives.ts +3 -9
  311. package/src/server/html-surface.ts +8 -0
  312. package/src/server/human-presence.ts +18 -5
  313. package/src/server/index.ts +54 -24
  314. package/src/server/intent-registry.ts +43 -5
  315. package/src/server/knowledge-lifecycle.ts +32 -9
  316. package/src/server/lifecycle-status.ts +29 -1
  317. package/src/server/mcp-app-runtime.ts +26 -7
  318. package/src/server/mutation-history.ts +37 -8
  319. package/src/server/operations/composites.ts +6 -3
  320. package/src/server/operations/mcp-contracts.ts +32 -2
  321. package/src/server/operations/mcp.ts +36 -9
  322. package/src/server/operations/ops/annotation.ts +22 -1
  323. package/src/server/operations/ops/ax-await.ts +1 -0
  324. package/src/server/operations/ops/ax-timeline.ts +28 -2
  325. package/src/server/operations/ops/batch.ts +25 -11
  326. package/src/server/operations/ops/boards.ts +45 -6
  327. package/src/server/operations/ops/nodes.ts +70 -0
  328. package/src/server/operations/ops/query.ts +32 -3
  329. package/src/server/operations/ops/snapshots.ts +1 -0
  330. package/src/server/operations/ops/viewport.ts +11 -9
  331. package/src/server/operations/registry.ts +49 -4
  332. package/src/server/server.ts +69 -75
  333. package/src/server/tour-control.ts +97 -12
  334. package/src/server/trace-manager.ts +4 -0
  335. package/src/server/web-artifacts/scripts/init-artifact.sh +2 -2
  336. package/src/shared/agent-context.ts +7 -1
  337. package/src/shared/auto-arrange.ts +5 -4
  338. package/src/shared/ax-kinds.ts +13 -0
  339. package/src/shared/ax-surface-protocol.ts +20 -9
  340. package/src/shared/content-height-reporter.ts +4 -3
  341. package/src/shared/human-presence.ts +1 -0
  342. package/src/shared/lifecycle-status.ts +2 -0
  343. package/src/shared/tour.ts +8 -0
@@ -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
  },
@@ -11,6 +11,7 @@ import { tourSchema, tourStopSchema } from '../../shared/tour.js';
11
11
  const id = z.string().min(1);
12
12
  const count = z.number().int().nonnegative();
13
13
  const ok = z.looseObject({ ok: z.boolean() });
14
+ const tourSuperseded = z.object({ ok: z.literal(true), superseded: z.literal(true) });
14
15
  const identified = z.looseObject({ id });
15
16
  const message = z.strictObject({ message: z.string() });
16
17
  const node = z.looseObject({
@@ -85,6 +86,20 @@ const payloads: Record<string, ZodType> = {
85
86
  board,
86
87
  z.looseObject({ ok: z.boolean(), sourceBoardId: id, cards: z.array(node), edges: z.array(edge) }),
87
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
+ ]),
88
103
  'board.open': boards,
89
104
  'board.update': boards,
90
105
  'board.pin': boards,
@@ -123,13 +138,28 @@ const payloads: Record<string, ZodType> = {
123
138
  'canvas.redo': history,
124
139
  'tour.get': z.looseObject({ tour: tourSchema, derived: z.boolean(), position: count.nullable() }),
125
140
  'tour.set': ok,
126
- 'tour.go': z.looseObject({ ok: z.boolean(), index: count, total: count, stop: tourStopSchema }),
127
- 'tour.exit': ok,
141
+ 'tour.go': z.union([
142
+ z.looseObject({
143
+ ok: z.boolean(),
144
+ catalogEpoch: z.string(),
145
+ boardGeneration: count,
146
+ sequence: count,
147
+ index: count,
148
+ total: count,
149
+ stop: tourStopSchema,
150
+ }),
151
+ tourSuperseded,
152
+ ]),
153
+ 'tour.exit': z.union([
154
+ z.looseObject({ ok: z.boolean(), catalogEpoch: z.string(), boardGeneration: count, sequence: count }),
155
+ tourSuperseded,
156
+ ]),
128
157
  'camera.move': z.looseObject({ ok: z.boolean(), stop: tourStopSchema, viewport }),
129
158
  arrange: z.looseObject({ ok: z.boolean(), layout: z.enum(['grid', 'column', 'flow']) }),
130
159
  'node.focus': z.looseObject({ ok: z.boolean(), focused: id, panned: z.boolean() }),
131
160
  'view.fit': z.looseObject({ ok: z.boolean(), viewport }),
132
161
  'canvas.clear': z.looseObject({ ok: z.boolean(), cleared: z.literal(true) }),
162
+ 'annotation.add': entity('annotation'),
133
163
  'annotation.remove': ok,
134
164
  'context.get': z.looseObject({ version: z.literal(1), budgetUnit: z.literal('utf16-code-units') }),
135
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
  };
@@ -289,7 +289,17 @@ const axTimelineGetOperation = defineOperation<z.infer<typeof axTimelineGetSchem
289
289
  const axDeliveryPendingShape = {
290
290
  consumer: z.unknown().optional().describe('Consumer/source label to exclude from results (e.g. copilot, mcp).'),
291
291
  limit: z.unknown().optional().describe('Max steering messages to return.'),
292
+ offset: z.coerce
293
+ .number()
294
+ .int()
295
+ .nonnegative()
296
+ .optional()
297
+ .describe('Skip this many matching pending messages. Restart at zero after an empty page.'),
292
298
  order: z.unknown().optional().describe('"oldest" (FIFO, default) or "newest" first.'),
299
+ addressedOnly: z
300
+ .boolean()
301
+ .optional()
302
+ .describe('Only messages addressed to consumer, excluding broadcasts before applying the limit.'),
293
303
  timeoutMs: z
294
304
  .unknown()
295
305
  .optional()
@@ -310,10 +320,13 @@ const axDeliveryPendingOperation = defineOperation<z.infer<typeof axDeliveryPend
310
320
  // consumer arrives or the timeout elapses. Custom readInput replaces the
311
321
  // default query merge, so re-read the standard params here too.
312
322
  readInput: (_req, params, url) => {
313
- const raw = Number(url.searchParams.get('waitMs') ?? '');
323
+ const raw = Number(url.searchParams.get('timeoutMs') ?? url.searchParams.get('waitMs') ?? '');
314
324
  return {
315
325
  ...Object.fromEntries(url.searchParams),
316
326
  ...params,
327
+ ...(url.searchParams.has('addressedOnly')
328
+ ? { addressedOnly: url.searchParams.get('addressedOnly') === 'true' }
329
+ : {}),
317
330
  timeoutMs: Number.isFinite(raw) && raw > 0 ? raw : 0,
318
331
  };
319
332
  },
@@ -353,7 +366,13 @@ const axDeliveryPendingOperation = defineOperation<z.infer<typeof axDeliveryPend
353
366
  // can't bury the human's current action behind stale undelivered rows. Both
354
367
  // queries apply the same loop-safe consumer filter before the limit.
355
368
  const newest = input.order === 'newest';
356
- const scope = { ...(consumer ? { consumer } : {}), ...(limit ? { limit } : {}) };
369
+ if (input.addressedOnly && !consumer) throw new OperationError('addressedOnly requires consumer.', 400);
370
+ const scope = {
371
+ ...(consumer ? { consumer } : {}),
372
+ ...(limit ? { limit } : {}),
373
+ addressedOnly: input.addressedOnly,
374
+ offset: input.offset,
375
+ };
357
376
  const readPending = () =>
358
377
  newest ? canvasState.getPendingSteeringForContext(scope) : canvasState.getPendingSteering(scope);
359
378
  let pending = readPending();
@@ -495,6 +514,12 @@ const axReadsListOperation = defineOperation<z.infer<typeof axReadsListSchema>,
495
514
  });
496
515
 
497
516
  const axReadsRecordShape = {
517
+ deliveryId: z
518
+ .string()
519
+ .min(1)
520
+ .max(200)
521
+ .optional()
522
+ .describe('Idempotent adapter receipt ID, scoped to source, consumer and board.'),
498
523
  channel: z.enum(CONTEXT_READ_CHANNELS).describe('How the read reached the agent.'),
499
524
  resource: z.string().min(1).describe('Resource URI or operation name that was read.'),
500
525
  consumer: z.string().nullable().optional().describe('Reader identity (MCP client name, adapter host).'),
@@ -532,6 +557,7 @@ const axReadsRecordOperation = defineOperation<z.infer<typeof axReadsRecordSchem
532
557
  handler: (input) => {
533
558
  const read = canvasState.recordContextRead(
534
559
  {
560
+ deliveryId: input.deliveryId,
535
561
  channel: input.channel,
536
562
  resource: input.resource,
537
563
  source: input.source ?? 'api',
@@ -26,7 +26,8 @@ import { z } from 'zod';
26
26
  import { canvasState } from '../../canvas-state.js';
27
27
  import { serializeCanvasNode, serializeCanvasNodeCompact } from '../../canvas-serialization.js';
28
28
  import { addCanvasNode } from '../../canvas-operations.js';
29
- import { executeOperation, runWithSuppressedEmits } from '../registry.js';
29
+ import { currentActor } from '../../attribution.js';
30
+ import { executeOperation, runWithSuppressedEmits, validateOperationTarget } from '../registry.js';
30
31
  import { defineOperation, type Operation, type OperationContext } from '../types.js';
31
32
  import { readJsonValue } from '../http.js';
32
33
  import { isRecord } from './nodes.js';
@@ -57,6 +58,8 @@ const SUPPORTED_BATCH_OPS = new Set([
57
58
  'group.create',
58
59
  'group.add',
59
60
  'group.remove',
61
+ 'annotation.add',
62
+ 'annotation.remove',
60
63
  'pin.set',
61
64
  'pin.add',
62
65
  'pin.remove',
@@ -224,6 +227,7 @@ function shapeBatchEntry(op: string, result: unknown, creationReceipt = false):
224
227
  async function runBatch(operations: BatchEntry[], ctx: OperationContext): Promise<BatchEnvelope> {
225
228
  const refs: Record<string, unknown> = {};
226
229
  const results: Array<Record<string, unknown>> = [];
230
+ const actor = currentActor('api');
227
231
 
228
232
  for (let index = 0; index < operations.length; index++) {
229
233
  const operation = operations[index]!;
@@ -233,6 +237,7 @@ async function runBatch(operations: BatchEntry[], ctx: OperationContext): Promis
233
237
  }
234
238
  try {
235
239
  ctx.signal?.throwIfAborted();
240
+ validateOperationTarget(operation.op, args);
236
241
  if (ctx.onProgress) {
237
242
  const boardGeneration = canvasState.boardGeneration;
238
243
  await ctx.onProgress({ progress: index, total: operations.length, message: operation.op });
@@ -268,14 +273,19 @@ async function runBatch(operations: BatchEntry[], ctx: OperationContext): Promis
268
273
  // Batch churn is exempt from auto-ghosts (matches the skill's explicit
269
274
  // ghost-cursor exemption for batch operations). No `fromWorkbench`: the
270
275
  // browser never issues batches, so inner writes are agent writes and the
271
- // scope fence applies to each of them.
272
- const pending = executeOperation(dispatch.name, dispatch.args, {
273
- suppressAutoGhost: true,
274
- signal: ctx.signal,
275
- onProgress: ctx.onProgress
276
- ? (progress) => ctx.onProgress!({ progress: index, total: operations.length, message: progress.message })
277
- : undefined,
278
- });
276
+ // scope fence applies to each of them using the original writer identity.
277
+ const pending = executeOperation(
278
+ dispatch.name,
279
+ { agentId: actor.agentId, ...dispatch.args },
280
+ {
281
+ source: actor.source,
282
+ suppressAutoGhost: true,
283
+ signal: ctx.signal,
284
+ onProgress: ctx.onProgress
285
+ ? (progress) => ctx.onProgress!({ progress: index, total: operations.length, message: progress.message })
286
+ : undefined,
287
+ },
288
+ );
279
289
  // The first write may synchronously open a board from Home. Capture after
280
290
  // dispatch, then refuse to continue a partially completed batch elsewhere.
281
291
  const boardGeneration = canvasState.boardGeneration;
@@ -319,6 +329,7 @@ const batchShape = {
319
329
  }),
320
330
  )
321
331
  .describe('Ordered array of batch operations'),
332
+ agentId: z.string().optional().describe('Default writer identity for entries without their own agentId'),
322
333
  full: z
323
334
  .boolean()
324
335
  .optional()
@@ -404,13 +415,16 @@ const batchOperation = defineOperation<z.infer<typeof batchSchema>, BatchEnvelop
404
415
  : isRecord(body) && Array.isArray(body.operations)
405
416
  ? body.operations
406
417
  : [];
407
- return { operations };
418
+ return {
419
+ operations,
420
+ ...(isRecord(body) ? { agentId: body.agentId, targetBoardId: body.targetBoardId } : {}),
421
+ };
408
422
  },
409
423
  },
410
424
  mcp: {
411
425
  toolName: 'canvas_batch',
412
426
  description:
413
- 'Run a non-atomic batch of canvas operations with optional assigned references. Use assign to name a result, then reference it later as "$name" for the created node id or "$name.id" for a specific result field. On failure, earlier successful operations remain applied and the response includes ok:false, failedIndex, error, results, and refs. Supports node.add, node.update, node.remove, graph.add, edge.add, edge.remove, group.create, group.add, group.remove, pin.set/add/remove, snapshot.save, and arrange.',
427
+ 'Run a non-atomic batch of canvas operations with optional assigned references. Use assign to name a result, then reference it later as "$name" for the created node id or "$name.id" for a specific result field. On failure, earlier successful operations remain applied and the response includes ok:false, failedIndex, error, results, and refs. Supports node.add, node.update, node.remove, graph.add, edge.add, edge.remove, group.create, group.add, group.remove, annotation.add, annotation.remove, pin.set/add/remove, snapshot.save, and arrange.',
414
428
  formatResult: (result, input) => {
415
429
  const envelope = result as BatchEnvelope;
416
430
  const payload = wantsFullBatch(input) ? envelope : compactBatchResult(envelope);
@@ -12,12 +12,12 @@
12
12
  *
13
13
  * This module must never import server.ts or index.ts.
14
14
  */
15
- import { randomUUID } from 'node:crypto';
16
15
  import { z } from 'zod';
17
16
  import { normalizeBoardFolder } from '../../../shared/boards.js';
18
17
  import { relationVocabularySchema } from '../../../shared/relations.js';
19
- import { canvasState, type CanvasLayout } from '../../canvas-state.js';
18
+ import { boardCatalogEpoch, canvasState, type CanvasLayout } from '../../canvas-state.js';
20
19
  import { openCanvasBoard } from '../../canvas-operations.js';
20
+ import { rewatchAllFileNodes } from '../../file-watcher.js';
21
21
  import { summarizeNodeForAgentContext } from '../../../shared/agent-context.js';
22
22
  import { BRIEF_SUMMARY_LENGTH } from '../../context-brief.js';
23
23
  import { memoryGraph, boardConnections } from '../../memory-graph.js';
@@ -25,7 +25,6 @@ import { defineOperation, OperationError, type Operation, type OperationContext
25
25
 
26
26
  /** The most cards a Home thumbnail draws (the largest first); the rest are noise at that size. */
27
27
  const PREVIEW_CARDS = 24;
28
- export const boardCatalogEpoch = randomUUID();
29
28
  let catalogRevision = 0;
30
29
  let reloadRevision = 0;
31
30
 
@@ -68,8 +67,8 @@ export function boardsPayload(reloaded = false): Record<string, unknown> {
68
67
  catalogEpoch: boardCatalogEpoch,
69
68
  catalogRevision: revision,
70
69
  reloadRevision,
71
- boardGeneration: canvasState.boardGeneration,
72
- activeBoardId: canvasState.activeBoardId,
70
+ boardGeneration: canvasState.visibleBoardGeneration,
71
+ activeBoardId: canvasState.visibleBoardId,
73
72
  boards: boards.map((board) => {
74
73
  const read = canvasState.readBoard(board.id, true);
75
74
  const readme = read?.layout.nodes.find((node) => node.id === board.readmeNodeId && node.type === 'markdown');
@@ -218,7 +217,8 @@ const boardGetOperation = defineOperation<z.infer<typeof getSchema>, Record<stri
218
217
  const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : canvasState.activeBoardId;
219
218
  const board = id ? canvasState.listBoards().find((entry) => entry.id === id) : undefined;
220
219
  if (!board) throw new OperationError(id ? `Board "${id}" not found.` : 'No board is open.', 404);
221
- return { ok: true, board, open: board.id === canvasState.activeBoardId };
220
+ const contentRevision = canvasState.readBoard(board.id, false)!.state.revisionState?.revision ?? 0;
221
+ return { ok: true, board, contentRevision, open: board.id === canvasState.activeBoardId };
222
222
  },
223
223
  });
224
224
 
@@ -406,6 +406,44 @@ const boardCreateFromOperation = defineOperation<z.infer<typeof fromSchema>, Rec
406
406
  },
407
407
  });
408
408
 
409
+ const moveShape = {
410
+ sourceBoardId: z.string().min(1),
411
+ nodeIds: z.array(z.string().min(1)).min(1),
412
+ name: z.string().trim().min(1).max(120),
413
+ expectedRevision: z.number().int().nonnegative(),
414
+ requestId: z.string().trim().min(1).max(200),
415
+ preview: z.boolean().optional(),
416
+ };
417
+ const moveSchema = z.object(moveShape);
418
+ const boardMoveOperation = defineOperation<z.infer<typeof moveSchema>, Record<string, unknown>>({
419
+ name: 'board.move-selection',
420
+ // Own validation precedes activation, and preview must never emit mutation/intent events.
421
+ mutates: false,
422
+ input: moveSchema,
423
+ inputShape: moveShape,
424
+ http: { method: 'POST', path: '/api/canvas/boards/move-selection' },
425
+ mcp: {
426
+ toolName: 'canvas_move_selection_to_board',
427
+ description:
428
+ 'Preview or atomically move selected cards/group descendants to a new board, preserving identities and leaving a portal. Requires explicit sourceBoardId, expectedRevision and a durable requestId. Retry with identical input; never opens the destination. Preview lists unsafe bindings and the Undo/snapshot boundary.',
429
+ formatResult: jsonResult,
430
+ },
431
+ handler: (input, ctx) => {
432
+ const outcome = canvasState.moveSelectionToNewBoard(input);
433
+ if ('preview' in outcome) return { ok: true, ...outcome };
434
+ if (!outcome.replayed) {
435
+ rewatchAllFileNodes();
436
+ emitBoardsChanged(ctx);
437
+ ctx.emit('context-pins-changed', {
438
+ count: canvasState.contextPinnedNodeIds.size,
439
+ nodeIds: [...canvasState.contextPinnedNodeIds],
440
+ });
441
+ ctx.emit('ax-state-changed', {});
442
+ }
443
+ return { ok: true, ...outcome.result, replayed: outcome.replayed };
444
+ },
445
+ });
446
+
409
447
  // ── board.pin / board.unpin ───────────────────────────────────
410
448
  // A pinned board joins the agent's working set: its README and pinned cards
411
449
  // travel in the brief whichever board is open. Pinning is context, never a
@@ -492,6 +530,7 @@ export const boardOperations: Operation[] = [
492
530
  boardGetOperation,
493
531
  boardCreateOperation,
494
532
  boardCreateFromOperation,
533
+ boardMoveOperation,
495
534
  boardUpdateOperation,
496
535
  boardPinOperation,
497
536
  boardUnpinOperation,