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,5 +1,7 @@
1
1
  import { HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
2
2
  import { BarHint } from './BarHint';
3
+ import { AttentionHistory } from './AttentionHistory';
4
+ import { AttentionToast } from './AttentionToast';
3
5
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
4
6
  import { ContextNode } from '../nodes/ContextNode';
5
7
  import { DiffNode } from '../nodes/DiffNode';
@@ -242,8 +244,7 @@ export function ExpandedNodeOverlay() {
242
244
  };
243
245
  }, [handlePresentationKeyDown, presentationExitToken, presenting]);
244
246
 
245
- // Focus trap (item 18): restore to the node this overlay opened from — its
246
- // world element is unmounted while expanded and remounted on close.
247
+ // Restore to the same spatial card at every zoom.
247
248
  const panelRef = useRef<HTMLDivElement>(null);
248
249
  const restoreToNode = useCallback(
249
250
  () => (nodeId ? document.querySelector<HTMLElement>(`.canvas-node[data-node-id="${CSS.escape(nodeId)}"]`) : null),
@@ -398,6 +399,8 @@ export function ExpandedNodeOverlay() {
398
399
  {pendingClose ? 'Saving edits…' : 'esc or click outside to close'}
399
400
  </span>
400
401
  </div>
402
+ <AttentionToast />
403
+ <AttentionHistory />
401
404
  {canPresent && presenting && (
402
405
  <div
403
406
  ref={presentationOverlayRef}
@@ -0,0 +1,85 @@
1
+ import { activeBoard } from '../state/boards-store';
2
+ import { useLayoutEffect, useRef } from 'preact/hooks';
3
+ import { reportClientViewportSize } from '../state/intent-bridge';
4
+ import { focusNode, groupContents, groupPath, groupViewNodes, leaveGroup, nodes } from '../state/canvas-store';
5
+
6
+ export function GroupBreadcrumbs() {
7
+ const path = groupPath.value;
8
+ const ref = useRef<HTMLElement>(null);
9
+ useLayoutEffect(() => {
10
+ const element = ref.current;
11
+ if (!element) return;
12
+ const resize = new ResizeObserver(() => void reportClientViewportSize());
13
+ resize.observe(element);
14
+ return () => resize.disconnect();
15
+ }, [path.length > 0]);
16
+ if (!path.length) return null;
17
+ return (
18
+ <nav
19
+ ref={ref}
20
+ class="group-breadcrumbs"
21
+ aria-label="Group navigation"
22
+ onPointerDown={(event) => event.stopPropagation()}
23
+ >
24
+ <button type="button" onClick={() => leaveGroup(0)}>
25
+ {activeBoard()?.name ?? 'Board'}
26
+ </button>
27
+ {path.map((visit, index) => (
28
+ <span key={visit.id}>
29
+ <span aria-hidden="true"> / </span>
30
+ <button
31
+ type="button"
32
+ aria-current={index === path.length - 1 ? 'location' : undefined}
33
+ disabled={index === path.length - 1}
34
+ onClick={() => leaveGroup(index + 1)}
35
+ >
36
+ {String(nodes.value.get(visit.id)?.data.title ?? 'Group')}
37
+ </button>
38
+ </span>
39
+ ))}
40
+ <button type="button" onClick={() => leaveGroup()} aria-label="Leave group">
41
+ Esc · goes up
42
+ </button>
43
+ </nav>
44
+ );
45
+ }
46
+
47
+ export function GroupOutsideLinks() {
48
+ const inside = groupContents.value;
49
+ if (!inside) return null;
50
+ const outside = [...groupViewNodes.value.values()].filter((node) => !inside.has(node.id));
51
+ const children = [...nodes.value.values()].filter((node) => inside.has(node.id));
52
+ const root = nodes.value.get(groupPath.value.at(-1)!.id)!;
53
+ const left = children.length ? Math.min(...children.map((node) => node.position.x)) - 24 : root.position.x;
54
+ const top = children.length ? Math.min(...children.map((node) => node.position.y)) - 40 : root.position.y;
55
+ const right = children.length
56
+ ? Math.max(...children.map((node) => node.position.x + node.size.width)) + 24
57
+ : left + 320;
58
+ const bottom = children.length
59
+ ? Math.max(...children.map((node) => node.position.y + node.size.height)) + 24
60
+ : top + 120;
61
+ return (
62
+ <>
63
+ <div class="group-inside-frame" style={{ left, top, width: right - left, height: bottom - top }}>
64
+ <span>Inside {String(root.data.title ?? 'Group')}</span>
65
+ </div>
66
+ {outside.map((node, index) => (
67
+ <div key={node.id} class="group-outside-endpoint" style={{ left: node.position.x, top: node.position.y }}>
68
+ {index === 0 && <span class="group-outside-label">Outside</span>}
69
+ <button
70
+ type="button"
71
+ class="group-outside-stub"
72
+ onPointerDown={(event) => event.stopPropagation()}
73
+ onClick={(event) => {
74
+ event.stopPropagation();
75
+ focusNode(node.id, { recordHistory: false, persistStacking: false });
76
+ }}
77
+ >
78
+ ↗ {String(node.data.title ?? node.type)}
79
+ <span>{node.type} · Show on canvas</span>
80
+ </button>
81
+ </div>
82
+ ))}
83
+ </>
84
+ );
85
+ }
@@ -4,6 +4,7 @@ import { canvasArea } from './canvas-area';
4
4
  import { cancelViewportAnimation, hasInitialServerLayout, viewport } from '../state/canvas-store';
5
5
  import {
6
6
  endPresentation,
7
+ presentationCursor,
7
8
  presentationError,
8
9
  presenting,
9
10
  recordingCamera,
@@ -11,6 +12,7 @@ import {
11
12
  stepTour,
12
13
  } from '../state/presentation';
13
14
  import { usePanZoom } from './use-pan-zoom';
15
+ import { BarHint } from './BarHint';
14
16
 
15
17
  declare global {
16
18
  interface Window {
@@ -25,6 +27,9 @@ declare global {
25
27
 
26
28
  export function Presentation() {
27
29
  const active = presenting.value;
30
+ const cursor = presentationCursor.value;
31
+ const canBack = cursor !== null && cursor.index > 0;
32
+ const canNext = cursor !== null && cursor.index < cursor.total - 1;
28
33
  const ready = hasInitialServerLayout.value;
29
34
  const moveCamera = (camera: Camera) => {
30
35
  cancelViewportAnimation();
@@ -89,9 +94,54 @@ export function Presentation() {
89
94
  }}
90
95
  onContextMenu={(event) => event.preventDefault()}
91
96
  >
92
- <button type="button" class="presentation-exit" onClick={endPresentation}>
93
- Exit presentation
94
- </button>
97
+ <div class="presentation-controls" role="group" aria-label="Presentation controls">
98
+ <BarHint
99
+ id="presentation-back-hint"
100
+ label="Previous stop"
101
+ shortcut="← / ↑"
102
+ fitWithin=".presentation-navigation"
103
+ >
104
+ <button
105
+ type="button"
106
+ class="presentation-step"
107
+ aria-describedby="presentation-back-hint"
108
+ aria-disabled={!canBack}
109
+ onClick={() => {
110
+ if (canBack) void stepTour({ step: 'previous' });
111
+ }}
112
+ >
113
+ Back
114
+ </button>
115
+ </BarHint>
116
+ <BarHint
117
+ id="presentation-next-hint"
118
+ label="Next stop"
119
+ shortcut="→ / ↓ / Space"
120
+ fitWithin=".presentation-navigation"
121
+ >
122
+ <button
123
+ type="button"
124
+ class="presentation-step"
125
+ aria-describedby="presentation-next-hint"
126
+ aria-disabled={!canNext}
127
+ onClick={() => {
128
+ if (canNext) void stepTour({ step: 'next' });
129
+ }}
130
+ >
131
+ Next
132
+ </button>
133
+ </BarHint>
134
+ <BarHint id="presentation-exit-hint" label="Exit presentation" shortcut="Esc" align="end">
135
+ <button
136
+ type="button"
137
+ class="presentation-exit"
138
+ aria-describedby="presentation-exit-hint"
139
+ onClick={endPresentation}
140
+ >
141
+ Exit presentation
142
+ </button>
143
+ </BarHint>
144
+ </div>
95
145
  </div>
96
146
  )}
97
147
  {presentationError.value && (
@@ -4,6 +4,7 @@ import { relationDefinition, relationSourceEnd } from '../../shared/relations.js
4
4
  import { activeBoardId, boardList, openMemoryTarget } from '../state/boards-store';
5
5
  import { edges, nodes } from '../state/canvas-store';
6
6
  import { requestJson } from '../state/intent-bridge';
7
+ import { showRelationsOnCanvas } from '../state/relation-store';
7
8
 
8
9
  /** The expanded card's evidence and uses, from the same graph agents navigate. */
9
10
  export function RelationsPanel({ nodeId }: { nodeId: string }) {
@@ -50,6 +51,14 @@ export function RelationsPanel({ nodeId }: { nodeId: string }) {
50
51
  return (
51
52
  <aside class="relations-panel" aria-label="Relations">
52
53
  <h3>Relations</h3>
54
+ <div class="relation-navigation">
55
+ <button type="button" onClick={() => showRelationsOnCanvas(nodeId, 'lineage')}>
56
+ Show lineage
57
+ </button>
58
+ <button type="button" onClick={() => showRelationsOnCanvas(nodeId, 'connections')}>
59
+ Show on canvas
60
+ </button>
61
+ </div>
53
62
  {!graph && <p role="status">{unavailable ? 'Relations unavailable. Reopen to retry.' : 'Loading relations…'}</p>}
54
63
  {graph && relations.length === 0 && <p>No connections yet.</p>}
55
64
  {relations.map((edge, index) => {
@@ -13,6 +13,7 @@ import { createEdgeFromClient, createGroupFromClient, removeNodeFromClient } fro
13
13
  import { sessionActive } from '../state/presence-store';
14
14
  import { BarHint } from './BarHint';
15
15
  import { useChromeHeight } from './use-chrome-height';
16
+ import { openBoardMove } from './BoardMoveDialog';
16
17
 
17
18
  /**
18
19
  * Floating bottom-center bar for a multi-selection (rail-chrome-v2 phase 7,
@@ -174,6 +175,26 @@ export function SelectionBar() {
174
175
  </BarHint>
175
176
  </>
176
177
  )}
178
+ <BarHint label="Move to new board" side="up">
179
+ <button
180
+ type="button"
181
+ class="selection-bar-icon"
182
+ aria-label="Move to new board"
183
+ onClick={() => openBoardMove([...selectedNodeIds.value])}
184
+ >
185
+ <svg
186
+ width="16"
187
+ height="16"
188
+ viewBox="0 0 16 16"
189
+ fill="none"
190
+ stroke="currentColor"
191
+ stroke-width="1.5"
192
+ aria-hidden="true"
193
+ >
194
+ <path d="M6 3H2v10h12V9M8 8l6-6M9 2h5v5" />
195
+ </svg>
196
+ </button>
197
+ </BarHint>
177
198
  <BarHint label="Pin to agent context" side="up">
178
199
  <button type="button" class="selection-bar-action is-pin" onClick={handlePinContext}>
179
200
  <span aria-hidden="true">✦</span>
@@ -4,7 +4,9 @@ import { IconChevronRight, IconUndo } from '../icons';
4
4
  import { useNow } from './use-now';
5
5
  import { agentPhaseLabel } from '../../shared/agent-presence.js';
6
6
  import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
7
- import { focusNode, selectedNodeIds } from '../state/canvas-store';
7
+ import { focusNode, nodes, selectedNodeIds } from '../state/canvas-store';
8
+ import { activeBoardId } from '../state/boards-store';
9
+ import { createNodeFromClient } from '../state/intent-bridge';
8
10
  import { activeSession, agentPresences, attachedSessions, writerColor } from '../state/presence-store';
9
11
  import {
10
12
  type ApprovalGateView,
@@ -96,6 +98,35 @@ function clock(iso: string): string {
96
98
  }
97
99
 
98
100
  function WorkItemRow({ item }: { item: WorkItemView }) {
101
+ const [busy, setBusy] = useState(false);
102
+ const [pending, setPending] = useState<{ boardId: string; nodeId: string } | null>(null);
103
+ const boardId = activeBoardId.value;
104
+ const boardNodes = nodes.value;
105
+ const presentation = [...nodes.value.values()].find(
106
+ (node) => node.type === 'status' && node.data.workItemId === item.id,
107
+ );
108
+ useEffect(() => {
109
+ if (!pending) return;
110
+ if (boardId === pending.boardId && !boardNodes.has(pending.nodeId)) return;
111
+ if (boardId === pending.boardId) focusNode(pending.nodeId);
112
+ setPending(null);
113
+ setBusy(false);
114
+ }, [pending, boardId, boardNodes]);
115
+ const show = async () => {
116
+ if (presentation) return focusNode(presentation.id);
117
+ if (!boardId || busy) return;
118
+ setBusy(true);
119
+ const result = await createNodeFromClient({
120
+ type: 'status',
121
+ data: { workItemId: item.id },
122
+ width: 360,
123
+ height: 200,
124
+ expectedBoardId: boardId,
125
+ });
126
+ if (result.ok && result.id && activeBoardId.value === boardId) {
127
+ setPending({ boardId, nodeId: result.id });
128
+ } else setBusy(false);
129
+ };
99
130
  const glyph = WORK_GLYPH[item.status] ?? 'queued';
100
131
  const nodeId = item.nodeIds[0];
101
132
  return (
@@ -106,6 +137,9 @@ function WorkItemRow({ item }: { item: WorkItemView }) {
106
137
  {item.title}
107
138
  </button>
108
139
  {item.detail && <div class="session-item-detail">{item.detail}</div>}
140
+ <button type="button" class="session-show-work" disabled={busy} onClick={() => void show()}>
141
+ {busy ? 'Showing…' : presentation ? 'Show card' : 'Show on board'}
142
+ </button>
109
143
  </div>
110
144
  <span class="session-item-status">{WORK_STATUS_TEXT[item.status] ?? glyph}</span>
111
145
  <span class="session-item-time">{clock(item.updatedAt)}</span>
@@ -1,5 +1,6 @@
1
1
  import { signal } from '@preact/signals';
2
2
  import { useEffect, useRef } from 'preact/hooks';
3
+ import { useFocusTrap } from './use-focus-trap';
3
4
 
4
5
  /**
5
6
  * In-canvas replacement for `window.prompt`, which embedded browser panes
@@ -9,6 +10,8 @@ import { useEffect, useRef } from 'preact/hooks';
9
10
  interface TextPromptRequest {
10
11
  title: string;
11
12
  placeholder: string;
13
+ description?: string;
14
+ maxLength?: number;
12
15
  /** Prefilled (and pre-selected) value — edit flows start from the current text. */
13
16
  initial: string;
14
17
  /** Submitting an empty field resolves '' instead of null — "clear the value" flows. */
@@ -25,7 +28,7 @@ let promptSeq = 0;
25
28
  export function askText(
26
29
  title: string,
27
30
  placeholder: string,
28
- opts: { initial?: string; allowEmpty?: boolean; confirm?: string } = {},
31
+ opts: { initial?: string; allowEmpty?: boolean; confirm?: string; description?: string; maxLength?: number } = {},
29
32
  ): Promise<string | null> {
30
33
  return new Promise((resolve) => {
31
34
  textPromptRequest.value?.resolve(null);
@@ -34,6 +37,8 @@ export function askText(
34
37
  seq: promptSeq,
35
38
  title,
36
39
  placeholder,
40
+ description: opts.description,
41
+ maxLength: opts.maxLength,
37
42
  initial: opts.initial ?? '',
38
43
  allowEmpty: opts.allowEmpty ?? false,
39
44
  confirm: opts.confirm ?? 'Add',
@@ -51,6 +56,8 @@ function settle(value: string | null): void {
51
56
  export function TextPrompt() {
52
57
  const request = textPromptRequest.value;
53
58
  const inputRef = useRef<HTMLInputElement>(null);
59
+ const dialogRef = useRef<HTMLDivElement>(null);
60
+ useFocusTrap(dialogRef, request !== null, { initial: inputRef });
54
61
 
55
62
  useEffect(() => {
56
63
  if (!request) return;
@@ -78,13 +85,21 @@ export function TextPrompt() {
78
85
  return (
79
86
  <div class="text-prompt-backdrop" onPointerDown={() => settle(null)}>
80
87
  <div
88
+ ref={dialogRef}
81
89
  class="text-prompt"
82
90
  role="dialog"
91
+ aria-modal="true"
83
92
  aria-label={request.title}
93
+ aria-describedby={request.description ? 'text-prompt-description' : undefined}
84
94
  data-testid="text-prompt"
85
95
  onPointerDown={(e) => e.stopPropagation()}
86
96
  >
87
97
  <div class="text-prompt-title">{request.title}</div>
98
+ {request.description && (
99
+ <div id="text-prompt-description" class="text-prompt-description">
100
+ {request.description}
101
+ </div>
102
+ )}
88
103
  <input
89
104
  // A fresh element per ask (so defaultValue applies) focused in the
90
105
  // ref callback, which Preact runs synchronously during commit — the
@@ -101,6 +116,8 @@ export function TextPrompt() {
101
116
  type="text"
102
117
  defaultValue={request.initial}
103
118
  placeholder={request.placeholder}
119
+ aria-label={request.title}
120
+ maxLength={request.maxLength}
104
121
  onKeyDown={(e) => {
105
122
  // Contained: the global shortcut layer must not see these.
106
123
  if (e.key === 'Enter') {
@@ -38,6 +38,18 @@ import {
38
38
  } from '../state/session-store';
39
39
  import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
40
40
  import { agentSessionsEnabled } from '../state/workspace-settings';
41
+ import { humanName, setHumanName } from '../state/human-store';
42
+ import { tabAgentKey } from '../state/intent-bridge';
43
+ import { HUMAN_NAME_MAX_LENGTH } from '../../shared/human-presence.js';
44
+ import { askText } from './TextPrompt';
45
+
46
+ const PRESENT_DESCRIPTION = (
47
+ <span style={{ whiteSpace: 'pre-line' }}>
48
+ {
49
+ 'A presentation follows either a saved tour or the board’s groups.\n\nSaved tour: ask your agent to define the stops and their order. Each stop can focus on a card, a group, a rectangular area, or a specific camera position.\n\nNo saved tour: every group becomes a stop, ordered top-to-bottom, then left-to-right. It doesn’t automatically visit every card.'
50
+ }
51
+ </span>
52
+ );
41
53
 
42
54
  /**
43
55
  * The attached session's chip (rail-chrome-v2 phase 3): phase-colored, with a
@@ -481,12 +493,16 @@ export function TopBar() {
481
493
  )}
482
494
  {activeBoardId.value && (
483
495
  <BarHint
496
+ id="present-hint"
484
497
  label="Present board"
485
- body="Arrow keys or Space step through stops; Esc exits. Without a saved tour, groups are read top-to-bottom, left-to-right."
498
+ body={PRESENT_DESCRIPTION}
499
+ align="end"
500
+ fitWithin="body"
486
501
  >
487
502
  <button
488
503
  type="button"
489
504
  class="present-button"
505
+ aria-describedby="present-hint"
490
506
  onClick={() => {
491
507
  void startPresentation();
492
508
  }}
@@ -524,6 +540,32 @@ export function TopBar() {
524
540
  <TopBarMore boardOpen={!!activeBoardId.value} onFit={handleFit} />
525
541
  </>
526
542
  )}
543
+ {!isHostedWorkbench() && !tabAgentKey && (
544
+ <span class="human-name-control">
545
+ <BarHint label="Your name" body={`Shown on your cursor: ${humanName.value}`} align="end">
546
+ <button
547
+ type="button"
548
+ class="top-bar-btn human-name-button"
549
+ aria-label={`Your name: ${humanName.value}`}
550
+ aria-haspopup="dialog"
551
+ onClick={async (event) => {
552
+ const button = event.currentTarget;
553
+ const name = await askText('Your name on this canvas', 'e.g. Alex', {
554
+ initial: humanName.value,
555
+ confirm: 'Save name',
556
+ maxLength: HUMAN_NAME_MAX_LENGTH,
557
+ description:
558
+ 'Shown on your cursor. Self-reported, not a verified identity or an access grant. Saved in this browser.',
559
+ });
560
+ if (name !== null) setHumanName(name);
561
+ button.focus({ preventScroll: true });
562
+ }}
563
+ >
564
+ {writerInitial(humanName.value)}
565
+ </button>
566
+ </BarHint>
567
+ </span>
568
+ )}
527
569
  </div>
528
570
  </div>
529
571
  );
@@ -592,14 +634,17 @@ function TopBarMore({ boardOpen, onFit }: { boardOpen: boolean; onFit: () => voi
592
634
  </button>
593
635
  )}
594
636
  {boardOpen && (
595
- <button
596
- type="button"
597
- role="menuitem"
598
- class="toolbar-menu-item"
599
- onClick={run(() => void startPresentation())}
600
- >
601
- Present board
602
- </button>
637
+ <BarHint id="present-menu-hint" label="Present board" body={PRESENT_DESCRIPTION}>
638
+ <button
639
+ type="button"
640
+ role="menuitem"
641
+ class="toolbar-menu-item"
642
+ aria-describedby="present-menu-hint"
643
+ onClick={run(() => void startPresentation())}
644
+ >
645
+ Present board
646
+ </button>
647
+ </BarHint>
603
648
  )}
604
649
  <button type="button" role="menuitem" class="toolbar-menu-item" onClick={run(() => zoomByFactor(1.25))}>
605
650
  Zoom in
@@ -7,12 +7,6 @@ export const AUTO_FIT_MAX_HEIGHT = 600;
7
7
  // Iframe surfaces (charts/dashboards/rich html) can legitimately need more room
8
8
  // than a text node, so they grow to a higher ceiling before scrolling.
9
9
  export const AUTO_FIT_MAX_HEIGHT_IFRAME = 1400;
10
- // `.node-body` adds 12px padding top+bottom around an iframe surface (global.css).
11
- // The bridge reports the iframe's OWN document scrollHeight, so the grow target
12
- // must add titlebar + this body padding or the node settles ~24px short and the
13
- // surface shows a residual inner scrollbar. (The DOM auto-fit above doesn't need
14
- // this: body.scrollHeight already includes the body's padding.)
15
- export const AUTO_FIT_BODY_PADDING = 24;
16
10
 
17
11
  /** Node types the DOM auto-fit can't measure: iframe-backed surfaces (html/
18
12
  * json-render/graph/mcp-app), where the body's scrollHeight equals the iframe
@@ -77,13 +71,17 @@ export function shouldContentFitIframeNode(node: CanvasNodeState): boolean {
77
71
  /**
78
72
  * Grow-only target height from a surface-reported content height. Returns null
79
73
  * when the node is exempt, the report is non-positive, or the node already fits
80
- * (so it never shrinks — monotonic growth can't oscillate). Adds the titlebar +
81
- * node-body padding so the content fully clears (no residual inner scrollbar),
82
- * capped at the iframe ceiling.
74
+ * (so it never shrinks). Callers measure the actual chrome in world pixels,
75
+ * including zoom-compensated headers and any captions. A fixed padding allowance
76
+ * makes a full-height viewer grow again on every content-height report.
83
77
  */
84
- export function computeContentGrowHeight(node: CanvasNodeState, contentHeight: number): number | null {
78
+ export function computeContentGrowHeight(
79
+ node: CanvasNodeState,
80
+ contentHeight: number,
81
+ chromeHeight: number,
82
+ ): number | null {
85
83
  if (!shouldContentFitIframeNode(node) || !Number.isFinite(contentHeight) || contentHeight <= 0) return null;
86
- const want = Math.min(contentHeight + AUTO_FIT_TITLEBAR_HEIGHT + AUTO_FIT_BODY_PADDING, AUTO_FIT_MAX_HEIGHT_IFRAME);
84
+ const want = Math.min(Math.ceil(contentHeight + chromeHeight), AUTO_FIT_MAX_HEIGHT_IFRAME);
87
85
  return want > node.size.height + 8 ? want : null;
88
86
  }
89
87
 
@@ -55,12 +55,12 @@ export interface CanvasFitInsets {
55
55
  * both (0.5.0 Amp report, finding A; architecture rule 9's trap).
56
56
  */
57
57
  export function canvasFitInsets(): CanvasFitInsets {
58
- let top = 16;
58
+ let top = 44;
59
59
  let bottom = 16;
60
60
  try {
61
61
  const area = canvasArea();
62
62
  const areaBottom = area.top + area.height;
63
- for (const selector of ['.lifecycle-filters', '.relation-legend']) {
63
+ for (const selector of ['.lifecycle-filters', '.relation-legend', '.group-breadcrumbs']) {
64
64
  const el = document.querySelector(selector);
65
65
  if (!(el instanceof HTMLElement) || !el.isConnected) continue;
66
66
  const rect = el.getBoundingClientRect();
@@ -71,7 +71,12 @@ export function useFocusTrap(
71
71
  // from (focus mode unmounts the node while expanded) names the element
72
72
  // to return to, which may only exist after the next frame.
73
73
  if (restoreTo) {
74
- requestAnimationFrame(() => restoreTo()?.focus());
74
+ requestAnimationFrame(() => {
75
+ // A newly opened menu owns focus before this deferred return runs.
76
+ if (document.activeElement === document.body || root.contains(document.activeElement)) {
77
+ restoreTo()?.focus();
78
+ }
79
+ });
75
80
  } else if (previous && document.contains(previous) && previous !== document.body) {
76
81
  previous.focus();
77
82
  }
@@ -5,16 +5,10 @@
5
5
  * HTML-escaped diagram source in `<pre class="mermaid-source">`; this script
6
6
  * reads it back via textContent, renders it to SVG, and re-renders when the
7
7
  * parent canvas live-switches the theme (the theme bridge toggles the document's
8
- * `data-theme` attribute). Keeping mermaid out of the main SPA bundle keeps the
9
- * canvas boot lean — only mermaid-node surfaces pay for the library.
8
+ * `data-theme` attribute). Standalone views and exports share the canvas renderer.
10
9
  */
11
10
 
12
- import mermaid from 'mermaid';
13
- import { canvasThemeScheme } from '../shared/themes.js';
14
-
15
- function documentScheme(): 'dark' | 'light' {
16
- return canvasThemeScheme(document.documentElement.getAttribute('data-theme'));
17
- }
11
+ import { renderMermaid } from './mermaid-render.js';
18
12
 
19
13
  function showError(container: HTMLElement, message: string): void {
20
14
  container.innerHTML = '';
@@ -65,18 +59,14 @@ function fitDiagram(container: HTMLElement): void {
65
59
  }
66
60
 
67
61
  async function renderDiagram(source: string, container: HTMLElement): Promise<void> {
68
- const scheme = documentScheme();
69
- mermaid.initialize({
70
- startOnLoad: false,
71
- theme: scheme === 'dark' ? 'dark' : 'default',
72
- securityLevel: 'strict',
73
- });
74
- renderSeq += 1;
62
+ const sequence = ++renderSeq;
75
63
  try {
76
- const { svg } = await mermaid.render(`pmx-mermaid-${renderSeq}`, source);
64
+ const svg = await renderMermaid(source);
65
+ if (sequence !== renderSeq) return;
77
66
  container.innerHTML = svg;
78
67
  fitDiagram(container);
79
68
  } catch (error) {
69
+ if (sequence !== renderSeq) return;
80
70
  // Never leave a blank/black frame: show the parse/render error instead.
81
71
  showError(container, error instanceof Error ? error.message : String(error));
82
72
  }
@@ -98,11 +88,7 @@ function boot(): void {
98
88
  void renderDiagram(source, container);
99
89
  // Live theme switching: the parent's theme bridge toggles data-theme on the
100
90
  // root element; re-render so the diagram palette follows the canvas theme.
101
- let lastScheme = documentScheme();
102
91
  const observer = new MutationObserver(() => {
103
- const scheme = documentScheme();
104
- if (scheme === lastScheme) return;
105
- lastScheme = scheme;
106
92
  void renderDiagram(source, container);
107
93
  });
108
94
  observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
@@ -0,0 +1,39 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ // Mermaid configuration is global: serialize configuration with each render.
4
+ let rendering: Promise<unknown> = Promise.resolve();
5
+
6
+ export function renderMermaid(source: string): Promise<string> {
7
+ const result = rendering.then(async () => {
8
+ // Diagram-only dependency: the SPA splits this module; self-contained MCP
9
+ // documents defer its initialization without requiring external assets.
10
+ const { default: mermaid } = await import('mermaid');
11
+ const style = getComputedStyle(document.documentElement);
12
+ const token = (name: string) => style.getPropertyValue(name).trim();
13
+ mermaid.initialize({
14
+ startOnLoad: false,
15
+ securityLevel: 'strict',
16
+ theme: 'base',
17
+ fontFamily: token('--font-ui'),
18
+ themeVariables: {
19
+ primaryColor: token('--c-panel-soft'),
20
+ primaryTextColor: token('--c-text'),
21
+ primaryBorderColor: token('--c-line'),
22
+ lineColor: token('--c-muted'),
23
+ secondaryColor: token('--c-panel'),
24
+ tertiaryColor: token('--c-panel'),
25
+ edgeLabelBackground: token('--c-panel'),
26
+ fontSize: '12px',
27
+ },
28
+ });
29
+ const { svg } = await mermaid.render(`mermaid-${crypto.randomUUID()}`, source);
30
+ return DOMPurify.sanitize(svg, {
31
+ ADD_TAGS: ['foreignObject'],
32
+ ADD_ATTR: ['dominant-baseline'],
33
+ HTML_INTEGRATION_POINTS: { foreignobject: true },
34
+ });
35
+ });
36
+ // A malformed diagram must not prevent the next diagram from rendering.
37
+ rendering = result.catch(() => {});
38
+ return result;
39
+ }