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
@@ -0,0 +1,175 @@
1
+ import { signal, type Signal } from '@preact/signals';
2
+ import { useMemo } from 'preact/hooks';
3
+ import { WORK_CARD_STATES, type AxWorkItemStatus } from '../../shared/ax-kinds.js';
4
+ import { activeBoardId } from '../state/boards-store';
5
+ import { updateNodeFromClient } from '../state/intent-bridge';
6
+ import type { CanvasNodeState } from '../types';
7
+
8
+ type WorkDraft = {
9
+ title: string;
10
+ state: AxWorkItemStatus;
11
+ detail: string;
12
+ progress: string;
13
+ boardId: string | null;
14
+ revision: number;
15
+ saving: boolean;
16
+ error: string;
17
+ };
18
+ // Like Markdown drafts, these belong to the qualified card, not its inline/focus renderer.
19
+ const drafts = new Map<string, Signal<WorkDraft | null>>();
20
+
21
+ export function hasUnsavedWorkCard(boardId: string, nodeId: string): boolean {
22
+ return !!drafts.get(JSON.stringify([boardId, nodeId]))?.value;
23
+ }
24
+
25
+ export function WorkCard({ node, state }: { node: CanvasNodeState; state: AxWorkItemStatus }) {
26
+ const key = JSON.stringify([activeBoardId.value, node.id]);
27
+ const editing = useMemo(() => drafts.get(key) ?? signal<WorkDraft | null>(null), [key]);
28
+ const draft = editing.value;
29
+ const saving = draft?.saving ?? false;
30
+ const error = draft?.error ?? '';
31
+ const setDraft = (next: WorkDraft | null) => {
32
+ if (next) drafts.set(key, editing);
33
+ else drafts.delete(key);
34
+ editing.value = next;
35
+ };
36
+ const detail = typeof node.data.detail === 'string' ? node.data.detail : '';
37
+ const progress = typeof node.data.progress === 'number' ? node.data.progress : null;
38
+
39
+ const edit = () => {
40
+ setDraft({
41
+ title: typeof node.data.title === 'string' ? node.data.title : '',
42
+ state,
43
+ detail,
44
+ progress: progress === null ? '' : String(progress),
45
+ boardId: activeBoardId.value,
46
+ revision: node.contentRevision ?? 0,
47
+ saving: false,
48
+ error: '',
49
+ });
50
+ };
51
+ const save = async () => {
52
+ if (!draft || saving) return;
53
+ setDraft({ ...draft, saving: true, error: '' });
54
+ const result = await updateNodeFromClient(node.id, {
55
+ title: draft.title.trim(),
56
+ data: {
57
+ workState: draft.state,
58
+ detail: draft.detail,
59
+ progress: draft.progress === '' ? null : Number(draft.progress),
60
+ },
61
+ expectedBoardId: draft.boardId,
62
+ expectedContentRevision: draft.revision,
63
+ });
64
+ if (result.ok) setDraft(null);
65
+ else
66
+ setDraft({
67
+ ...draft,
68
+ saving: false,
69
+ error: 'Could not save. Your draft is still here. Check the connection or whether the card changed.',
70
+ });
71
+ };
72
+
73
+ if (draft) {
74
+ return (
75
+ <form
76
+ class="work-card-editor"
77
+ onSubmit={(event) => {
78
+ event.preventDefault();
79
+ void save();
80
+ }}
81
+ onKeyDown={(event) => {
82
+ event.stopPropagation();
83
+ if (event.key === 'Escape' && !saving) {
84
+ event.preventDefault();
85
+ setDraft(null);
86
+ }
87
+ }}
88
+ >
89
+ <fieldset disabled={saving}>
90
+ <label>
91
+ Title
92
+ <input required value={draft.title} onInput={(e) => setDraft({ ...draft, title: e.currentTarget.value })} />
93
+ </label>
94
+ <label>
95
+ Work state
96
+ <select
97
+ value={draft.state}
98
+ onChange={(e) => setDraft({ ...draft, state: e.currentTarget.value as AxWorkItemStatus })}
99
+ >
100
+ {Object.entries(WORK_CARD_STATES).map(([value, item]) => (
101
+ <option key={value} value={value}>
102
+ {item.label}
103
+ </option>
104
+ ))}
105
+ </select>
106
+ </label>
107
+ <label>
108
+ Detail
109
+ <textarea value={draft.detail} onInput={(e) => setDraft({ ...draft, detail: e.currentTarget.value })} />
110
+ </label>
111
+ <label>
112
+ Progress (%) · optional
113
+ <input
114
+ type="number"
115
+ min="0"
116
+ max="100"
117
+ step="any"
118
+ value={draft.progress}
119
+ onInput={(e) => setDraft({ ...draft, progress: e.currentTarget.value })}
120
+ />
121
+ </label>
122
+ {error && (
123
+ <div class="work-card-error" role="alert">
124
+ {error}
125
+ </div>
126
+ )}
127
+ <footer>
128
+ <button type="button" onClick={() => setDraft(null)}>
129
+ Cancel
130
+ </button>
131
+ <button class="work-card-save" type="submit">
132
+ {saving ? 'Saving…' : 'Save'}
133
+ </button>
134
+ </footer>
135
+ </fieldset>
136
+ </form>
137
+ );
138
+ }
139
+
140
+ return (
141
+ <div class={`work-card work-card-${state}`}>
142
+ {typeof node.data.workItemId === 'string' && (
143
+ <div class="work-card-live">
144
+ ↗ Live task{node.data.liveWorkSource ? ` · ${node.data.liveWorkSource}` : ''} · Read-only
145
+ </div>
146
+ )}
147
+ <div class="work-card-state">
148
+ <span aria-hidden="true">{WORK_CARD_STATES[state].glyph}</span> {WORK_CARD_STATES[state].label}
149
+ </div>
150
+ {detail && <div class="work-card-detail">{detail}</div>}
151
+ {progress !== null && (
152
+ <div class="work-card-progress">
153
+ <span>{progress}% complete</span>
154
+ <div
155
+ class="status-progress"
156
+ role="progressbar"
157
+ aria-label="Progress"
158
+ aria-valuemin={0}
159
+ aria-valuemax={100}
160
+ aria-valuenow={progress}
161
+ >
162
+ <div style={{ width: `${progress}%`, background: 'currentColor' }} />
163
+ </div>
164
+ </div>
165
+ )}
166
+ {typeof node.data.workItemId !== 'string' && (
167
+ <footer>
168
+ <button type="button" onClick={edit}>
169
+ Edit
170
+ </button>
171
+ </footer>
172
+ )}
173
+ </div>
174
+ );
175
+ }
@@ -1,8 +1,8 @@
1
- import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
1
+ import { useEffect, useRef, useState } from 'preact/hooks';
2
2
  import { workbenchConnectionEpoch } from '../state/canvas-store';
