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
@@ -1,13 +1,15 @@
1
1
  import { effect } from '@preact/signals';
2
2
  import { JSON_VIEWER_STATE_SOURCE } from '../../shared/ax-surface-protocol.js';
3
3
  import { canvasInteractionEpoch, nodes } from '../state/canvas-store';
4
+ import type { CanvasNodeState } from '../types';
5
+ import { VIEWER_LOAD_DEADLINE_MS } from './ViewerFrame';
4
6
 
5
7
  type Snapshot = {
6
8
  epoch: number;
7
9
  version: unknown;
8
10
  documentUrl: unknown;
9
11
  state: Record<string, unknown> | null;
10
- owners: Array<{ token: string; source: Window; mounted: () => void; ready: () => void; cancel: () => void }>;
12
+ owners: Array<{ token: string; source: Window }>;
11
13
  };
12
14
 
13
15
  // Tab-local UI state only: never persist form drafts in canvas data or AX. Keep
@@ -18,11 +20,12 @@ const snapshots = new Map<string, Snapshot>();
18
20
  let listening = false;
19
21
 
20
22
  export function restoreViewerRuntimeState(
21
- nodeId: string,
23
+ authored: CanvasNodeState,
22
24
  token: string,
23
25
  source: Window,
24
26
  signal?: AbortSignal,
25
27
  ): Promise<boolean | 'cancelled'> {
28
+ const nodeId = authored.id;
26
29
  if (!listening) {
27
30
  listening = true;
28
31
  effect(() => {
@@ -36,7 +39,6 @@ export function restoreViewerRuntimeState(
36
39
  snapshot.version !== node.data.specVersion ||
37
40
  snapshot.documentUrl !== node.data.url
38
41
  ) {
39
- for (const owner of snapshot.owners) owner.cancel();
40
42
  snapshots.delete(id);
41
43
  }
42
44
  }
@@ -54,57 +56,61 @@ export function restoreViewerRuntimeState(
54
56
  const ownerIndex =
55
57
  snapshot?.owners.findIndex((owner) => owner.token === message.token && owner.source === event.source) ?? -1;
56
58
  if (!snapshot || ownerIndex < 0) return;
57
- if (message.type === 'mounted') {
58
- snapshot.owners[ownerIndex]!.mounted();
59
- return;
60
- }
61
- if (message.type === 'ready') {
62
- snapshot.owners[ownerIndex]!.ready();
63
- return;
64
- }
65
59
  if (message.type !== 'snapshot') return;
66
60
  if (!message.state || typeof message.state !== 'object' || Array.isArray(message.state)) return;
67
61
  const { ax: _ax, ...state } = message.state as Record<string, unknown>;
68
62
  snapshot.state = state;
69
63
  // Once the replacement emits, the retired document cannot overwrite it.
70
- const retired = snapshot.owners.slice(0, ownerIndex);
71
64
  snapshot.owners = snapshot.owners.slice(ownerIndex);
72
- for (const owner of retired) owner.cancel();
73
65
  });
74
66
  }
75
67
  const node = nodes.value.get(nodeId);
76
68
  if (!node || signal?.aborted) return Promise.resolve('cancelled');
77
- const snapshot: Snapshot = snapshots.get(nodeId) ?? {
78
- epoch: canvasInteractionEpoch.value,
79
- version: node.data.specVersion,
80
- documentUrl: node.data.url,
81
- state: null,
82
- owners: [],
83
- };
84
- snapshots.set(nodeId, snapshot);
69
+ // A retained document may load after its authored spec was replaced. It can
70
+ // still acknowledge paint, but cannot own or restore the new spec's drafts.
71
+ const current = node.data.specVersion === authored.data.specVersion && node.data.url === authored.data.url;
72
+ const snapshot: Snapshot | undefined = current
73
+ ? (snapshots.get(nodeId) ?? {
74
+ epoch: canvasInteractionEpoch.value,
75
+ version: authored.data.specVersion,
76
+ documentUrl: authored.data.url,
77
+ state: null,
78
+ owners: [],
79
+ })
80
+ : undefined;
81
+ if (snapshot) snapshots.set(nodeId, snapshot);
85
82
  const ready = new Promise<boolean | 'cancelled'>((resolve) => {
86
83
  const finish = (loaded: boolean | 'cancelled') => {
87
84
  clearTimeout(timer);
88
- signal?.removeEventListener('abort', owner.cancel);
89
- if (loaded !== true) snapshot.owners = snapshot.owners.filter((entry) => entry !== owner);
85
+ window.removeEventListener('message', receive);
86
+ signal?.removeEventListener('abort', cancel);
87
+ if (loaded !== true && snapshot) snapshot.owners = snapshot.owners.filter((entry) => entry !== owner);
90
88
  resolve(loaded);
91
89
  };
92
- const owner = {
93
- token,
94
- source,
95
- mounted: () => clearTimeout(timer),
96
- ready: () => finish(true),
97
- cancel: () => finish('cancelled'),
90
+ const cancel = () => finish('cancelled');
91
+ const receive = (event: MessageEvent) => {
92
+ if (
93
+ event.source !== source ||
94
+ event.data?.source !== JSON_VIEWER_STATE_SOURCE ||
95
+ event.data?.token !== token ||
96
+ event.data?.nodeId !== nodeId
97
+ )
98
+ return;
99
+ if (event.data.type === 'mounted') clearTimeout(timer);
100
+ if (event.data.type === 'ready') finish(true);
98
101
  };
102
+ const owner = { token, source };
99
103
  // A missing runtime fails, but a healthy offscreen document can wait for
100
104
  // paint indefinitely. Its synchronous mount acknowledgement ends the deadline;
101
105
  // only the later frame-gated ready message may promote it.
102
- const timer = setTimeout(() => finish(false), 10_000);
103
- for (const previous of snapshot.owners.filter((entry) => entry.source === source)) previous.cancel();
104
- while (snapshot.owners.length > 1) snapshot.owners[0]!.cancel();
105
- snapshot.owners.push(owner);
106
- signal?.addEventListener('abort', owner.cancel, { once: true });
106
+ const timer = setTimeout(() => finish(false), VIEWER_LOAD_DEADLINE_MS);
107
+ if (snapshot) {
108
+ snapshot.owners = snapshot.owners.filter((entry) => entry.source !== source).slice(-1);
109
+ snapshot.owners.push(owner);
110
+ }
111
+ window.addEventListener('message', receive);
112
+ signal?.addEventListener('abort', cancel, { once: true });
107
113
  });
108
- source.postMessage({ source: JSON_VIEWER_STATE_SOURCE, type: 'restore', token, state: snapshot.state }, '*');
114
+ source.postMessage({ source: JSON_VIEWER_STATE_SOURCE, type: 'restore', token, state: snapshot?.state ?? null }, '*');
109
115
  return ready;
110
116
  }
@@ -9,6 +9,7 @@ import {
9
9
  type SseMessage,
10
10
  } from '../../shared/semantic-attention.js';
11
11
  import {
12
+ attentionHistoryOpen,
12
13
  pushAttentionHistory,
13
14
  resetAttentionState,
14
15
  setAttentionPulse,
@@ -291,6 +292,10 @@ function entryFromEvent(event: SemanticWatchEvent): AttentionEntry | null {
291
292
  }
292
293
 
293
294
  function flushToastQueue(): void {
295
+ if (attentionHistoryOpen.value) {
296
+ toastQueue = [];
297
+ return;
298
+ }
294
299
  if (toastTimer !== null) return;
295
300
  const next = toastQueue.shift() ?? null;
296
301
  if (!next) {
@@ -26,6 +26,7 @@ export function resetAttentionState(): void {
26
26
  }
27
27
 
28
28
  export function openAttentionHistory(): void {
29
+ attentionToast.value = null;
29
30
  attentionHistoryOpen.value = true;
30
31
  attentionHistoryUnread.value = 0;
31
32
  }
@@ -1,4 +1,4 @@
1
- import { batch, computed, signal } from '@preact/signals';
1
+ import { batch, computed, effect, signal } from '@preact/signals';
2
2
  import {
3
3
  isExcalidrawNode,
4
4
  type CanvasAnnotation,
@@ -159,6 +159,7 @@ export function resetCanvasInteractionState(): void {
159
159
  }
160
160
  batch(() => {
161
161
  canvasInteractionEpoch.value += 1;
162
+ groupPath.value = [];
162
163
  contextPinsRevision++;
163
164
  contextPinsPendingWrites = 0;
164
165
  activeNodeId.value = null;
@@ -226,13 +227,14 @@ export function trackDragMembership(nodeId: string): void {
226
227
  const cy = node.position.y + node.size.height / 2;
227
228
  let candidate: CanvasNodeState | null = null;
228
229
  for (const other of nodes.value.values()) {
230
+ if (groupContents.value && !groupContents.value.has(other.id)) continue;
229
231
  if (other.type !== 'group' || other.id === parentId || other.collapsed) continue;
230
232
  if (contains(other, cx, cy)) candidate = other;
231
233
  }
232
234
  if (!dropSuppressedForDrag) {
233
235
  const next = candidate
234
236
  ? { nodeId, groupId: candidate.id, mode: 'add' as const }
235
- : parent && fullyOutside(node, parent)
237
+ : parent && parent.id !== groupPath.value.at(-1)?.id && fullyOutside(node, parent)
236
238
  ? { nodeId, groupId: parent.id, mode: 'remove' as const }
237
239
  : null;
238
240
  const current = dragDropTarget.value;
@@ -266,9 +268,14 @@ export const hiddenByCollapsedGroup = computed(() => {
266
268
  const hidden = new Map<string, string>();
267
269
  for (const node of nodes.value.values()) {
268
270
  if (node.type !== 'group' || !node.collapsed) continue;
269
- const children = Array.isArray(node.data.children) ? node.data.children : [];
270
- for (const childId of children) {
271
- if (typeof childId === 'string') hidden.set(childId, node.id);
271
+ if (groupContents.value && !groupContents.value.has(node.id)) continue;
272
+ const pending = new Set(Array.isArray(node.data.children) ? node.data.children : []);
273
+ for (const childId of pending) {
274
+ if (typeof childId !== 'string' || childId === node.id) continue;
275
+ hidden.set(childId, node.id);
276
+ const child = nodes.value.get(childId);
277
+ if (child?.type === 'group' && Array.isArray(child.data.children))
278
+ for (const descendant of child.data.children) pending.add(descendant);
272
279
  }
273
280
  }
274
281
  return hidden;
@@ -276,10 +283,131 @@ export const hiddenByCollapsedGroup = computed(() => {
276
283
 
277
284
  /** The node that stands in for `id` on the canvas: itself, or its collapsed group's chip. */
278
285
  export function visibleNodeFor(id: string): CanvasNodeState | undefined {
279
- const chipId = hiddenByCollapsedGroup.value.get(id);
280
- return nodes.value.get(chipId ?? id);
286
+ const visited = new Set<string>();
287
+ while (hiddenByCollapsedGroup.value.has(id) && !visited.has(id)) {
288
+ visited.add(id);
289
+ id = hiddenByCollapsedGroup.value.get(id)!;
290
+ }
291
+ return nodes.value.get(id);
292
+ }
293
+
294
+ interface GroupVisit {
295
+ id: string;
296
+ parent: unknown;
297
+ viewport: ViewportState;
298
+ selection: Set<string>;
299
+ active: string | null;
281
300
  }
282
301
 
302
+ /** Human navigation only. The canonical node/edge maps and agent reads remain whole-board. */
303
+ export const groupPath = signal<GroupVisit[]>([]);
304
+ export const groupContents = computed(() => {
305
+ const id = groupPath.value.at(-1)?.id;
306
+ if (!id) return null;
307
+ const ids = new Set<string>();
308
+ const pending = [id];
309
+ const visited = new Set<string>();
310
+ for (const parent of pending) {
311
+ if (visited.has(parent)) continue;
312
+ visited.add(parent);
313
+ const node = nodes.value.get(parent);
314
+ if (node?.type !== 'group' || !Array.isArray(node.data.children)) continue;
315
+ for (const child of node.data.children) {
316
+ if (typeof child !== 'string' || child === id || !nodes.value.has(child)) continue;
317
+ ids.add(child);
318
+ pending.push(child);
319
+ }
320
+ }
321
+ return ids;
322
+ });
323
+
324
+ /** The drawing's outside endpoints are view-only stubs, never inserted into nodes. */
325
+ export const groupViewNodes = computed(() => {
326
+ const inside = groupContents.value;
327
+ if (!inside) return nodes.value;
328
+ const view = new Map([...nodes.value].filter(([id]) => inside.has(id)));
329
+ const outside = new Set<string>();
330
+ for (const edge of edges.value.values()) {
331
+ if (edge.toBoardId || inside.has(edge.from) === inside.has(edge.to)) continue;
332
+ outside.add(inside.has(edge.from) ? edge.to : edge.from);
333
+ }
334
+ const root = nodes.value.get(groupPath.value.at(-1)!.id);
335
+ const bounds = [...view.values()];
336
+ const left = bounds.length ? Math.min(...bounds.map((node) => node.position.x)) : (root?.position.x ?? 0);
337
+ const bottom = bounds.length
338
+ ? Math.max(...bounds.map((node) => node.position.y + node.size.height))
339
+ : (root?.position.y ?? 0) + 80;
340
+ [...outside].sort().forEach((id, index) => {
341
+ const node = nodes.value.get(id);
342
+ if (node)
343
+ view.set(id, {
344
+ ...node,
345
+ position: { x: left + (index % 3) * 194, y: bottom + 88 + Math.floor(index / 3) * 84 },
346
+ size: { width: 170, height: 60 },
347
+ collapsed: false,
348
+ });
349
+ });
350
+ return view;
351
+ });
352
+
353
+ export function leaveGroup(depth = groupPath.value.length - 1): void {
354
+ const visit = groupPath.value[depth];
355
+ if (!visit) return;
356
+ cancelViewportAnimation();
357
+ batch(() => {
358
+ groupPath.value = groupPath.value.slice(0, depth);
359
+ viewport.value = visit.viewport;
360
+ selectedNodeIds.value = filterNodeIdSet(visit.selection, nodes.value);
361
+ activeNodeId.value = visit.active && nodes.value.has(visit.active) ? visit.active : null;
362
+ selectedEdgeId.value = null;
363
+ });
364
+ void updateViewportFromClient(visit.viewport, { recordHistory: false });
365
+ }
366
+
367
+ export function openGroup(id: string, fit = true): void {
368
+ const node = nodes.value.get(id);
369
+ if (node?.type !== 'group' || groupPath.value.some((visit) => visit.id === id)) return;
370
+ if (groupContents.value && !groupContents.value.has(id)) leaveGroup(0);
371
+ cancelViewportAnimation();
372
+ batch(() => {
373
+ groupPath.value = [
374
+ ...groupPath.value,
375
+ {
376
+ id,
377
+ parent: node.data.parentGroup,
378
+ viewport: { ...viewport.value },
379
+ selection: new Set(selectedNodeIds.value),
380
+ active: activeNodeId.value,
381
+ },
382
+ ];
383
+ clearSelection();
384
+ activeNodeId.value = null;
385
+ });
386
+ // Wait for the breadcrumb/legend to paint before measuring their screen insets.
387
+ if (!fit) return;
388
+ afterGroupView(() => {
389
+ const area = canvasArea();
390
+ fitAll(area.width, area.height);
391
+ });
392
+ }
393
+
394
+ effect(() => {
395
+ const path = groupPath.value;
396
+ const map = nodes.value;
397
+ const invalid = path.findIndex((visit) => {
398
+ const node = map.get(visit.id);
399
+ return node?.type !== 'group' || node.data.parentGroup !== visit.parent;
400
+ });
401
+ if (invalid >= 0) leaveGroup(invalid);
402
+ const inside = groupContents.value;
403
+ if (inside) {
404
+ const selected = selectedNodeIds.value;
405
+ if ([...selected].some((id) => !inside.has(id)))
406
+ selectedNodeIds.value = new Set([...selected].filter((id) => inside.has(id)));
407
+ if (activeNodeId.value && !inside.has(activeNodeId.value)) activeNodeId.value = null;
408
+ }
409
+ });
410
+
283
411
  /** Collapsed groups render as a chip; children keep their positions for restore. */
284
412
  export function groupsOfSelection(): string[] {
285
413
  const ids = new Set<string>();
@@ -637,18 +765,22 @@ export function resizeNode(id: string, size: { width: number; height: number }):
637
765
  updateNode(id, { size });
638
766
  }
639
767
 
640
- export function bringToFront(id: string): void {
768
+ export function bringToFront(id: string, persist = true): void {
641
769
  const existing = nodes.value.get(id);
642
770
  if (!existing) return;
643
- batch(() => {
644
- updateNode(id, { zIndex: maxZ++ });
645
- // A group is a frame: it must never stack above its own children.
646
- if (existing.type === 'group' && Array.isArray(existing.data.children)) {
647
- for (const childId of existing.data.children) {
648
- if (typeof childId === 'string' && nodes.value.has(childId)) updateNode(childId, { zIndex: maxZ++ });
649
- }
771
+ const updates = [{ id, zIndex: maxZ++ }];
772
+ // A group is a frame: it must never stack above its own children.
773
+ if (existing.type === 'group' && Array.isArray(existing.data.children)) {
774
+ for (const childId of existing.data.children) {
775
+ if (typeof childId === 'string' && nodes.value.has(childId)) updates.push({ id: childId, zIndex: maxZ++ });
650
776
  }
777
+ }
778
+ batch(() => {
779
+ for (const update of updates) updateNode(update.id, { zIndex: update.zIndex });
651
780
  });
781
+ // Title-bar gestures commit stacking with geometry at pointer-up. A separate
782
+ // drag-start write would broadcast an old layout over the in-progress move.
783
+ if (persist) void pushCanvasUpdate(updates, { recordHistory: false });
652
784
  activeNodeId.value = id;
653
785
  }
654
786
 
@@ -817,17 +949,31 @@ export function cancelViewportAnimation(): void {
817
949
  }
818
950
  }
819
951
 
952
+ /** Share the camera slot so newer navigation and direct gestures cancel queued fits. */
953
+ function afterGroupView(action: () => void): void {
954
+ cancelViewportAnimation();
955
+ const epoch = canvasInteractionEpoch.value;
956
+ const path = groupPath.value;
957
+ animationId = requestAnimationFrame(() => {
958
+ animationId = null;
959
+ if (epoch === canvasInteractionEpoch.value && path === groupPath.value) action();
960
+ });
961
+ }
962
+
820
963
  // ── Persistence ───────────────────────────────────────────────
821
964
  const STORAGE_KEY = 'pmx-canvas-layout';
822
965
 
823
- export function persistLayout(options: { recordHistory?: boolean } = {}): void {
966
+ export function persistLayout(options: { recordHistory?: boolean; raisedNodeId?: string } = {}): void {
824
967
  try {
825
968
  const allNodes = Array.from(nodes.value.values());
969
+ const raised = options.raisedNodeId ? nodes.value.get(options.raisedNodeId) : undefined;
970
+ const raisedChildren = raised?.type === 'group' && Array.isArray(raised.data.children) ? raised.data.children : [];
826
971
  const nodeUpdates = allNodes.map((n) => ({
827
972
  id: n.id,
828
973
  position: n.position,
829
974
  size: n.size,
830
975
  collapsed: n.collapsed,
976
+ ...(n.id === raised?.id || raisedChildren.includes(n.id) ? { zIndex: n.zIndex } : {}),
831
977
  }));
832
978
  const layout = {
833
979
  viewport: viewport.value,
@@ -896,7 +1042,9 @@ export function restoreLayout(): Map<string, Partial<CanvasNodeState>> | null {
896
1042
 
897
1043
  // ── Fit all ───────────────────────────────────────────────────
898
1044
  export function fitAll(containerW: number, containerH: number): void {
899
- const all = Array.from(nodes.value.values());
1045
+ const all = Array.from(groupViewNodes.value.values());
1046
+ const group = nodes.value.get(groupPath.value.at(-1)?.id ?? '');
1047
+ if (!all.length && group) all.push(group);
900
1048
  if (all.length === 0) return;
901
1049
 
902
1050
  let minX = Number.POSITIVE_INFINITY;
@@ -909,6 +1057,12 @@ export function fitAll(containerW: number, containerH: number): void {
909
1057
  maxX = Math.max(maxX, n.position.x + n.size.width);
910
1058
  maxY = Math.max(maxY, n.position.y + n.size.height);
911
1059
  }
1060
+ if (groupPath.value.length) {
1061
+ minX -= 24;
1062
+ minY -= 40;
1063
+ maxX += 24;
1064
+ maxY += 24;
1065
+ }
912
1066
 
913
1067
  // Screen-space fit (architecture rule 9): the margins that must clear the
914
1068
  // floating chrome are SCREEN constants. The old world-space PAD shrank with
@@ -923,17 +1077,41 @@ export function fitAll(containerW: number, containerH: number): void {
923
1077
  const cx = (minX + maxX) / 2;
924
1078
  const cy = (minY + maxY) / 2;
925
1079
 
926
- animateViewport({
927
- x: insets.left + usableW / 2 - cx * scale,
928
- y: insets.top + usableH / 2 - cy * scale,
929
- scale,
930
- });
1080
+ animateViewport(
1081
+ {
1082
+ x: insets.left + usableW / 2 - cx * scale,
1083
+ y: insets.top + usableH / 2 - cy * scale,
1084
+ scale,
1085
+ },
1086
+ 300,
1087
+ { recordHistory: groupPath.value.length === 0 },
1088
+ );
931
1089
  }
932
1090
 
933
1091
  // ── Focus node ────────────────────────────────────────────────
934
- export function focusNode(id: string, options: { recordHistory?: boolean } = {}): void {
1092
+ export function focusNode(id: string, options: { recordHistory?: boolean; persistStacking?: boolean } = {}): void {
935
1093
  const node = nodes.value.get(id);
936
1094
  if (!node) return;
1095
+ cancelViewportAnimation();
1096
+ const previousPath = groupPath.value;
1097
+ if (groupContents.value && !groupContents.value.has(id)) leaveGroup(0);
1098
+ // Reveal collapsed targets through view state, without changing authored collapse flags.
1099
+ const ancestors: string[] = [];
1100
+ let parent = node.data.parentGroup;
1101
+ while (typeof parent === 'string' && !ancestors.includes(parent)) {
1102
+ ancestors.push(parent);
1103
+ parent = nodes.value.get(parent)?.data.parentGroup;
1104
+ }
1105
+ for (const ancestor of ancestors.reverse()) {
1106
+ if (nodes.value.get(ancestor)?.collapsed && !groupPath.value.some((visit) => visit.id === ancestor))
1107
+ openGroup(ancestor, false);
1108
+ }
1109
+ bringToFront(id, options.persistStacking !== false);
1110
+ if (groupPath.value.length || previousPath !== groupPath.value) afterGroupView(() => focusNodeCamera(node, options));
1111
+ else focusNodeCamera(node, options);
1112
+ }
1113
+
1114
+ function focusNodeCamera(node: CanvasNodeState, options: { recordHistory?: boolean }): void {
937
1115
  const v = viewport.value;
938
1116
  // Same screen-space frame as fitAll: viewport x/y are CONTAINER-relative
939
1117
  // (screen = world*scale + viewport inside the canvas region), so the centre
@@ -959,17 +1137,15 @@ export function focusNode(id: string, options: { recordHistory?: boolean } = {})
959
1137
  300,
960
1138
  options,
961
1139
  );
962
- bringToFront(id);
963
1140
  }
964
1141
 
965
1142
  // ── Cycle focus ───────────────────────────────────────────────
966
1143
  export function cycleActiveNode(direction: 1 | -1 = 1): void {
967
- const all = Array.from(nodes.value.keys());
1144
+ const all = Array.from(nodes.value.keys()).filter((id) => !groupContents.value || groupContents.value.has(id));
968
1145
  if (all.length === 0) return;
969
1146
  const currentIdx = activeNodeId.value ? all.indexOf(activeNodeId.value) : -1;
970
1147
  const nextIdx = (currentIdx + direction + all.length) % all.length;
971
1148
  const nextId = all[nextIdx];
972
- bringToFront(nextId);
973
1149
  focusNode(nextId);
974
1150
  }
975
1151
 
@@ -1,6 +1,7 @@
1
1
  import { computed, signal } from '@preact/signals';
2
2
  import {
3
3
  HUMAN_CURSOR_THROTTLE_MS,
4
+ HUMAN_NAME_MAX_LENGTH,
4
5
  type HumanPresence,
5
6
  type HumanPresenceSnapshot,
6
7
  } from '../../shared/human-presence.js';
@@ -34,12 +35,12 @@ export const humanClientId = sessionScoped(
34
35
 
35
36
  function resolveName(): string {
36
37
  try {
37
- const fromQuery = new URLSearchParams(window.location.search).get('name')?.trim();
38
+ const fromQuery = new URLSearchParams(window.location.search).get('name')?.trim().slice(0, HUMAN_NAME_MAX_LENGTH);
38
39
  if (fromQuery) {
39
40
  window.localStorage.setItem('pmx-canvas-human-name', fromQuery);
40
41
  return fromQuery;
41
42
  }
42
- const stored = window.localStorage.getItem('pmx-canvas-human-name')?.trim();
43
+ const stored = window.localStorage.getItem('pmx-canvas-human-name')?.trim().slice(0, HUMAN_NAME_MAX_LENGTH);
43
44
  if (stored) return stored;
44
45
  } catch {
45
46
  // storage unavailable: fall through to a generated name
@@ -51,6 +52,25 @@ export const humanName = signal<string>(resolveName());
51
52
  export const humans = signal<HumanPresence[]>([]);
52
53
  export const otherHumans = computed(() => humans.value.filter((human) => human.clientId !== humanClientId));
53
54
 
55
+ /** Change the display label, never the per-tab identity or anyone's access. */
56
+ export function setHumanName(name: string): void {
57
+ const value = name.trim().slice(0, HUMAN_NAME_MAX_LENGTH);
58
+ if (!value) return;
59
+ humanName.value = value;
60
+ try {
61
+ window.localStorage.setItem('pmx-canvas-human-name', value);
62
+ } catch {
63
+ // Storage can be blocked in embedded panes; the current tab still has its name.
64
+ }
65
+ // A link's initial name must not undo an explicit rename on reload.
66
+ const url = new URL(window.location.href);
67
+ if (url.searchParams.has('name')) {
68
+ url.searchParams.delete('name');
69
+ window.history.replaceState(window.history.state, '', url);
70
+ }
71
+ post({ cursor: lastCursor, grabbingNodeId: grabbingNodeId.value });
72
+ }
73
+
54
74
  /** Nodes a human took over mid-edit → that human's name, shown as the yield pill for a moment. */
55
75
  export const yieldedNodes = signal<Map<string, string>>(new Map());
56
76
 
@@ -250,6 +250,7 @@ export async function pushCanvasUpdate(
250
250
  position?: { x: number; y: number };
251
251
  size?: { width: number; height: number };
252
252
  collapsed?: boolean;
253
+ zIndex?: number;
253
254
  }>,
254
255
  options: { recordHistory?: boolean } = {},
255
256
  ): Promise<void> {
@@ -299,11 +300,13 @@ export async function createNodeFromClient(opts: {
299
300
  type?: string;
300
301
  title?: string;
301
302
  content?: string;
303
+ data?: Record<string, unknown>;
302
304
  x?: number;
303
305
  y?: number;
304
306
  width?: number;
305
307
  height?: number;
306
308
  boardId?: string;
309
+ expectedBoardId?: string;
307
310
  }): Promise<{ ok: boolean; id?: string }> {
308
311
  return requestJson(
309
312
  'createNodeFromClient',
@@ -489,7 +492,7 @@ export async function reportClientViewportSize(): Promise<void> {
489
492
  if (isHostedWorkbench() && !boardsLoaded.value) return;
490
493
  const size = clientWindowSize();
491
494
  if (!('clientWidth' in size)) return;
492
- await requestBestEffort('reportClientViewportSize', '/api/canvas/viewport', {
495
+ const init: RequestInit = {
493
496
  method: 'POST',
494
497
  headers: { 'Content-Type': 'application/json' },
495
498
  body: JSON.stringify({
@@ -497,7 +500,15 @@ export async function reportClientViewportSize(): Promise<void> {
497
500
  ...(boardsLoaded.value ? { expectedBoardId: activeBoardId.value } : {}),
498
501
  recordHistory: false,
499
502
  }),
500
- });
503
+ };
504
+ try {
505
+ const res = await workbenchFetch('/api/canvas/viewport', withWorkbenchMarker(init));
506
+ // Resize observations can cross a board switch. Discard that stale report,
507
+ // without telling the human an edit failed; actual camera writes still report refusals.
508
+ if (!res.ok && res.status !== 409) reportRefusedWrite(res, init, await readJsonBody(res));
509
+ } catch (error) {
510
+ logRequestError('reportClientViewportSize', error);
511
+ }
501
512
  }
502
513
 
503
514
  // ── Group API ─────────────────────────────────────────────────
@@ -611,6 +622,7 @@ export async function updateEdgeFromClient(
611
622
  style?: string;
612
623
  animated?: boolean;
613
624
  },
625
+ expectedBoardId = activeBoardId.value,
614
626
  ): Promise<{ ok: boolean }> {
615
627
  return requestJson(
616
628
  'updateEdgeFromClient',
@@ -619,13 +631,16 @@ export async function updateEdgeFromClient(
619
631
  {
620
632
  method: 'PATCH',
621
633
  headers: { 'Content-Type': 'application/json' },
622
- body: JSON.stringify(patch),
634
+ body: JSON.stringify({ ...patch, expectedBoardId }),
623
635
  },
624
636
  );
625
637
  }
626
638
 
627
639
  /** Remove a canvas edge via the server. */
628
- export async function removeEdgeFromClient(edgeId: string): Promise<{ ok: boolean }> {
640
+ export async function removeEdgeFromClient(
641
+ edgeId: string,
642
+ expectedBoardId = activeBoardId.value,
643
+ ): Promise<{ ok: boolean }> {
629
644
  return requestJson(
630
645
  'removeEdgeFromClient',
631
646
  '/api/canvas/edge',
@@ -633,7 +648,7 @@ export async function removeEdgeFromClient(edgeId: string): Promise<{ ok: boolea
633
648
  {
634
649
  method: 'DELETE',
635
650
  headers: { 'Content-Type': 'application/json' },
636
- body: JSON.stringify({ edge_id: edgeId }),
651
+ body: JSON.stringify({ edge_id: edgeId, expectedBoardId }),
637
652
  },
638
653
  );
639
654
  }