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
@@ -32,6 +32,7 @@ import {
32
32
  } from '../../canvas-operations.js';
33
33
  import { resolveCreateGeometry, resolvePatchGeometry, validateCanvasNodePatch } from '../../canvas-geometry.js';
34
34
  import { normalizeNodeAxCapabilities } from '../../ax-interaction.js';
35
+ import { isWorkCardState } from '../../../shared/ax-kinds.js';
35
36
  import { applyFileContentToNodeData, readFileNodeContent } from '../../file-content.js';
36
37
  import { unwatchFileForNode, watchFileForNode } from '../../file-watcher.js';
37
38
  import {
@@ -327,6 +328,21 @@ export function createdNodePayloadFromNode(
327
328
 
328
329
  // ── Operation cores (the SDK wraps these directly) ────────────
329
330
 
331
+ function validateWorkCardData(data: Record<string, unknown> | undefined): void {
332
+ if (!data || data.workState === undefined) return;
333
+ if (!isWorkCardState(data.workState))
334
+ throw new OperationError('workState must be todo, in-progress, blocked, done or cancelled.');
335
+ if (data.detail !== undefined && typeof data.detail !== 'string')
336
+ throw new OperationError('Work card detail must be a string.');
337
+ if (
338
+ data.progress !== undefined &&
339
+ data.progress !== null &&
340
+ (typeof data.progress !== 'number' || !Number.isFinite(data.progress) || data.progress < 0 || data.progress > 100)
341
+ ) {
342
+ throw new OperationError('Work card progress must be a number from 0 to 100, or null to clear it.');
343
+ }
344
+ }
345
+
330
346
  /**
331
347
  * Create a basic (non-webpage / non-group / non-primitive) node. Union of the
332
348
  * legacy handleCanvasAddNode generic branch; the SDK passes fileMode 'path',
@@ -338,6 +354,34 @@ export function createBasicCanvasNode(
338
354
  ): { node: CanvasNodeState; needsCodeGraphRecompute: boolean; sizeAdjustment?: CanvasSizeAdjustment } {
339
355
  const type = typeof body.type === 'string' ? body.type : '';
340
356
  const extraData = isRecord(body.data) ? body.data : undefined;
357
+ if (type === 'status' && extraData?.workItemId !== undefined) {
358
+ if (body.boardId !== undefined || options.prepareOnlyNodes)
359
+ throw new OperationError('Live work cards must be created on the active board.');
360
+ const owner = extraData.workItemBoardId ?? canvasState.activeBoardId;
361
+ if (
362
+ typeof extraData.workItemId !== 'string' ||
363
+ (owner !== null && typeof owner !== 'string') ||
364
+ !(owner === null ? canvasState.getWorkItems() : canvasState.getWorkItemsOnBoard(owner)).some(
365
+ (item) => item.id === extraData.workItemId,
366
+ )
367
+ )
368
+ throw new OperationError('Live work card requires an existing work item on its owning board.');
369
+ if (
370
+ ['title', 'content'].some((key) => body[key] !== undefined) ||
371
+ [
372
+ 'title',
373
+ 'content',
374
+ 'workState',
375
+ 'detail',
376
+ 'progress',
377
+ 'axWorkStatus',
378
+ 'liveWorkAvailable',
379
+ 'liveWorkSource',
380
+ ].some((key) => key in extraData)
381
+ )
382
+ throw new OperationError('Live work card fields are read-only; update the work item instead.');
383
+ }
384
+ if (type === 'status') validateWorkCardData(extraData);
341
385
  if (
342
386
  extraData &&
343
387
  ('attachmentId' in extraData || (isRecord(extraData.source) && 'attachmentId' in extraData.source))
@@ -415,6 +459,9 @@ export function createBasicCanvasNode(
415
459
  : {}),
416
460
  ...(typeof content === 'string' ? { content } : {}),
417
461
  ...(htmlMergedData && Object.keys(htmlMergedData).length > 0 ? { data: htmlMergedData } : {}),
462
+ ...(type === 'status' && typeof extraData?.workItemId === 'string'
463
+ ? { data: { ...extraData, workItemBoardId: extraData.workItemBoardId ?? canvasState.activeBoardId } }
464
+ : {}),
418
465
  ...(type === 'trace' && typeof body.toolName === 'string' ? { toolName: body.toolName } : {}),
419
466
  ...(type === 'trace' && typeof body.category === 'string' ? { category: body.category } : {}),
420
467
  ...(type === 'trace' && typeof body.status === 'string' ? { status: body.status } : {}),
@@ -446,6 +493,28 @@ export function buildNodePatch(
446
493
  body: Record<string, unknown>,
447
494
  ): { patch: Partial<CanvasNodeState>; groupChildIds?: string[] } {
448
495
  if (body.type !== undefined && body.type !== existing.type) throw new OperationError('Node type is immutable.');
496
+ if (existing.type === 'status') {
497
+ const data = isRecord(body.data) ? body.data : {};
498
+ if (
499
+ 'workItemId' in data ||
500
+ 'workItemBoardId' in data ||
501
+ (typeof existing.data.workItemId === 'string' &&
502
+ (body.title !== undefined ||
503
+ body.content !== undefined ||
504
+ [
505
+ 'title',
506
+ 'content',
507
+ 'workState',
508
+ 'detail',
509
+ 'progress',
510
+ 'axWorkStatus',
511
+ 'liveWorkAvailable',
512
+ 'liveWorkSource',
513
+ ].some((key) => key in data)))
514
+ ) {
515
+ throw new OperationError('Live work cards are read-only; create a presentation or update the work item instead.');
516
+ }
517
+ }
449
518
  if (
450
519
  existing.type === 'file' &&
451
520
  typeof existing.data.attachmentId === 'string' &&
@@ -561,6 +630,7 @@ export function buildNodePatch(
561
630
  }
562
631
  patch.data = existing.type === 'trace' ? mergeTraceNodeDataFields(data, body) : data;
563
632
  }
633
+ if (existing.type === 'status' && isRecord(patch.data)) validateWorkCardData(patch.data);
564
634
  const error = validateCanvasNodePatch({
565
635
  ...(patch.position ? { position: patch.position as { x: number; y: number } } : {}),
566
636
  ...(patch.size ? { size: patch.size as { width: number; height: number } } : {}),
@@ -284,6 +284,11 @@ const contextShape = {
284
284
  consumer: z.unknown().optional().describe('Stable consumer id used for the durable board-scoped cursor'),
285
285
  since: z.unknown().optional().describe('Explicit content revision; bypasses the durable consumer cursor'),
286
286
  budget: z.unknown().optional().describe('Maximum response size in UTF-16 code units'),
287
+ contextDelivery: z
288
+ .boolean()
289
+ .optional()
290
+ .describe('False prepares a brief without recording delivery or advancing a cursor.'),
291
+ includeSteering: z.boolean().optional().describe('False omits steering for hosts that deliver it separately.'),
287
292
  };
288
293
  const contextSchema = z.looseObject(contextShape);
289
294
 
@@ -292,8 +297,20 @@ const contextOperation = defineOperation<z.infer<typeof contextSchema>, Record<s
292
297
  mutates: false,
293
298
  input: contextSchema,
294
299
  inputShape: contextShape,
295
- http: { method: 'GET', path: '/api/canvas/context' },
300
+ http: {
301
+ method: 'GET',
302
+ path: '/api/canvas/context',
303
+ readInput: (_req, params, url) => {
304
+ const input: Record<string, unknown> = { ...Object.fromEntries(url.searchParams), ...params };
305
+ for (const key of ['contextDelivery', 'includeSteering']) {
306
+ if (input[key] === 'false') input[key] = false;
307
+ else if (input[key] === 'true') input[key] = true;
308
+ }
309
+ return input;
310
+ },
311
+ },
296
312
  handler: (input) => {
313
+ canvasState.refreshContextWorkCards();
297
314
  const active = canvasState.getActiveBoard();
298
315
  if (!active) throw new OperationError('Open a board before requesting its context.', 409);
299
316
  const consumer =
@@ -507,7 +524,10 @@ const contextOperation = defineOperation<z.infer<typeof contextSchema>, Record<s
507
524
  })),
508
525
  );
509
526
  entries.push(
510
- ...canvasState.getPendingSteering({ consumer: consumer ?? undefined, limit: 50 }).map((item) => ({
527
+ ...(input.includeSteering === false
528
+ ? []
529
+ : canvasState.getPendingSteering({ consumer: consumer ?? undefined, limit: 50 })
530
+ ).map((item) => ({
511
531
  sourceBoardId: active.id,
512
532
  nodeId: `steering:${item.id}`,
513
533
  reason: 'steer' as const,
@@ -553,7 +573,14 @@ const contextOperation = defineOperation<z.infer<typeof contextSchema>, Record<s
553
573
  entries,
554
574
  budget,
555
575
  });
556
- if (consumer && !explicitSince && result.document && result.nextCursor !== null && !result.invalidCursor) {
576
+ if (
577
+ input.contextDelivery !== false &&
578
+ consumer &&
579
+ !explicitSince &&
580
+ result.document &&
581
+ result.nextCursor !== null &&
582
+ !result.invalidCursor
583
+ ) {
557
584
  canvasState.advanceContextBriefCursor(active.id, consumer, result.nextCursor);
558
585
  }
559
586
  return (result.document ?? {
@@ -634,6 +661,7 @@ const undoOperation = defineOperation<z.infer<typeof undoRedoSchema>, Record<str
634
661
  count: canvasState.contextPinnedNodeIds.size,
635
662
  nodeIds: Array.from(canvasState.contextPinnedNodeIds),
636
663
  });
664
+ ctx.emit('ax-state-changed', {});
637
665
  return { ok: true, description: `Undid: ${entry.description}` };
638
666
  },
639
667
  });
@@ -662,6 +690,7 @@ const redoOperation = defineOperation<z.infer<typeof undoRedoSchema>, Record<str
662
690
  count: canvasState.contextPinnedNodeIds.size,
663
691
  nodeIds: Array.from(canvasState.contextPinnedNodeIds),
664
692
  });
693
+ ctx.emit('ax-state-changed', {});
665
694
  return { ok: true, description: `Redid: ${entry.description}` };
666
695
  },
667
696
  });
@@ -364,6 +364,7 @@ const snapshotRestoreNodeOperation = defineOperation<
364
364
  handler: ({ id, nodeId, preserveHumanEdits }) => {
365
365
  const snapshot = canvasState.getSnapshotData(id ?? '');
366
366
  if (!snapshot) throw new OperationError('Snapshot not found', 404);
367
+ canvasState.assertSnapshotRestorable(id ?? '');
367
368
  const current = canvasState.getNode(nodeId ?? '');
368
369
  const earlier = snapshot.nodes.find((node) => node.id === nodeId);
369
370
  if (!current || !earlier)
@@ -13,7 +13,7 @@
13
13
  * This module must never import server.ts or index.ts.
14
14
  */
15
15
  import { z } from 'zod';
16
- import { tourSchema, rectSchema, cameraSchema, tourStopSchema } from '../../../shared/tour.js';
16
+ import { tourSchema, rectSchema, cameraSchema, tourStopSchema, tourPresenterSchema } from '../../../shared/tour.js';
17
17
  import { canvasState } from '../../canvas-state.js';
18
18
  import {
19
19
  arrangeCanvasNodes,
@@ -339,6 +339,7 @@ const tourGetOperation = defineOperation({
339
339
  const tourGoShape = {
340
340
  stop: z.number().int().nonnegative().optional().describe('Zero-based stop index to go to'),
341
341
  step: z.enum(['next', 'previous']).optional().describe('Step relative to the current stop (clamped at the ends)'),
342
+ presenter: tourPresenterSchema.optional().describe('Document intent order and expected board activation'),
342
343
  present: z
343
344
  .boolean()
344
345
  .optional()
@@ -360,23 +361,24 @@ const tourGoOperation = defineOperation({
360
361
  if ((input.stop === undefined) === (input.step === undefined)) {
361
362
  throw new OperationError('Give exactly one of stop (index) or step ("next" | "previous").');
362
363
  }
363
- const result = goToTourStop(input.stop ?? (input.step as 'next' | 'previous'));
364
- ctx.emit('canvas-tour-step', { ...result, present: input.present !== false });
364
+ const result = goToTourStop(input.stop ?? (input.step as 'next' | 'previous'), input.presenter);
365
+ if (!('superseded' in result)) ctx.emit('canvas-tour-step', { ...result, present: input.present !== false });
365
366
  return { ok: true, ...result };
366
367
  },
367
368
  });
368
369
 
370
+ const tourExitShape = { presenter: tourGoShape.presenter };
369
371
  const tourExitOperation = defineOperation({
370
372
  name: 'tour.exit',
371
373
  mutates: false,
372
- input: z.object({}),
373
- inputShape: {},
374
+ input: z.object(tourExitShape),
375
+ inputShape: tourExitShape,
374
376
  http: { method: 'POST', path: '/api/canvas/tour/exit' },
375
377
  mcp: { toolName: 'canvas_tour_exit', description: 'Leave presentation in every viewer.' },
376
- handler: (_input, ctx) => {
377
- exitTour();
378
- ctx.emit('canvas-tour-exit', {});
379
- return { ok: true };
378
+ handler: (input, ctx) => {
379
+ const result = exitTour(input.presenter);
380
+ if (!('superseded' in result)) ctx.emit('canvas-tour-exit', { ...result });
381
+ return { ok: true, ...result };
380
382
  },
381
383
  });
382
384
 
@@ -359,6 +359,7 @@ function notePresence(
359
359
  ): void {
360
360
  if (!isPresenceWrite(op) || meta.suppressAutoGhost) return;
361
361
  const input = asRecord(rawInput);
362
+ if (name === 'board.move-selection' && (input.preview === true || asRecord(result).replayed === true)) return;
362
363
  // Delivery plumbing (claim/mark) must not book an anonymous transport
363
364
  // writer nor spam the timeline: attribute it to the consumer that marked,
364
365
  // and record no activity row — the Steer row flipping to delivered IS the
@@ -388,11 +389,52 @@ function notePresence(
388
389
  }
389
390
  }
390
391
 
392
+ const operationTarget = new AsyncLocalStorage<{ boardId: string; agentId?: string }>();
393
+
394
+ /** Also used by batch's internal node path, which does not dispatch an operation. */
395
+ export function validateOperationTarget(name: string, input: Record<string, unknown>): string | undefined {
396
+ const inherited = operationTarget.getStore();
397
+ const target = input.targetBoardId === undefined ? inherited?.boardId : input.targetBoardId;
398
+ if (target === undefined) return undefined;
399
+ if (typeof target !== 'string' || !target.trim())
400
+ throw new OperationError('targetBoardId must be an existing board ID.');
401
+ if (
402
+ inherited &&
403
+ (target !== inherited.boardId || (input.agentId !== undefined && input.agentId !== inherited.agentId))
404
+ )
405
+ throw new OperationError('Nested operations must retain their board and agent identity.', 409);
406
+ if (name === 'board.update' && input.id !== target)
407
+ throw new OperationError('Board id conflicts with targetBoardId. Rebind before editing another board.', 409);
408
+ for (const key of [
409
+ 'board',
410
+ 'expectedBoardId',
411
+ 'destinationBoardId',
412
+ ...(name === 'board.move-selection' ? ['sourceBoardId'] : []),
413
+ ]) {
414
+ if (input[key] !== undefined && input[key] !== target)
415
+ throw new OperationError(`${key} conflicts with targetBoardId. Rebind before targeting another board.`, 409);
416
+ }
417
+ if (['board.open', 'board.delete', 'backup.restore'].includes(name) || name.startsWith('webview.'))
418
+ throw new OperationError('This operation controls the shared view or workspace, not a scoped board.', 409);
419
+ return target;
420
+ }
421
+
391
422
  export async function executeOperation(
392
423
  name: string,
393
424
  rawInput: unknown,
394
425
  meta: ExecuteOperationMeta = {},
395
426
  ): Promise<unknown> {
427
+ const input = asRecord(rawInput);
428
+ const target = validateOperationTarget(name, input);
429
+ if (target === undefined) return executeOnCurrentBoard(name, rawInput, meta);
430
+ const agentId =
431
+ operationTarget.getStore()?.agentId ?? (typeof input.agentId === 'string' ? input.agentId : undefined);
432
+ return operationTarget.run({ boardId: target, agentId }, () =>
433
+ canvasState.withBoard(target, () => executeOnCurrentBoard(name, { ...input, agentId }, meta)),
434
+ );
435
+ }
436
+
437
+ async function executeOnCurrentBoard(name: string, rawInput: unknown, meta: ExecuteOperationMeta): Promise<unknown> {
396
438
  // Check before attribution, board auto-open, ghosts, or any handler writes.
397
439
  meta.signal?.throwIfAborted();
398
440
  // Attribution follows this call chain across awaits and nested operations.
@@ -406,7 +448,8 @@ export async function executeOperation(
406
448
  async () => {
407
449
  const input = asRecord(rawInput);
408
450
  const deliversContext =
409
- name !== 'node.get' || (input.contextDelivery !== false && input.contextDelivery !== 'false');
451
+ !['node.get', 'context.get'].includes(name) ||
452
+ (input.contextDelivery !== false && input.contextDelivery !== 'false');
410
453
  const recordRead = CONTEXT_READ_OPS.has(name) && deliversContext && !meta.fromWorkbench && !meta.proxiedRead;
411
454
  const requestedBoard =
412
455
  recordRead && 'board' in getOperation(name).inputShape && isString(input.board) && input.board.trim()
@@ -510,7 +553,8 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
510
553
  const rawExpectedBoard = asRecord(rawInput).expectedBoardId;
511
554
  // HTTP DELETE query values are strings, including Home's JSON null.
512
555
  const expectedBoard = rawExpectedBoard === 'null' ? null : rawExpectedBoard;
513
- if (expectedBoard !== undefined && expectedBoard !== canvasState.activeBoardId) {
556
+ // Relocations check their durable receipt first, then their explicit source board.
557
+ if (name !== 'board.move-selection' && expectedBoard !== undefined && expectedBoard !== canvasState.activeBoardId) {
514
558
  throw new OperationError('The active board changed. Refresh before retrying this action.', 409);
515
559
  }
516
560
  // File drops carry their captured board through the HTTP round trip. Check
@@ -638,8 +682,9 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
638
682
  }
639
683
 
640
684
  const beforeBoard = canvasState.activeBoardId;
641
- const beforeRevision =
642
- name === 'board.create-from' || name === 'board.delete' ? canvasState.getContentRevision().revision : null;
685
+ const beforeRevision = ['board.create-from', 'board.delete', 'board.move-selection'].includes(name)
686
+ ? canvasState.getContentRevision().revision
687
+ : null;
643
688
  meta.signal?.throwIfAborted();
644
689
  const result = await op.execute(rawInput, operationContext);
645
690
  // Library writes can invalidate relations on the open board without being
@@ -47,8 +47,8 @@ import { basename, extname, isAbsolute, join, relative, resolve, sep } from 'nod
47
47
  import * as marked from 'marked';
48
48
  import { renderWikiMarkdown, resolveWikiLinks, wikiReferences } from './wiki-links.js';
49
49
  import { wikiLinksFromData } from '../shared/memory-graph.js';
50
- import { type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
51
- import { boardCatalogEpoch, boardsPayload } from './operations/ops/boards.js';
50
+ import { type CanvasNodeState, IMAGE_MIME_MAP, boardCatalogEpoch, canvasState } from './canvas-state.js';
51
+ import { boardsPayload } from './operations/ops/boards.js';
52
52
  import {
53
53
  buildAxBridge,
54
54
  buildAxStateBridge,
@@ -72,16 +72,16 @@ import { buildAgentContextPreamble } from '../shared/agent-context.js';
72
72
  import { buildCanvasAxSurfaceSnapshot } from './ax-context.js';
73
73
  import { resolveNodeAxCapabilities } from './ax-interaction.js';
74
74
  import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
75
- import { canvasThemeScheme, isCanvasTheme } from '../shared/themes.js';
75
+ import { isCanvasTheme } from '../shared/themes.js';
76
76
  import { canOpenNodeAsSurface } from '../shared/surface.js';
77
77
  import { validateLocalImageFile } from './image-source.js';
78
78
  import {
79
79
  addCanvasNode,
80
80
  cancelCodeGraphRecompute,
81
- primeCanvasRuntimeBackends,
82
81
  setCanvasLayoutUpdateEmitter,
83
- syncCanvasRuntimeBackends,
82
+ startBoardRuntime,
84
83
  } from './canvas-operations.js';
84
+ import { createBoardLocal } from './board-local.js';
85
85
  import { MAX_ATTACHMENT_BYTES, MAX_ATTACHMENT_NAME_LENGTH } from './document-import.js';
86
86
  import { dispatchOperationRoute, setOperationEventEmitter } from './operations/index.js';
87
87
  import { intentRegistry } from './intent-registry.js';
@@ -105,7 +105,10 @@ const DEFAULT_PORT = 4313;
105
105
 
106
106
  let server: ReturnType<typeof Bun.serve> | null = null;
107
107
  let activeWorkspaceRoot = resolve(process.cwd());
108
- let primaryWorkbenchPath: string | null = null;
108
+ const boardReplay = createBoardLocal(() => ({
109
+ path: null as string | null,
110
+ contextCards: null as Record<string, unknown> | null,
111
+ }));
109
112
  let primaryWorkbenchSessionId = `pmx-${Date.now().toString(36)}`;
110
113
  let nextWorkbenchEventId = 1;
111
114
  let nextWorkbenchSubscriberId = 1;
@@ -113,7 +116,6 @@ const workbenchSubscribers = new Map<number, ReadableStreamDefaultController<Uin
113
116
  const textEncoder = new TextEncoder();
114
117
  let primaryWorkbenchAutoOpenEnabled = true;
115
118
  const initialCanvasThemeSetting = normalizeCanvasTheme(process.env.PMX_CANVAS_THEME);
116
- let lastWorkbenchContextCardsEnvelope: Record<string, unknown> | null = null;
117
119
 
118
120
  // Operation-registry SSE wiring (plan-005): the registry never imports this
119
121
  // module — the workbench event emitter is injected here, mirroring the
@@ -941,6 +943,21 @@ export function onWorkbenchEvent(listener: (event: string) => void): () => void
941
943
  }
942
944
 
943
945
  function broadcastWorkbenchEvent(event: string, payload: PrimaryWorkbenchEventPayload): void {
946
+ if (canvasState.isBackgroundBoard) {
947
+ if (event === 'canvas-layout-update' || event === 'boards-changed')
948
+ canvasState.withVisibleBoard(() => broadcastWorkbenchEvent('boards-changed', boardsPayload()));
949
+ else if (event === 'ax-state-changed') {
950
+ const ownerBoardId = canvasState.activeBoardId;
951
+ canvasState.withVisibleBoard(() => {
952
+ const layout = canvasState.getLayout();
953
+ if (layout.nodes.some((node) => node.type === 'status' && node.data.workItemBoardId === ownerBoardId))
954
+ broadcastWorkbenchEvent('canvas-layout-update', { layout });
955
+ });
956
+ } else if (event === 'agent-presence')
957
+ canvasState.withVisibleBoard(() => broadcastWorkbenchEvent(event, { ...payload, ...agentPresence.snapshot() }));
958
+ return;
959
+ }
960
+ payload = { ...payload, boardId: canvasState.visibleBoardId };
944
961
  let remoteEdgesChanged = false;
945
962
  if (event === 'canvas-layout-update') {
946
963
  // Remote footer titles come from the catalog. Refresh it on endpoint changes
@@ -960,7 +977,7 @@ function broadcastWorkbenchEvent(event: string, payload: PrimaryWorkbenchEventPa
960
977
  payload = {
961
978
  ...payload,
962
979
  catalogEpoch: boardCatalogEpoch,
963
- boardGeneration: canvasState.boardGeneration,
980
+ boardGeneration: canvasState.visibleBoardGeneration,
964
981
  boardId: canvasState.activeBoardId,
965
982
  };
966
983
  }
@@ -990,8 +1007,8 @@ function broadcastWorkbenchEvent(event: string, payload: PrimaryWorkbenchEventPa
990
1007
 
991
1008
  function setPrimaryWorkbenchPath(safePath: string, source: string): void {
992
1009
  const resolved = resolve(safePath);
993
- if (primaryWorkbenchPath === resolved) return;
994
- primaryWorkbenchPath = resolved;
1010
+ if (boardReplay.current.path === resolved) return;
1011
+ boardReplay.current.path = resolved;
995
1012
  broadcastWorkbenchEvent('workbench-open', {
996
1013
  path: resolved,
997
1014
  title: basename(resolved),
@@ -1504,44 +1521,6 @@ function handleIframeProbe(): Response {
1504
1521
  );
1505
1522
  }
1506
1523
 
1507
- // ── Ext-app recovery diagnostics (0.3.2 report Finding N) ──────
1508
- //
1509
- // The WebKit black-tile recovery trail is otherwise trapped inside host panels
1510
- // without devtools (the Copilot WKWebView). Clients mirror their bounded
1511
- // recovery log here; agents/testers read it back over HTTP. In-memory only,
1512
- // diagnostics-grade: no persistence, no snapshots.
1513
- const EXT_APP_RECOVERY_LOG_MAX = 500;
1514
- const EXT_APP_RECOVERY_FIELD_MAX = 300;
1515
- const extAppRecoveryEntries: Array<Record<string, unknown>> = [];
1516
-
1517
- /**
1518
- * Clamp a reported entry to the known trail shape ({ t, nodeId, event,
1519
- * visibility }) with bounded string lengths. The count trim below caps the
1520
- * number of entries; this caps bytes per entry, so the in-memory log stays
1521
- * diagnostics-sized no matter what a client posts.
1522
- */
1523
- function clampExtAppRecoveryEntry(entry: Record<string, unknown>): Record<string, unknown> {
1524
- const clamped: Record<string, unknown> = {};
1525
- if (typeof entry.t === 'number' && Number.isFinite(entry.t)) clamped.t = entry.t;
1526
- for (const key of ['nodeId', 'event', 'visibility'] as const) {
1527
- const value = entry[key];
1528
- if (typeof value === 'string') clamped[key] = value.slice(0, EXT_APP_RECOVERY_FIELD_MAX);
1529
- }
1530
- return clamped;
1531
- }
1532
-
1533
- function handleExtAppRecoveryReport(entries: unknown): Response {
1534
- if (!Array.isArray(entries)) return responseJson({ ok: false, error: 'entries must be an array' }, 400);
1535
- for (const entry of entries.slice(0, 100)) {
1536
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
1537
- extAppRecoveryEntries.push(clampExtAppRecoveryEntry(entry as Record<string, unknown>));
1538
- }
1539
- if (extAppRecoveryEntries.length > EXT_APP_RECOVERY_LOG_MAX) {
1540
- extAppRecoveryEntries.splice(0, extAppRecoveryEntries.length - EXT_APP_RECOVERY_LOG_MAX);
1541
- }
1542
- return responseJson({ ok: true, total: extAppRecoveryEntries.length });
1543
- }
1544
-
1545
1524
  // ── Node surfaces ("Open as site") ─────────────────────────────
1546
1525
  //
1547
1526
  // One stable, node-addressable URL — /api/canvas/surface/:nodeId — that serves
@@ -1841,14 +1820,7 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
1841
1820
  );
1842
1821
 
1843
1822
  const themeValue = url.searchParams.get('theme');
1844
- // The viewer bundle themes dark/light/high-contrast directly; other named
1845
- // canvas themes (midnight, sepia, …) collapse to their dark/light scheme.
1846
- const theme =
1847
- themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast'
1848
- ? themeValue
1849
- : isCanvasTheme(themeValue)
1850
- ? canvasThemeScheme(themeValue)
1851
- : undefined;
1823
+ const theme = themeValue === 'dark' || themeValue === 'light' || isCanvasTheme(themeValue) ? themeValue : 'harbor';
1852
1824
  const title = (node.data.title as string) || node.id;
1853
1825
  // Devtools panel is double-gated: the operator must opt in via the env flag
1854
1826
  // AND the request must carry ?devtools=1. Off by default in all normal runs.
@@ -2118,6 +2090,7 @@ function handleRead(pathLike: string): Response {
2118
2090
 
2119
2091
  function handleWorkbenchState(): Response {
2120
2092
  const mcpAppHost = getMcpAppHostSnapshot();
2093
+ const primaryWorkbenchPath = boardReplay.current.path;
2121
2094
  if (!primaryWorkbenchPath) {
2122
2095
  return responseJson({
2123
2096
  sessionId: primaryWorkbenchSessionId,
@@ -2130,7 +2103,7 @@ function handleWorkbenchState(): Response {
2130
2103
 
2131
2104
  const safePath = resolveWorkspaceMarkdownPath(primaryWorkbenchPath);
2132
2105
  if (!safePath || !existsSync(safePath)) {
2133
- primaryWorkbenchPath = null;
2106
+ boardReplay.current.path = null;
2134
2107
  return responseJson({
2135
2108
  sessionId: primaryWorkbenchSessionId,
2136
2109
  path: null,
@@ -2247,6 +2220,7 @@ async function handleWorkbenchIntent(req: Request): Promise<Response> {
2247
2220
  function buildWorkbenchConnectSnapshot(
2248
2221
  requestedSessionId: string,
2249
2222
  ): Array<{ event: string; payload: Record<string, unknown> }> {
2223
+ const { path: primaryWorkbenchPath, contextCards: lastWorkbenchContextCardsEnvelope } = boardReplay.current;
2250
2224
  const continuity =
2251
2225
  requestedSessionId.length === 0
2252
2226
  ? 'fresh'
@@ -2274,6 +2248,9 @@ function buildWorkbenchConnectSnapshot(
2274
2248
  timestamp: new Date().toISOString(),
2275
2249
  },
2276
2250
  });
2251
+ // Establish ownership in this ordered stream before layouts and live tour
2252
+ // commands. The separate catalog HTTP response may arrive after either.
2253
+ events.push({ event: 'boards-changed', payload: boardsPayload() });
2277
2254
  if (primaryWorkbenchPath) {
2278
2255
  events.push({
2279
2256
  event: 'workbench-open',
@@ -3353,10 +3330,16 @@ export function emitPrimaryWorkbenchEvent(event: string, payload: PrimaryWorkben
3353
3330
  timestamp: new Date().toISOString(),
3354
3331
  };
3355
3332
  if (event === 'context-cards') {
3356
- lastWorkbenchContextCardsEnvelope = { ...envelope };
3333
+ boardReplay.current.contextCards = { ...envelope };
3357
3334
  }
3358
3335
  syncEventToCanvasState(event, envelope);
3359
- if (primaryWorkbenchAutoOpenEnabled && (event === 'workbench-open' || event === 'ext-app-open')) {
3336
+ // Events can create server state. Apply them to their producer's board before
3337
+ // deciding whether they belong in the human's currently visible projection.
3338
+ if (
3339
+ !canvasState.isBackgroundBoard &&
3340
+ primaryWorkbenchAutoOpenEnabled &&
3341
+ (event === 'workbench-open' || event === 'ext-app-open')
3342
+ ) {
3360
3343
  ensureCanvasBrowserOpen();
3361
3344
  }
3362
3345
  broadcastWorkbenchEvent(event, envelope);
@@ -3443,6 +3426,27 @@ export interface CanvasServerOptions {
3443
3426
  allowExternalMcp?: boolean;
3444
3427
  }
3445
3428
 
3429
+ // SDK/MCP producers can own cards before an HTTP listener starts.
3430
+ canvasState.setMoveRuntimeBlockers(() => {
3431
+ const blockers = new Map<string, string>();
3432
+ for (const id of traceManager.ownedNodeIds) blockers.set(id, 'the trace manager still owns this card');
3433
+ for (const [response, prompt] of serverResponseToThreadMap) {
3434
+ blockers.set(response, 'an active response is routed to this card');
3435
+ blockers.set(prompt, 'an active response is routed to this prompt');
3436
+ }
3437
+ for (const intent of pendingWorkbenchIntents) {
3438
+ if (intent.type !== 'canvas-prompt') continue;
3439
+ for (const id of [
3440
+ intent.payload.nodeId,
3441
+ intent.payload.parentNodeId,
3442
+ ...(Array.isArray(intent.payload.contextNodeIds) ? intent.payload.contextNodeIds : []),
3443
+ ]) {
3444
+ if (typeof id === 'string') blockers.set(id, 'a queued prompt still refers to this card');
3445
+ }
3446
+ }
3447
+ return blockers;
3448
+ });
3449
+
3446
3450
  export function startCanvasServer(options: CanvasServerOptions = {}): string | null {
3447
3451
  if (server) {
3448
3452
  return typeof server.port === 'number' ? loopbackBaseUrl(server.port) : null;
@@ -3489,12 +3493,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3489
3493
  });
3490
3494
  if (loaded) {
3491
3495
  console.log(' Canvas state restored from .pmx-canvas/canvas.db');
3492
- primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
3493
- const rehydrate = () => syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true });
3494
- const pending = options.allowExternalMcp === false ? withoutExternalMcp(rehydrate) : rehydrate();
3495
- void pending.finally(() => {
3496
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
3497
- });
3496
+ const rehydrate = () => startBoardRuntime({ forceRehydrateExtApps: true });
3497
+ if (options.allowExternalMcp === false) withoutExternalMcp(rehydrate);
3498
+ else rehydrate();
3498
3499
  }
3499
3500
 
3500
3501
  rotatePrimaryWorkbenchSessionIfNeeded();
@@ -3549,6 +3550,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3549
3550
  workspace: activeWorkspaceRoot,
3550
3551
  pid: process.pid,
3551
3552
  version: serverPackageVersion(),
3553
+ // The stamp open tabs reload on, read once per process: after a rebuild it lags
3554
+ // dist/ on disk, and already-open tabs keep old JS until the server restarts.
3555
+ bundleStamp: canvasBundleStamp(),
3552
3556
  contracts: [CANVAS_APP_WRITE_CONTRACT],
3553
3557
  persistence: canvasState.persistenceHealth,
3554
3558
  });
@@ -3571,16 +3575,6 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3571
3575
  return handleJsonRenderView(url);
3572
3576
  }
3573
3577
 
3574
- if (url.pathname === '/api/canvas/debug/ext-app-recovery' && req.method === 'POST') {
3575
- const body = await readJson(req);
3576
- if (body === null) return malformedJsonResponse();
3577
- return handleExtAppRecoveryReport(body.entries);
3578
- }
3579
-
3580
- if (url.pathname === '/api/canvas/debug/ext-app-recovery' && req.method === 'GET') {
3581
- return responseJson({ ok: true, entries: extAppRecoveryEntries });
3582
- }
3583
-
3584
3578
  if (url.pathname === '/api/canvas/frame-documents' && req.method === 'POST') {
3585
3579
  return handleCreateFrameDocument(req);
3586
3580
  }