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.
- package/.github/extensions/pmx-canvas/extension.mjs +3 -1
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +688 -128
- package/CHANGELOG.md +69 -0
- package/dist/amp-plugin/pmx-canvas/LICENSE +21 -0
- package/dist/amp-plugin/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
- package/dist/amp-plugin/pmx-canvas/index.js +5445 -0
- package/dist/amp-plugin/pmx-canvas/runtime.json +3 -0
- package/dist/amp-plugin/pmx-canvas/shared-runtime.mjs +194 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/SKILL.md +251 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/evals.json +1086 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/jwt.ts +17 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/login.ts +12 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/middleware.ts +13 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/routes/auth.ts +13 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/investigation-board/src/handlers/users.ts +27 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/mcp.json +36 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/ax-html-control-surface.md +101 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/codex-app-adapter.md +151 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +150 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/full-reference.md +1785 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/github-copilot-app-adapter.md +223 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/html-primitives.md +139 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/importing-context.md +132 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/installing-pmx-canvas.md +251 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/tours-and-recording.md +111 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/workbench-workflows.md +900 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-orchestration/SKILL.md +141 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-testing/SKILL.md +266 -0
- package/dist/amp-plugin/pmx-canvas/start-mcp.mjs +14857 -0
- package/dist/amp-plugin/pmx-canvas/tools.json +10963 -0
- package/dist/canvas/chunks/abnfDiagram-N423BO3Z-cwtsds5d.js +1 -0
- package/dist/canvas/chunks/architecture-TIHT7OUA-x63cwejs.js +1 -0
- package/dist/canvas/chunks/architectureDiagram-T3A2C74G-b4xc9tpz.js +36 -0
- package/dist/canvas/chunks/blockDiagram-VBNYF7ZC-ahp3yxxs.js +132 -0
- package/dist/canvas/chunks/c4Diagram-5PPSVZJV-nappnay7.js +10 -0
- package/dist/canvas/chunks/classDiagram-JCYQIIEL-k1j2xc1t.js +1 -0
- package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-33x08xf2.js +1 -0
- package/dist/canvas/chunks/cose-bilkent-JH36ORCC-a428syda.js +1 -0
- package/dist/canvas/chunks/cynefin-VYW2F7L2-8v6jxfz8.js +1 -0
- package/dist/canvas/chunks/cynefinDiagram-MW4NZA55-nspsqebj.js +62 -0
- package/dist/canvas/chunks/dagre-VZM6K2ZE-8y6177fh.js +4 -0
- package/dist/canvas/chunks/diagram-7IWD3JNH-vym028wn.js +30 -0
- package/dist/canvas/chunks/diagram-B4RE2ZJO-jddqb8sy.js +3 -0
- package/dist/canvas/chunks/diagram-LBJQPF4R-bjwbpkrn.js +24 -0
- package/dist/canvas/chunks/diagram-Q27KOJAE-j6ph6hcx.js +24 -0
- package/dist/canvas/chunks/diagram-UB23O5K3-p09w8jej.js +41 -0
- package/dist/canvas/chunks/ebnfDiagram-BXEA7PRR-j975ycq5.js +1 -0
- package/dist/canvas/chunks/erDiagram-JOGREHBK-sxp6hjn8.js +85 -0
- package/dist/canvas/chunks/eventmodeling-45OFAUF4-xzfgn6xg.js +1 -0
- package/dist/canvas/chunks/flowDiagram-UKHOOZJN-2nsmkskb.js +1 -0
- package/dist/canvas/chunks/ganttDiagram-PKOTCBZU-4tv230y2.js +292 -0
- package/dist/canvas/chunks/gitGraph-TEB2WS4Q-macbhand.js +1 -0
- package/dist/canvas/chunks/gitGraphDiagram-DS77QQ5N-mqgmnw9g.js +106 -0
- package/dist/canvas/chunks/index-037z5dz2.js +232 -0
- package/dist/canvas/chunks/index-081n3har.js +207 -0
- package/dist/canvas/chunks/index-5n03dmb9.js +2 -0
- package/dist/canvas/chunks/index-5tv8v1hk.js +1 -0
- package/dist/canvas/chunks/index-60pgzdqe.js +157 -0
- package/dist/canvas/chunks/index-6ceya556.js +332 -0
- package/dist/canvas/chunks/index-8g1vb57n.js +2 -0
- package/dist/canvas/chunks/index-8rjes9r6.js +2 -0
- package/dist/canvas/chunks/index-9022d1n9.js +16 -0
- package/dist/canvas/chunks/index-9b02042t.js +2 -0
- package/dist/canvas/chunks/index-ap4q4vez.js +2 -0
- package/dist/canvas/chunks/index-dyp2nhd3.js +2 -0
- package/dist/canvas/chunks/index-frvta4tm.js +2 -0
- package/dist/canvas/chunks/index-gf4e0et4.js +2 -0
- package/dist/canvas/chunks/index-h9ycj8n2.js +2 -0
- package/dist/canvas/chunks/index-hj99nz1f.js +1 -0
- package/dist/canvas/chunks/index-mergmj97.js +2 -0
- package/dist/canvas/chunks/index-nwsya6dj.js +301 -0
- package/dist/canvas/chunks/index-nz8qbd1p.js +2 -0
- package/dist/canvas/chunks/index-qhfrazv3.js +2 -0
- package/dist/canvas/chunks/index-v119nznk.js +4 -0
- package/dist/canvas/chunks/index-vpt37m3d.js +89 -0
- package/dist/canvas/chunks/index-y9ma6azy.js +2 -0
- package/dist/canvas/chunks/index-zx0j3a2z.js +179 -0
- package/dist/canvas/chunks/info-DKCQHKI2-a0ykzs9z.js +1 -0
- package/dist/canvas/chunks/infoDiagram-6WML65LV-qv97qq6a.js +2 -0
- package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-p9306gh0.js +70 -0
- package/dist/canvas/chunks/journeyDiagram-NVQOT4AX-sv67zats.js +139 -0
- package/dist/canvas/chunks/kanban-definition-27J2QSJJ-k2pfam6p.js +89 -0
- package/dist/canvas/chunks/katex-dmg8k4zm.js +257 -0
- package/dist/canvas/chunks/mermaid.core-83r0fdce.js +12 -0
- package/dist/canvas/chunks/mindmap-definition-FAOFIHXS-vg2newm1.js +96 -0
- package/dist/canvas/chunks/packet-7NZHBO7P-tqnabcdv.js +1 -0
- package/dist/canvas/chunks/pegDiagram-VL7TDLO6-k2h7zc5n.js +1 -0
- package/dist/canvas/chunks/pie-RZYD4A2V-wke6gyhe.js +1 -0
- package/dist/canvas/chunks/pieDiagram-7S7Q4E2Y-hg169p1v.js +39 -0
- package/dist/canvas/chunks/quadrantDiagram-CIZ2JOQS-bxtvsq0b.js +7 -0
- package/dist/canvas/chunks/radar-I7S5WNFK-w7ka0drw.js +1 -0
- package/dist/canvas/chunks/railroad-3IZDKUUU-y3vg318x.js +1 -0
- package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-6hqvcz3c.js +1 -0
- package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-vcae260m.js +1 -0
- package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-0dbyme23.js +1 -0
- package/dist/canvas/chunks/railroadDiagram-AXF67PYL-njyhcp4p.js +1 -0
- package/dist/canvas/chunks/requirementDiagram-LRYGKXZP-2z6v09gm.js +84 -0
- package/dist/canvas/chunks/sankeyDiagram-W5VNT64P-15mzam0e.js +40 -0
- package/dist/canvas/chunks/sequenceDiagram-SI44F4Z6-rbshyd2c.js +162 -0
- package/dist/canvas/chunks/sizeCapture-X5ZJPWSS-sg26xwts.js +1 -0
- package/dist/canvas/chunks/stateDiagram-OKZ733FA-6eahp98e.js +1 -0
- package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-tty0ggn5.js +1 -0
- package/dist/canvas/chunks/swimlanes-SLNWSIFB-ze5db10t.js +1 -0
- package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-thze75gq.js +8 -0
- package/dist/canvas/chunks/timeline-definition-Z64GVDOM-vnp7jyy8.js +120 -0
- package/dist/canvas/chunks/treeView-QDETBFTQ-6ysffnm6.js +1 -0
- package/dist/canvas/chunks/treemap-6X3UGDF4-m3h9p066.js +1 -0
- package/dist/canvas/chunks/vennDiagram-T6HMQDX7-0qrnb7mj.js +34 -0
- package/dist/canvas/chunks/wardley-OPB4EBWU-dp37drwx.js +1 -0
- package/dist/canvas/chunks/wardleyDiagram-T6FBY63Y-8nxg8wqt.js +78 -0
- package/dist/canvas/chunks/xychartDiagram-ELKLHX3M-nj921e7s.js +7 -0
- package/dist/canvas/global.css +483 -230
- package/dist/canvas/index.js +158 -219
- package/dist/canvas/mcp-app.html +4301 -548
- package/dist/canvas/mermaid-entry.js +338 -338
- package/dist/canvas/surface-theme.css +6 -6
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +49 -49
- package/dist/types/client/canvas/BarHint.d.ts +5 -3
- package/dist/types/client/canvas/BoardMoveDialog.d.ts +2 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +0 -11
- package/dist/types/client/canvas/EdgeEditor.d.ts +19 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +1 -2
- package/dist/types/client/canvas/GroupNavigation.d.ts +2 -0
- package/dist/types/client/canvas/TextPrompt.d.ts +4 -0
- package/dist/types/client/canvas/auto-fit.d.ts +4 -5
- package/dist/types/client/mermaid-entry.d.ts +1 -2
- package/dist/types/client/mermaid-render.d.ts +1 -0
- package/dist/types/client/nodes/BoardNode.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +7 -90
- package/dist/types/client/nodes/McpAppNode.d.ts +1 -25
- package/dist/types/client/nodes/MermaidNode.d.ts +1 -8
- package/dist/types/client/nodes/PromptNode.d.ts +1 -0
- package/dist/types/client/nodes/ViewerFrame.d.ts +29 -0
- package/dist/types/client/nodes/ViewerHost.d.ts +6 -0
- package/dist/types/client/nodes/WorkCard.d.ts +7 -0
- package/dist/types/client/nodes/iframe-document-url.d.ts +3 -9
- package/dist/types/client/nodes/primitive-copy.d.ts +5 -0
- package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +1 -0
- package/dist/types/client/nodes/use-iframe-content-height.d.ts +1 -1
- package/dist/types/client/nodes/use-surface-frame.d.ts +4 -6
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +2 -1
- package/dist/types/client/state/canvas-store.d.ts +18 -1
- package/dist/types/client/state/human-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +5 -2
- package/dist/types/client/state/presentation.d.ts +17 -6
- package/dist/types/client/state/relation-store.d.ts +11 -0
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/json-render/catalog.d.ts +2 -2
- package/dist/types/json-render/server.d.ts +2 -1
- package/dist/types/server/agent-presence.d.ts +1 -0
- package/dist/types/server/ax-interaction.d.ts +1 -0
- package/dist/types/server/ax-state-manager.d.ts +10 -2
- package/dist/types/server/board-local.d.ts +4 -0
- package/dist/types/server/board-move.d.ts +38 -0
- package/dist/types/server/canvas-db.d.ts +12 -2
- package/dist/types/server/canvas-operations.d.ts +5 -6
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +86 -20
- package/dist/types/server/card-relocation.d.ts +6 -0
- package/dist/types/server/context-reads.d.ts +3 -1
- package/dist/types/server/human-presence.d.ts +1 -1
- package/dist/types/server/index.d.ts +26 -10
- package/dist/types/server/intent-registry.d.ts +8 -4
- package/dist/types/server/knowledge-lifecycle.d.ts +4 -2
- package/dist/types/server/mutation-history.d.ts +7 -5
- package/dist/types/server/operations/ops/boards.d.ts +0 -1
- package/dist/types/server/operations/registry.d.ts +2 -0
- package/dist/types/server/tour-control.d.ts +13 -3
- package/dist/types/server/trace-manager.d.ts +1 -0
- package/dist/types/shared/ax-kinds.d.ts +6 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +8 -9
- package/dist/types/shared/human-presence.d.ts +1 -0
- package/dist/types/shared/lifecycle-status.d.ts +2 -0
- package/dist/types/shared/tour.d.ts +8 -0
- package/docs/RELEASE.md +21 -14
- package/docs/agent-plugin.md +16 -2
- package/docs/amp-plugin.md +271 -0
- package/docs/cli.md +42 -3
- package/docs/design/Chrome.dc.html +32 -6
- package/docs/design/GuestName.dc.html +50 -0
- package/docs/design/PluginLogo.svg +7 -0
- package/docs/design/Types1.dc.html +3 -2
- package/docs/design/Types2.dc.html +1 -1
- package/docs/design/Types3.dc.html +1 -1
- package/docs/design/WorkCards.dc.html +39 -0
- package/docs/design/Zoom.dc.html +37 -60
- package/docs/design/canvas.json +17 -1
- package/docs/design/plugin-logo.png +0 -0
- package/docs/design.md +280 -12
- package/docs/evals/agents-md-sections.md +38 -0
- package/docs/http-api.md +179 -11
- package/docs/mcp.md +69 -9
- package/docs/node-types.md +109 -4
- package/docs/plans/plan-009-tech-debt-backlog.md +3 -3
- package/docs/plans/plan-019-post-0.9-roadmap.md +327 -31
- package/docs/plans/plan-020-wave-5-work-planning.md +553 -0
- package/docs/product-vision-2026-09.md +42 -17
- package/docs/sdk.md +18 -0
- package/package.json +7 -4
- package/plugins/pmx-canvas/assets/preview.png +0 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +3 -1
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +2 -2
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/tools.json +688 -128
- package/skills/data-analysis/SKILL.md +13 -26
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +3 -0
- package/skills/pmx-canvas/SKILL.md +232 -849
- package/skills/pmx-canvas/evals/evals.json +26 -3
- package/skills/pmx-canvas/references/full-reference.md +89 -24
- package/skills/pmx-canvas/references/html-primitives.md +7 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas/references/tours-and-recording.md +29 -2
- package/skills/pmx-canvas/references/workbench-workflows.md +900 -0
- package/skills/pmx-canvas-testing/SKILL.md +7 -5
- package/skills/published-consumer-e2e/SKILL.md +25 -8
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +51 -77
- package/skills/web-artifacts-builder/LICENSE.txt +202 -0
- package/skills/web-artifacts-builder/SKILL.md +6 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +2 -2
- package/src/cli/commands/apps.ts +1 -0
- package/src/cli/commands/boards.ts +30 -0
- package/src/client/App.tsx +20 -4
- package/src/client/canvas/AttentionHistory.tsx +10 -2
- package/src/client/canvas/BarHint.tsx +6 -3
- package/src/client/canvas/BoardMoveDialog.tsx +225 -0
- package/src/client/canvas/CanvasNode.tsx +79 -49
- package/src/client/canvas/CanvasViewport.tsx +42 -14
- package/src/client/canvas/ContextMenu.tsx +97 -15
- package/src/client/canvas/EdgeEditor.tsx +149 -0
- package/src/client/canvas/EdgeLayer.tsx +441 -174
- package/src/client/canvas/ExpandedNodeOverlay.tsx +5 -2
- package/src/client/canvas/GroupNavigation.tsx +85 -0
- package/src/client/canvas/Presentation.tsx +53 -3
- package/src/client/canvas/RelationsPanel.tsx +9 -0
- package/src/client/canvas/SelectionBar.tsx +21 -0
- package/src/client/canvas/SessionPanel.tsx +35 -1
- package/src/client/canvas/TextPrompt.tsx +18 -1
- package/src/client/canvas/TopBar.tsx +54 -9
- package/src/client/canvas/auto-fit.ts +9 -11
- package/src/client/canvas/canvas-area.ts +2 -2
- package/src/client/canvas/use-focus-trap.ts +6 -1
- package/src/client/mermaid-entry.ts +6 -20
- package/src/client/mermaid-render.ts +39 -0
- package/src/client/nodes/AxStepControls.tsx +45 -69
- package/src/client/nodes/BoardNode.tsx +65 -9
- package/src/client/nodes/ContextNode.tsx +88 -396
- package/src/client/nodes/ExtAppFrame.tsx +302 -1465
- package/src/client/nodes/FileNode.tsx +92 -81
- package/src/client/nodes/HtmlNode.tsx +8 -36
- package/src/client/nodes/LedgerNode.tsx +31 -17
- package/src/client/nodes/MarkdownNode.tsx +14 -24
- package/src/client/nodes/McpAppNode.tsx +48 -154
- package/src/client/nodes/MermaidNode.tsx +80 -69
- package/src/client/nodes/PromptNode.tsx +44 -17
- package/src/client/nodes/ResponseNode.tsx +8 -6
- package/src/client/nodes/StatusNode.tsx +34 -7
- package/src/client/nodes/StatusSummary.tsx +16 -3
- package/src/client/nodes/TraceNode.tsx +8 -8
- package/src/client/nodes/ViewerFrame.tsx +273 -0
- package/src/client/nodes/ViewerHost.tsx +41 -0
- package/src/client/nodes/WebpageNode.tsx +22 -24
- package/src/client/nodes/WorkCard.tsx +175 -0
- package/src/client/nodes/iframe-document-url.ts +20 -27
- package/src/client/nodes/primitive-copy.ts +97 -0
- package/src/client/nodes/use-ax-surface-bridge.ts +18 -14
- package/src/client/nodes/use-iframe-content-height.ts +34 -10
- package/src/client/nodes/use-surface-frame.ts +22 -11
- package/src/client/nodes/viewer-runtime-state.ts +41 -35
- package/src/client/state/attention-bridge.ts +5 -0
- package/src/client/state/attention-store.ts +1 -0
- package/src/client/state/canvas-store.ts +202 -26
- package/src/client/state/human-store.ts +22 -2
- package/src/client/state/intent-bridge.ts +20 -5
- package/src/client/state/presentation.ts +135 -15
- package/src/client/state/relation-store.ts +127 -1
- package/src/client/state/sse-bridge.ts +18 -19
- package/src/client/theme/global.css +483 -230
- package/src/client/theme/surface-theme.css +6 -6
- package/src/client/types.ts +1 -0
- package/src/json-render/charts/tufte-components.tsx +44 -2
- package/src/json-render/renderer/index.css +62 -2
- package/src/json-render/renderer/index.tsx +31 -7
- package/src/json-render/server.ts +3 -2
- package/src/mcp/server.ts +13 -3
- package/src/server/agent-presence.ts +43 -23
- package/src/server/ax-flow-loop.ts +6 -0
- package/src/server/ax-gate-ttl.ts +8 -1
- package/src/server/ax-interaction.ts +16 -10
- package/src/server/ax-state-manager.ts +17 -4
- package/src/server/ax-wait.ts +20 -6
- package/src/server/board-export.ts +54 -30
- package/src/server/board-local.ts +20 -0
- package/src/server/board-move.ts +297 -0
- package/src/server/canvas-db.ts +249 -158
- package/src/server/canvas-geometry.ts +1 -0
- package/src/server/canvas-operations.ts +139 -109
- package/src/server/canvas-schema.ts +21 -0
- package/src/server/canvas-serialization.ts +2 -0
- package/src/server/canvas-state.ts +797 -159
- package/src/server/card-relocation.ts +53 -0
- package/src/server/code-graph.ts +4 -3
- package/src/server/context-reads.ts +83 -55
- package/src/server/demo-state.json +140 -114
- package/src/server/file-watcher.ts +30 -10
- package/src/server/html-primitives.ts +3 -9
- package/src/server/html-surface.ts +8 -0
- package/src/server/human-presence.ts +18 -5
- package/src/server/index.ts +54 -24
- package/src/server/intent-registry.ts +43 -5
- package/src/server/knowledge-lifecycle.ts +32 -9
- package/src/server/lifecycle-status.ts +29 -1
- package/src/server/mcp-app-runtime.ts +26 -7
- package/src/server/mutation-history.ts +37 -8
- package/src/server/operations/composites.ts +6 -3
- package/src/server/operations/mcp-contracts.ts +32 -2
- package/src/server/operations/mcp.ts +36 -9
- package/src/server/operations/ops/annotation.ts +22 -1
- package/src/server/operations/ops/ax-await.ts +1 -0
- package/src/server/operations/ops/ax-timeline.ts +28 -2
- package/src/server/operations/ops/batch.ts +25 -11
- package/src/server/operations/ops/boards.ts +45 -6
- package/src/server/operations/ops/nodes.ts +70 -0
- package/src/server/operations/ops/query.ts +32 -3
- package/src/server/operations/ops/snapshots.ts +1 -0
- package/src/server/operations/ops/viewport.ts +11 -9
- package/src/server/operations/registry.ts +49 -4
- package/src/server/server.ts +69 -75
- package/src/server/tour-control.ts +97 -12
- package/src/server/trace-manager.ts +4 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +2 -2
- package/src/shared/agent-context.ts +7 -1
- package/src/shared/auto-arrange.ts +5 -4
- package/src/shared/ax-kinds.ts +13 -0
- package/src/shared/ax-surface-protocol.ts +20 -9
- package/src/shared/content-height-reporter.ts +4 -3
- package/src/shared/human-presence.ts +1 -0
- package/src/shared/lifecycle-status.ts +2 -0
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 {
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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() ||
|
|
35
|
-
const reported =
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
21
|
-
*
|
|
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):
|
|
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`, {
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
56
|
-
return {};
|
|
67
|
+
return { srcdoc: doc?.html, error, retry: () => setAttempt((value) => value + 1) };
|
|
57
68
|
}
|