3
3
  import { iframeMode } from '../state/iframe-mode';
4
- import { surfaceContentHash } from './surface-url';
5
4
  import { workbenchFetch } from '../state/workbench-transport';
5
+ import { VIEWER_LOAD_DEADLINE_MS, type ViewerSource } from './ViewerFrame';
6
6
 
7
7
  interface FrameDocumentCreateResponse {
8
8
  ok: boolean;
@@ -21,11 +21,12 @@ function isFrameDocumentCreateResponse(value: unknown): value is FrameDocumentCr
21
21
  );
22
22
  }
23
23
 
24
- export async function createIframeDocumentUrl(html: string, sandbox: string): Promise<string> {
24
+ export async function createIframeDocumentUrl(html: string, sandbox: string, signal?: AbortSignal): Promise<string> {
25
25
  const response = await workbenchFetch('/api/canvas/frame-documents', {
26
26
  method: 'POST',
27
27
  headers: { 'Content-Type': 'application/json' },
28
28
  body: JSON.stringify({ html, sandbox }),
29
+ signal,
29
30
  });
30
31
  const json = (await response.json()) as unknown;
31
32
  if (!response.ok || !isFrameDocumentCreateResponse(json) || !json.ok || typeof json.url !== 'string') {
@@ -38,32 +39,37 @@ export async function createIframeDocumentUrl(html: string, sandbox: string): Pr
38
39
  return json.url;
39
40
  }
40
41
 
41
- export function useIframeDocument(
42
- html: string,
43
- sandbox: string,
44
- ): { attributes: { src?: string; srcdoc?: string }; ready: boolean; key: string } {
42
+ export function useIframeDocument(html: string, sandbox: string): ViewerSource {
45
43
  // Boot-wide embed probe (state/iframe-mode.ts): srcdoc mode means src-URL
46
44
  // iframes are blocked here (nested-iframe hosts like Amp orb portals), so the
47
45
  // frame-documents round-trip would produce a URL the browser refuses to load.
48
46
  const mode = iframeMode.value;
49
47
  const [src, setSrc] = useState<string | null>(null);
48
+ const [error, setError] = useState(false);
50
49
  const [remintNonce, setRemintNonce] = useState(0);
51
50
  const mintEpochRef = useRef(0);
52
51
 
53
52
  useEffect(() => {
54
- setSrc(null);
53
+ setError(false);
55
54
  if (!html || mode !== 'src') return;
56
55
  let cancelled = false;
56
+ const controller = new AbortController();
57
+ const deadline = setTimeout(() => controller.abort(), VIEWER_LOAD_DEADLINE_MS);
57
58
  mintEpochRef.current = workbenchConnectionEpoch.value;
58
- void createIframeDocumentUrl(html, sandbox)
59
+ void createIframeDocumentUrl(html, sandbox, controller.signal)
59
60
  .then((url) => {
60
61
  if (!cancelled) setSrc(url);
61
62
  })
62
63
  .catch((error) => {
64
+ if (cancelled) return;
63
65
  console.error('[iframe-document] failed to create frame document:', error);
64
- });
66
+ setError(true);
67
+ })
68
+ .finally(() => clearTimeout(deadline));
65
69
  return () => {
66
70
  cancelled = true;
71
+ clearTimeout(deadline);
72
+ controller.abort();
67
73
  };
68
74
  }, [html, sandbox, mode, remintNonce]);
69
75
 
@@ -101,21 +107,8 @@ export function useIframeDocument(
101
107
  };
102
108
  }, [epoch, src, mode]);
103
109
 
104
- return useMemo(() => {
105
- if (mode === 'srcdoc' && html) {
106
- // The document already lives client-side, so serve it inline. Consumers
107
- // always render the iframe `sandbox` attribute, which supplies the
108
- // sandboxing the frame-document CSP header would have.
109
- return {
110
- attributes: { srcdoc: html },
111
- ready: true,
112
- key: `srcdoc-${surfaceContentHash(html)}`,
113
- };
114
- }
115
- return {
116
- attributes: src ? { src } : {},
117
- ready: Boolean(src),
118
- key: src ?? '',
119
- };
120
- }, [src, mode, html]);
110
+ if (!html || mode === null) return {};
111
+ // The host always supplies the sandbox, including on inline documents.
112
+ if (mode === 'srcdoc') return { srcdoc: html };
113
+ return { src: src ?? undefined, error, retry: () => setRemintNonce((value) => value + 1) };
121
114
  }
@@ -0,0 +1,97 @@
1
+ import type { RefObject } from 'preact';
2
+ import { useLayoutEffect } from 'preact/hooks';
3
+ import { HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
4
+ import { showToast } from '../state/attention-bridge';
5
+ import type { CanvasNodeState } from '../types';
6
+
7
+ type Owner = { nodeId: string; token: string; frame: RefObject<HTMLIFrameElement>; expanded: boolean };
8
+ const owners = new Set<Owner>();
9
+
10
+ export function canCopyPrimitiveJson(node: CanvasNodeState): boolean {
11
+ return (
12
+ node.type === 'html' && typeof node.data.htmlPrimitive === 'string' && node.data.htmlPrimitive !== 'presentation'
13
+ );
14
+ }
15
+
16
+ export function usePrimitiveJson(
17
+ node: CanvasNodeState,
18
+ token: string,
19
+ frame: RefObject<HTMLIFrameElement>,
20
+ expanded: boolean,
21
+ ): void {
22
+ const enabled = canCopyPrimitiveJson(node);
23
+ useLayoutEffect(() => {
24
+ if (!enabled) return;
25
+ const owner = { nodeId: node.id, token, frame, expanded };
26
+ owners.add(owner);
27
+ return () => {
28
+ owners.delete(owner);
29
+ };
30
+ }, [enabled, node.id, token, expanded]);
31
+ }
32
+
33
+ function readCurrentJson(nodeId: string): Promise<string> {
34
+ // The expanded document, when present, owns the latest interactive state.
35
+ const connected = [...owners].filter((owner) => owner.nodeId === nodeId && owner.frame.current?.isConnected);
36
+ const owner = connected.find((owner) => owner.expanded) ?? connected[0];
37
+ const frame = owner?.frame.current;
38
+ if (!owner || !frame?.contentWindow) return Promise.reject(new Error('Open the viewer before copying JSON.'));
39
+ const requestId = crypto.randomUUID();
40
+ return new Promise((resolve, reject) => {
41
+ const finish = (json?: string) => {
42
+ clearTimeout(timer);
43
+ window.removeEventListener('message', receive);
44
+ frame.removeEventListener('load', changed);
45
+ if (json !== undefined) resolve(json);
46
+ else reject(new Error('The viewer is unavailable or changed. Wait for it to load, then try again.'));
47
+ };
48
+ const changed = () => finish();
49
+ const receive = (event: MessageEvent) => {
50
+ const message = event.data;
51
+ if (
52
+ event.source !== frame.contentWindow ||
53
+ event.origin !== 'null' ||
54
+ message?.source !== HTML_SURFACE_PUSH_SOURCE ||
55
+ message.type !== 'copy-json-result' ||
56
+ message.token !== owner.token ||
57
+ message.requestId !== requestId
58
+ )
59
+ return;
60
+ finish(
61
+ owners.has(owner) && owner.frame.current === frame && frame.isConnected && typeof message.json === 'string'
62
+ ? message.json
63
+ : undefined,
64
+ );
65
+ };
66
+ const timer = setTimeout(changed, 3000);
67
+ window.addEventListener('message', receive);
68
+ frame.addEventListener('load', changed, { once: true });
69
+ // Opaque-origin sandbox: exact source + per-document nonce bind the reply.
70
+ frame.contentWindow!.postMessage(
71
+ { source: HTML_SURFACE_PUSH_SOURCE, type: 'copy-json', token: owner.token, requestId },
72
+ '*',
73
+ );
74
+ });
75
+ }
76
+
77
+ export async function copyPrimitiveJson(nodeId: string): Promise<void> {
78
+ try {
79
+ if (!navigator.clipboard) throw new Error('Clipboard access is unavailable in this browser.');
80
+ const json = readCurrentJson(nodeId);
81
+ // Keep clipboard.write inside the user's activation, including WebKit. The
82
+ // iframe supplies bytes, never clipboard permission or a stale server export.
83
+ void json.catch(() => {});
84
+ if (typeof ClipboardItem !== 'undefined' && navigator.clipboard.write) {
85
+ await navigator.clipboard.write([
86
+ new ClipboardItem({ 'text/plain': json.then((text) => new Blob([text], { type: 'text/plain' })) }),
87
+ ]);
88
+ } else {
89
+ await navigator.clipboard.writeText(await json);
90
+ }
91
+ showToast('context', 'JSON copied', '', [nodeId]);
92
+ } catch (error) {
93
+ showToast('remove', 'Could not copy JSON', error instanceof Error ? error.message : 'Clipboard access failed.', [
94
+ nodeId,
95
+ ]);
96
+ }
97
+ }
@@ -1,6 +1,11 @@
1
1
  import type { RefObject } from 'preact';
2
2
  import { useLayoutEffect } from 'preact/hooks';
3
- import { AX_SURFACE_ACK_SOURCE, AX_SURFACE_EMIT_SOURCE } from '../../shared/ax-surface-protocol.js';
3
+ import {
4
+ AX_SURFACE_ACK_SOURCE,
5
+ AX_SURFACE_EMIT_SOURCE,
6
+ readSurfaceNotification,
7
+ surfaceNotification,
8
+ } from '../../shared/ax-surface-protocol.js';
4
9
  import { showToast } from '../state/attention-bridge';
5
10
  import { submitAxInteractionFromClient } from '../state/intent-bridge';
6
11
 
@@ -23,8 +28,9 @@ export function useAxSurfaceBridge(options: {
23
28
  nodeId: string;
24
29
  sourceSurface: AxSurfaceBridgeSurface;
25
30
  iframeRef: RefObject<HTMLIFrameElement>;
31
+ jsonRpc?: boolean;
26
32
  }): void {
27
- const { enabled, token, nodeId, sourceSurface, iframeRef } = options;
33
+ const { enabled, token, nodeId, sourceSurface, iframeRef, jsonRpc = false } = options;
28
34
  // Register before paint: a fast iframe can accept input before passive effects
29
35
  // run, and its first emit must not be lost in that gap.
30
36
  useLayoutEffect(() => {
@@ -33,7 +39,7 @@ export function useAxSurfaceBridge(options: {
33
39
  // Bind to THIS node's own iframe; the nonce + nodeId are a second gate,
34
40
  // not the only one.
35
41
  if (event.source !== iframeRef.current?.contentWindow) return;
36
- const data = event.data as {
42
+ const data = (jsonRpc ? readSurfaceNotification(event.data) : event.data) as {
37
43
  source?: string;
38
44
  token?: string;
39
45
  nodeId?: string;
@@ -54,20 +60,18 @@ export function useAxSurfaceBridge(options: {
54
60
  }).then((res) => {
55
61
  // A refusal is toasted by the bridge with the server's reason.
56
62
  if (res.ok) showToast('context', 'AX interaction', interactionType, [nodeId]);
57
- iframeRef.current?.contentWindow?.postMessage(
58
- {
59
- source: AX_SURFACE_ACK_SOURCE,
60
- token,
61
- ...(data.correlationId ? { correlationId: data.correlationId } : {}),
62
- interaction: { type: interactionType },
63
- result: res,
64
- },
65
- '*',
66
- );
63
+ const message = {
64
+ source: AX_SURFACE_ACK_SOURCE,
65
+ token,
66
+ ...(data.correlationId ? { correlationId: data.correlationId } : {}),
67
+ interaction: { type: interactionType },
68
+ result: res,
69
+ };
70
+ iframeRef.current?.contentWindow?.postMessage(jsonRpc ? surfaceNotification(message) : message, '*');
67
71
  });
68
72
  }
69
73
  window.addEventListener('message', onAxMessage);
70
74
  return () => window.removeEventListener('message', onAxMessage);
71
75
  // eslint-disable-next-line react-hooks/exhaustive-deps
72
- }, [enabled, token, nodeId, sourceSurface]);
76
+ }, [enabled, token, nodeId, sourceSurface, jsonRpc]);
73
77
  }
@@ -22,21 +22,35 @@ export function useIframeContentHeight(
22
22
  node: CanvasNodeState,
23
23
  iframeRef: { current: HTMLIFrameElement | null },
24
24
  frameToken: string,
25
- ): void {
25
+ ): () => void {
26
26
  useEffect(() => {
27
27
  if (!frameToken) return undefined;
28
28
  let timer: ReturnType<typeof setTimeout> | null = null;
29
- let pendingHeight: number | null = null;
29
+ let pending: { frame: HTMLIFrameElement; height: number } | null = null;
30
+ let lastMeasurement: { frame: HTMLIFrameElement; height: number; width: number; chrome: number } | null = null;
30
31
  const held = () =>
31
32
  grabbingNodeId.value === node.id || otherHumans.value.some((human) => human.grabbingNodeId === node.id);
32
33
  function apply() {
33
34
  timer = null;
34
- if (held() || pendingHeight === null) return;
35
- const reported = pendingHeight;
36
- pendingHeight = null;
35
+ if (held() || pending === null) return;
36
+ const { frame, height: reported } = pending;
37
+ pending = null;
38
+ if (frame !== iframeRef.current) return;
37
39
  const current = nodes.value.get(node.id);
38
- if (!current) return;
39
- const target = computeContentGrowHeight(current, reported);
40
+ const card = frame.closest<HTMLElement>('.canvas-node');
41
+ if (!current || !card) return;
42
+ const chrome = card.offsetHeight - frame.clientHeight;
43
+ // Undo changes the viewport, which makes the reporter resend unchanged
44
+ // content. A replacement document owns a fresh fit even at the same size.
45
+ if (
46
+ lastMeasurement?.frame === frame &&
47
+ lastMeasurement.height === reported &&
48
+ lastMeasurement.width === current.size.width &&
49
+ lastMeasurement.chrome === chrome
50
+ )
51
+ return;
52
+ lastMeasurement = { frame, height: reported, width: current.size.width, chrome };
53
+ const target = computeContentGrowHeight(current, reported, chrome);
40
54
  if (target === null) return;
41
55
  const position = contentFitPosition(current, target, Array.from(nodes.value.values()));
42
56
  const moved = position.x !== current.position.x || position.y !== current.position.y;
@@ -49,18 +63,19 @@ export function useIframeContentHeight(
49
63
  }
50
64
  function schedule() {
51
65
  if (timer !== null) clearTimeout(timer);
52
- timer = pendingHeight !== null && !held() ? setTimeout(apply, 300) : null;
66
+ timer = pending !== null && !held() ? setTimeout(apply, 300) : null;
53
67
  }
54
68
  const dispose = effect(() => {
55
69
  held();
56
70
  schedule();
57
71
  });
58
72
  function onMessage(event: MessageEvent) {
59
- if (event.source !== iframeRef.current?.contentWindow) return;
73
+ const frame = iframeRef.current;
74
+ if (!frame || event.source !== frame.contentWindow) return;
60
75
  const d = event.data as { source?: string; type?: string; token?: string; height?: unknown } | null;
61
76
  if (!d || d.source !== 'pmx-canvas-frame' || d.type !== 'content-height' || d.token !== frameToken) return;
62
77
  if (typeof d.height !== 'number' || !Number.isFinite(d.height) || d.height <= 0) return;
63
- pendingHeight = d.height;
78
+ pending = { frame, height: d.height };
64
79
  schedule();
65
80
  }
66
81
  window.addEventListener('message', onMessage);
@@ -70,4 +85,13 @@ export function useIframeContentHeight(
70
85
  if (timer !== null) clearTimeout(timer);
71
86
  };
72
87
  }, [node.id, frameToken]);
88
+ // Pending documents can report before promotion, when they are deliberately
89
+ // not the active ref. Ask the promoted document for a fresh measurement.
90
+ return () => {
91
+ if (!frameToken) return;
92
+ iframeRef.current?.contentWindow?.postMessage(
93
+ { source: 'pmx-canvas-frame', type: 'measure-content', token: frameToken },
94
+ '*',
95
+ );
96
+ };
73
97
  }
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
2
  import { iframeMode } from '../state/iframe-mode';
3
3
  import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
4
+ import { VIEWER_LOAD_DEADLINE_MS, type ViewerSource } from './ViewerFrame';
4
5
 
5
6
  function isSameOriginUrl(url: string): boolean {
6
7
  if (isHostedWorkbench()) return url.startsWith('/') && !url.startsWith('//');
@@ -17,34 +18,45 @@ function isSameOriginUrl(url: string): boolean {
17
18
  * iframes are blocked by the embedding context (nested-iframe hosts like Amp
18
19
  * orb portals). Cross-origin URLs always stay `src` — they cannot be fetched
19
20
  * from here. While the probe is pending, same-origin frames stay empty rather
20
- * than loading a src that may show the broken placeholder; a failed srcdoc
21
- * fetch falls back to `src` (no worse than before).
21
+ * than loading a src that may show the broken placeholder. Preparation failures
22
+ * use the shared host's Retry, never a second load path the embed probe rejected.
22
23
  */
23
- export function useSurfaceFrame(url: string): { src?: string; srcdoc?: string } {
24
+ export function useSurfaceFrame(url: string): ViewerSource {
24
25
  const mode = iframeMode.value;
25
26
  const [doc, setDoc] = useState<{ url: string; html: string } | null>(null);
27
+ const [error, setError] = useState(false);
28
+ const [attempt, setAttempt] = useState(0);
26
29
  const sameOrigin = Boolean(url) && isSameOriginUrl(url);
27
30
  const wantsSrcdoc = mode === 'srcdoc' && sameOrigin;
28
31
 
29
32
  useEffect(() => {
33
+ setError(false);
30
34
  if (!wantsSrcdoc) return;
31
35
  let cancelled = false;
36
+ const controller = new AbortController();
37
+ const deadline = setTimeout(() => controller.abort(), VIEWER_LOAD_DEADLINE_MS);
32
38
  // Ask the server to inline bundle assets (the mermaid renderer): hosts
33
39
  // that force srcdoc mode block sub-frame subresource requests too.
34
- void workbenchFetch(`${url}${url.includes('?') ? '&' : '?'}inline-assets=1`, { credentials: 'same-origin' })
40
+ void workbenchFetch(`${url}${url.includes('?') ? '&' : '?'}inline-assets=1`, {
41
+ credentials: 'same-origin',
42
+ signal: controller.signal,
43
+ })
35
44
  .then((res) => (res.ok ? res.text() : Promise.reject(new Error(`HTTP ${res.status}`))))
36
45
  .then((html) => {
37
46
  if (!cancelled) setDoc({ url, html });
38
47
  })
39
48
  .catch((error) => {
40
- console.error('[surface-frame] srcdoc fetch failed, falling back to src:', url, error);
41
- // Empty html marks the src fallback below.
42
- if (!cancelled) setDoc({ url, html: '' });
43
- });
49
+ if (cancelled) return;
50
+ console.error('[surface-frame] document fetch failed:', url, error);
51
+ setError(true);
52
+ })
53
+ .finally(() => clearTimeout(deadline));
44
54
  return () => {
45
55
  cancelled = true;
56
+ clearTimeout(deadline);
57
+ controller.abort();
46
58
  };
47
- }, [wantsSrcdoc, url]);
59
+ }, [wantsSrcdoc, url, attempt]);
48
60
 
49
61
  if (!url) return {};
50
62
  if (!wantsSrcdoc) {
@@ -52,6 +64,5 @@ export function useSurfaceFrame(url: string): { src?: string; srcdoc?: string }
52
64
  }
53
65
  // Keep the previous document visible while a newer URL is being fetched —
54
66
  // same UX as src mode, where the old document stays until the new one loads.
55
- if (doc) return doc.html ? { srcdoc: doc.html } : { src: doc.url };
56
- return {};
67
+ return { srcdoc: doc?.html, error, retry: () => setAttempt((value) => value + 1) };
57
68
  }