pmx-canvas 0.10.0 → 0.11.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 (201) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +246 -108
  3. package/CHANGELOG.md +32 -0
  4. package/dist/canvas/chunks/abnfDiagram-N423BO3Z-p8dtp6va.js +1 -0
  5. package/dist/canvas/chunks/architecture-TIHT7OUA-9tffvpsv.js +1 -0
  6. package/dist/canvas/chunks/architectureDiagram-T3A2C74G-5jhmxrzf.js +36 -0
  7. package/dist/canvas/chunks/blockDiagram-VBNYF7ZC-pw1fa9yb.js +132 -0
  8. package/dist/canvas/chunks/c4Diagram-5PPSVZJV-x07nq0tt.js +10 -0
  9. package/dist/canvas/chunks/classDiagram-JCYQIIEL-nc6g5x1r.js +1 -0
  10. package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-0qnbpwvh.js +1 -0
  11. package/dist/canvas/chunks/cose-bilkent-JH36ORCC-nm39npq0.js +1 -0
  12. package/dist/canvas/chunks/cynefin-VYW2F7L2-ys8p46ts.js +1 -0
  13. package/dist/canvas/chunks/cynefinDiagram-MW4NZA55-vdxrwahz.js +62 -0
  14. package/dist/canvas/chunks/dagre-VZM6K2ZE-bqdm7wpd.js +4 -0
  15. package/dist/canvas/chunks/diagram-7IWD3JNH-gs5gpr93.js +30 -0
  16. package/dist/canvas/chunks/diagram-B4RE2ZJO-cgwjg47s.js +3 -0
  17. package/dist/canvas/chunks/diagram-LBJQPF4R-w3vahpav.js +24 -0
  18. package/dist/canvas/chunks/diagram-Q27KOJAE-42837z6h.js +24 -0
  19. package/dist/canvas/chunks/diagram-UB23O5K3-4fz83q59.js +41 -0
  20. package/dist/canvas/chunks/ebnfDiagram-BXEA7PRR-zphtgegj.js +1 -0
  21. package/dist/canvas/chunks/erDiagram-JOGREHBK-zvbbdxp4.js +85 -0
  22. package/dist/canvas/chunks/eventmodeling-45OFAUF4-v9q6dn4c.js +1 -0
  23. package/dist/canvas/chunks/flowDiagram-UKHOOZJN-yrr5z8tc.js +1 -0
  24. package/dist/canvas/chunks/ganttDiagram-PKOTCBZU-282jzycm.js +292 -0
  25. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-kdwb8m0a.js +1 -0
  26. package/dist/canvas/chunks/gitGraphDiagram-DS77QQ5N-vqqy6p1b.js +106 -0
  27. package/dist/canvas/chunks/index-0rk4gp1g.js +2 -0
  28. package/dist/canvas/chunks/index-194m3hc3.js +2 -0
  29. package/dist/canvas/chunks/index-1r72hdzb.js +301 -0
  30. package/dist/canvas/chunks/index-353m7b0p.js +2 -0
  31. package/dist/canvas/chunks/index-3cy1bzwh.js +2 -0
  32. package/dist/canvas/chunks/index-5tv8v1hk.js +1 -0
  33. package/dist/canvas/chunks/index-6ceya556.js +332 -0
  34. package/dist/canvas/chunks/index-6qmsnxwy.js +4 -0
  35. package/dist/canvas/chunks/index-d3b3gqe9.js +2 -0
  36. package/dist/canvas/chunks/index-fjtdggjn.js +2 -0
  37. package/dist/canvas/chunks/index-g3pwnqeb.js +2 -0
  38. package/dist/canvas/chunks/index-gf4e0et4.js +2 -0
  39. package/dist/canvas/chunks/index-hj99nz1f.js +1 -0
  40. package/dist/canvas/chunks/index-nprm96xp.js +16 -0
  41. package/dist/canvas/chunks/index-qj8fxwsf.js +2 -0
  42. package/dist/canvas/chunks/index-r2fyc9w1.js +89 -0
  43. package/dist/canvas/chunks/index-rft2hcz5.js +2 -0
  44. package/dist/canvas/chunks/index-rh7nv68n.js +2 -0
  45. package/dist/canvas/chunks/index-s4j678gj.js +2 -0
  46. package/dist/canvas/chunks/index-tb1wf525.js +179 -0
  47. package/dist/canvas/chunks/index-vv56h019.js +2 -0
  48. package/dist/canvas/chunks/index-w2mra17t.js +207 -0
  49. package/dist/canvas/chunks/index-wgv7en21.js +232 -0
  50. package/dist/canvas/chunks/index-yw8h9vha.js +157 -0
  51. package/dist/canvas/chunks/info-DKCQHKI2-wpzs6sz3.js +1 -0
  52. package/dist/canvas/chunks/infoDiagram-6WML65LV-v1yvggqj.js +2 -0
  53. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-5hnq1a9p.js +70 -0
  54. package/dist/canvas/chunks/journeyDiagram-NVQOT4AX-zjcffveh.js +139 -0
  55. package/dist/canvas/chunks/kanban-definition-27J2QSJJ-dakqtfp1.js +89 -0
  56. package/dist/canvas/chunks/katex-dmg8k4zm.js +257 -0
  57. package/dist/canvas/chunks/mermaid.core-j2geswrd.js +12 -0
  58. package/dist/canvas/chunks/mindmap-definition-FAOFIHXS-142xhr3k.js +96 -0
  59. package/dist/canvas/chunks/packet-7NZHBO7P-xn439f0s.js +1 -0
  60. package/dist/canvas/chunks/pegDiagram-VL7TDLO6-5wvk7vsd.js +1 -0
  61. package/dist/canvas/chunks/pie-RZYD4A2V-0kf16rwh.js +1 -0
  62. package/dist/canvas/chunks/pieDiagram-7S7Q4E2Y-ztdhwycc.js +39 -0
  63. package/dist/canvas/chunks/quadrantDiagram-CIZ2JOQS-c5p9gmg6.js +7 -0
  64. package/dist/canvas/chunks/radar-I7S5WNFK-0g2pz36t.js +1 -0
  65. package/dist/canvas/chunks/railroad-3IZDKUUU-fcaran6j.js +1 -0
  66. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-wgdya41q.js +1 -0
  67. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-gy07a4ka.js +1 -0
  68. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-ewk6wtkh.js +1 -0
  69. package/dist/canvas/chunks/railroadDiagram-AXF67PYL-xkmqkjwf.js +1 -0
  70. package/dist/canvas/chunks/requirementDiagram-LRYGKXZP-9wm2dyaw.js +84 -0
  71. package/dist/canvas/chunks/sankeyDiagram-W5VNT64P-2w19etgb.js +40 -0
  72. package/dist/canvas/chunks/sequenceDiagram-SI44F4Z6-0jn0vwmz.js +162 -0
  73. package/dist/canvas/chunks/sizeCapture-X5ZJPWSS-x75epwdq.js +1 -0
  74. package/dist/canvas/chunks/stateDiagram-OKZ733FA-schjpy6d.js +1 -0
  75. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-k8sfp2e7.js +1 -0
  76. package/dist/canvas/chunks/swimlanes-SLNWSIFB-gkw4xce3.js +1 -0
  77. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-nqysw67j.js +8 -0
  78. package/dist/canvas/chunks/timeline-definition-Z64GVDOM-x7nkcy01.js +120 -0
  79. package/dist/canvas/chunks/treeView-QDETBFTQ-cesw6gp0.js +1 -0
  80. package/dist/canvas/chunks/treemap-6X3UGDF4-zdv4knp9.js +1 -0
  81. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-x8dqn9yf.js +34 -0
  82. package/dist/canvas/chunks/wardley-OPB4EBWU-44pdddez.js +1 -0
  83. package/dist/canvas/chunks/wardleyDiagram-T6FBY63Y-t1j52t9r.js +78 -0
  84. package/dist/canvas/chunks/xychartDiagram-ELKLHX3M-38t16pme.js +7 -0
  85. package/dist/canvas/global.css +389 -229
  86. package/dist/canvas/index.js +141 -211
  87. package/dist/canvas/mcp-app.html +4205 -554
  88. package/dist/canvas/mermaid-entry.js +338 -338
  89. package/dist/canvas/surface-theme.css +6 -6
  90. package/dist/json-render/index.css +1 -1
  91. package/dist/json-render/index.js +49 -49
  92. package/dist/types/client/canvas/BarHint.d.ts +5 -3
  93. package/dist/types/client/canvas/CanvasNode.d.ts +0 -11
  94. package/dist/types/client/canvas/EdgeLayer.d.ts +1 -2
  95. package/dist/types/client/canvas/auto-fit.d.ts +4 -5
  96. package/dist/types/client/mermaid-entry.d.ts +1 -2
  97. package/dist/types/client/mermaid-render.d.ts +1 -0
  98. package/dist/types/client/nodes/BoardNode.d.ts +1 -1
  99. package/dist/types/client/nodes/ExtAppFrame.d.ts +7 -90
  100. package/dist/types/client/nodes/McpAppNode.d.ts +1 -25
  101. package/dist/types/client/nodes/MermaidNode.d.ts +1 -8
  102. package/dist/types/client/nodes/ViewerFrame.d.ts +29 -0
  103. package/dist/types/client/nodes/ViewerHost.d.ts +6 -0
  104. package/dist/types/client/nodes/iframe-document-url.d.ts +3 -9
  105. package/dist/types/client/nodes/primitive-copy.d.ts +5 -0
  106. package/dist/types/client/nodes/use-iframe-content-height.d.ts +1 -1
  107. package/dist/types/client/nodes/use-surface-frame.d.ts +4 -6
  108. package/dist/types/client/nodes/viewer-runtime-state.d.ts +2 -1
  109. package/dist/types/client/state/canvas-store.d.ts +3 -1
  110. package/dist/types/client/state/intent-bridge.d.ts +1 -0
  111. package/dist/types/client/state/presentation.d.ts +17 -6
  112. package/dist/types/json-render/server.d.ts +2 -1
  113. package/dist/types/server/canvas-state.d.ts +2 -0
  114. package/dist/types/server/index.d.ts +10 -5
  115. package/dist/types/server/operations/ops/boards.d.ts +0 -1
  116. package/dist/types/server/tour-control.d.ts +13 -3
  117. package/dist/types/shared/ax-surface-protocol.d.ts +0 -9
  118. package/dist/types/shared/tour.d.ts +8 -0
  119. package/docs/design/Chrome.dc.html +32 -6
  120. package/docs/design/Types1.dc.html +3 -2
  121. package/docs/design/Types2.dc.html +1 -1
  122. package/docs/design/Types3.dc.html +1 -1
  123. package/docs/design/Zoom.dc.html +37 -60
  124. package/docs/design/canvas.json +1 -1
  125. package/docs/design.md +169 -4
  126. package/docs/http-api.md +51 -1
  127. package/docs/mcp.md +8 -0
  128. package/docs/node-types.md +38 -3
  129. package/docs/plans/plan-009-tech-debt-backlog.md +2 -2
  130. package/docs/plans/plan-019-post-0.9-roadmap.md +283 -11
  131. package/docs/product-vision-2026-09.md +9 -0
  132. package/package.json +2 -2
  133. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  134. package/plugins/pmx-canvas/plugin.json +1 -1
  135. package/plugins/pmx-canvas/runtime.json +1 -1
  136. package/plugins/pmx-canvas/tools.json +246 -108
  137. package/skills/pmx-canvas/SKILL.md +11 -18
  138. package/skills/pmx-canvas/references/full-reference.md +9 -4
  139. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  140. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +3 -5
  141. package/src/client/canvas/BarHint.tsx +6 -3
  142. package/src/client/canvas/CanvasNode.tsx +34 -47
  143. package/src/client/canvas/CanvasViewport.tsx +6 -7
  144. package/src/client/canvas/ContextMenu.tsx +63 -3
  145. package/src/client/canvas/EdgeLayer.tsx +365 -169
  146. package/src/client/canvas/ExpandedNodeOverlay.tsx +1 -2
  147. package/src/client/canvas/Presentation.tsx +53 -3
  148. package/src/client/canvas/TopBar.tsx +24 -9
  149. package/src/client/canvas/auto-fit.ts +9 -11
  150. package/src/client/canvas/canvas-area.ts +1 -1
  151. package/src/client/canvas/use-focus-trap.ts +6 -1
  152. package/src/client/mermaid-entry.ts +6 -20
  153. package/src/client/mermaid-render.ts +39 -0
  154. package/src/client/nodes/AxStepControls.tsx +45 -69
  155. package/src/client/nodes/BoardNode.tsx +65 -9
  156. package/src/client/nodes/ContextNode.tsx +88 -396
  157. package/src/client/nodes/ExtAppFrame.tsx +283 -1462
  158. package/src/client/nodes/FileNode.tsx +92 -81
  159. package/src/client/nodes/HtmlNode.tsx +8 -36
  160. package/src/client/nodes/LedgerNode.tsx +31 -17
  161. package/src/client/nodes/MarkdownNode.tsx +8 -17
  162. package/src/client/nodes/McpAppNode.tsx +48 -154
  163. package/src/client/nodes/MermaidNode.tsx +80 -69
  164. package/src/client/nodes/PromptNode.tsx +12 -14
  165. package/src/client/nodes/ResponseNode.tsx +8 -6
  166. package/src/client/nodes/StatusNode.tsx +22 -7
  167. package/src/client/nodes/StatusSummary.tsx +5 -3
  168. package/src/client/nodes/TraceNode.tsx +8 -8
  169. package/src/client/nodes/ViewerFrame.tsx +262 -0
  170. package/src/client/nodes/ViewerHost.tsx +41 -0
  171. package/src/client/nodes/WebpageNode.tsx +22 -24
  172. package/src/client/nodes/iframe-document-url.ts +20 -27
  173. package/src/client/nodes/primitive-copy.ts +97 -0
  174. package/src/client/nodes/use-iframe-content-height.ts +34 -10
  175. package/src/client/nodes/use-surface-frame.ts +22 -11
  176. package/src/client/nodes/viewer-runtime-state.ts +41 -35
  177. package/src/client/state/canvas-store.ts +18 -12
  178. package/src/client/state/intent-bridge.ts +1 -0
  179. package/src/client/state/presentation.ts +135 -15
  180. package/src/client/state/sse-bridge.ts +18 -19
  181. package/src/client/theme/global.css +389 -229
  182. package/src/client/theme/surface-theme.css +6 -6
  183. package/src/json-render/charts/tufte-components.tsx +44 -2
  184. package/src/json-render/renderer/index.css +62 -2
  185. package/src/json-render/renderer/index.tsx +31 -7
  186. package/src/json-render/server.ts +3 -2
  187. package/src/server/board-export.ts +35 -24
  188. package/src/server/canvas-geometry.ts +1 -0
  189. package/src/server/canvas-state.ts +5 -0
  190. package/src/server/demo-state.json +106 -100
  191. package/src/server/html-primitives.ts +3 -9
  192. package/src/server/html-surface.ts +8 -0
  193. package/src/server/index.ts +19 -9
  194. package/src/server/operations/mcp-contracts.ts +17 -2
  195. package/src/server/operations/ops/boards.ts +1 -3
  196. package/src/server/operations/ops/viewport.ts +11 -9
  197. package/src/server/server.ts +7 -59
  198. package/src/server/tour-control.ts +66 -6
  199. package/src/shared/ax-surface-protocol.ts +0 -11
  200. package/src/shared/content-height-reporter.ts +4 -3
  201. package/src/shared/tour.ts +8 -0
@@ -812,13 +812,12 @@ function page(input: {
812
812
  * { box-sizing: border-box; }
813
813
  body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }
814
814
  .shell { max-width: 1160px; margin: 0 auto; }
815
- header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }
815
+ header.hero { margin-bottom: 20px; }
816
816
  .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }
817
817
  h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }
818
818
  h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }
819
819
  h3 { margin: 0 0 8px; font-size: 15px; }
820
820
  p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }
821
- .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
822
821
  button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }
823
822
  button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }
824
823
  table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }
@@ -861,20 +860,20 @@ function page(input: {
861
860
  .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }
862
861
  .preview { white-space: pre-wrap; min-height: 220px; }
863
862
  .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }
864
- @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }
863
+ @media (max-width: 760px) { body { padding: 14px; } .two, .three { grid-template-columns: 1fr; } .sticky { position: static; } }
865
864
  </style>
866
865
  </head>
867
866
  <body>
868
867
  <div class="shell">
869
868
  <header class="hero">
870
869
  <div><div class="kicker">PMX HTML primitive / ${escapeHtml(input.kind)}</div><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.summary)}</p></div>
871
- <div class="actions"><button type="button" data-copy-json>Copy JSON</button><button type="button" data-copy-prompt>Copy prompt</button></div>
872
870
  </header>
873
871
  ${input.body}
874
872
  </div>
875
873
  <script type="application/json" id="pmx-data">${safeJson(input.data)}</script>
876
874
  <script>
877
875
  const PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);
876
+ window.__pmxGetCopyJson = () => PMX_DATA;
878
877
  function fallbackCopy(text) {
879
878
  const el = document.createElement('textarea');
880
879
  el.value = text;
@@ -890,11 +889,6 @@ async function copyText(text) {
890
889
  if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);
891
890
  else fallbackCopy(text);
892
891
  }
893
- document.querySelector('[data-copy-json]')?.addEventListener('click', () => {
894
- const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;
895
- copyText(JSON.stringify(payload, null, 2));
896
- });
897
- document.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));
898
892
  ${input.script ?? ''}
899
893
  </script>
900
894
  </body>
@@ -61,6 +61,14 @@ const PMX_CANVAS_THEME_TOKEN = ${token};
61
61
  window.addEventListener('message', (event) => {
62
62
  const message = event.data;
63
63
  if (event.source !== window.parent || !message || message.source !== '${HTML_SURFACE_PUSH_SOURCE}' || message.token !== PMX_CANVAS_THEME_TOKEN) return;
64
+ if (message.type === 'copy-json' && PMX_CANVAS_THEME_TOKEN && typeof message.requestId === 'string') {
65
+ let json;
66
+ try {
67
+ if (typeof window.__pmxGetCopyJson === 'function') json = JSON.stringify(window.__pmxGetCopyJson(), null, 2);
68
+ } catch {}
69
+ window.parent.postMessage({ source: '${HTML_SURFACE_PUSH_SOURCE}', type: 'copy-json-result', token: PMX_CANVAS_THEME_TOKEN, requestId: message.requestId, json }, '*');
70
+ return;
71
+ }
64
72
  if (message.type === 'check-ready') {
65
73
  const reply = (type) => window.parent.postMessage({ source: '${HTML_SURFACE_PUSH_SOURCE}', type, token: PMX_CANVAS_THEME_TOKEN }, '*');
66
74
  reply('mounted');
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import type { Camera, Tour, TourStop } from '../shared/tour.js';
2
+ import type { Camera, Tour, TourPresenter, TourStop } from '../shared/tour.js';
3
3
  import {
4
4
  currentTour,
5
5
  exitTour,
@@ -8,9 +8,10 @@ import {
8
8
  type TourState,
9
9
  type TourStep,
10
10
  type TourStepResult,
11
+ type TourSuperseded,
11
12
  } from './tour-control.js';
12
- export type { Tour, TourStop } from '../shared/tour.js';
13
- export type { TourState, TourStep, TourStepResult } from './tour-control.js';
13
+ export type { Tour, TourPresenter, TourStop } from '../shared/tour.js';
14
+ export type { TourState, TourStep, TourStepResult, TourSuperseded } from './tour-control.js';
14
15
  import { agentPresence } from './agent-presence.js';
15
16
  import { checkFenceTarget, type FenceTarget } from './scope-fence.js';
16
17
  import type { AgentPhase, AgentPresence, AgentPresenceSnapshot } from '../shared/agent-presence.js';
@@ -631,16 +632,25 @@ export class PmxCanvas extends EventEmitter {
631
632
  }
632
633
 
633
634
  /** Present a tour stop (index, `next` or `previous`) in the workbench. Not recorded in undo history. */
634
- goToTourStop(step: TourStep, options?: { present?: boolean }): TourStepResult {
635
- const result = goToTourStop(step);
636
- emitPrimaryWorkbenchEvent('canvas-tour-step', { ...result, present: options?.present !== false });
635
+ goToTourStop(step: TourStep, options?: { present?: boolean; presenter?: undefined }): TourStepResult;
636
+ goToTourStop(
637
+ step: TourStep,
638
+ options: { present?: boolean; presenter?: TourPresenter },
639
+ ): TourStepResult | TourSuperseded;
640
+ goToTourStop(
641
+ step: TourStep,
642
+ options?: { present?: boolean; presenter?: TourPresenter },
643
+ ): TourStepResult | TourSuperseded {
644
+ const result = goToTourStop(step, options?.presenter);
645
+ if (!('superseded' in result))
646
+ emitPrimaryWorkbenchEvent('canvas-tour-step', { ...result, present: options?.present !== false });
637
647
  return result;
638
648
  }
639
649
 
640
650
  /** Leave presentation in every viewer. */
641
- exitTour(): void {
642
- exitTour();
643
- emitPrimaryWorkbenchEvent('canvas-tour-exit', {});
651
+ exitTour(presenter?: TourPresenter): void {
652
+ const result = exitTour(presenter);
653
+ if (!('superseded' in result)) emitPrimaryWorkbenchEvent('canvas-tour-exit', { ...result });
644
654
  }
645
655
 
646
656
  /** Ease the camera to a node, world rect or viewport. Not recorded in undo history. */
@@ -11,6 +11,7 @@ import { tourSchema, tourStopSchema } from '../../shared/tour.js';
11
11
  const id = z.string().min(1);
12
12
  const count = z.number().int().nonnegative();
13
13
  const ok = z.looseObject({ ok: z.boolean() });
14
+ const tourSuperseded = z.object({ ok: z.literal(true), superseded: z.literal(true) });
14
15
  const identified = z.looseObject({ id });
15
16
  const message = z.strictObject({ message: z.string() });
16
17
  const node = z.looseObject({
@@ -123,8 +124,22 @@ const payloads: Record<string, ZodType> = {
123
124
  'canvas.redo': history,
124
125
  'tour.get': z.looseObject({ tour: tourSchema, derived: z.boolean(), position: count.nullable() }),
125
126
  'tour.set': ok,
126
- 'tour.go': z.looseObject({ ok: z.boolean(), index: count, total: count, stop: tourStopSchema }),
127
- 'tour.exit': ok,
127
+ 'tour.go': z.union([
128
+ z.looseObject({
129
+ ok: z.boolean(),
130
+ catalogEpoch: z.string(),
131
+ boardGeneration: count,
132
+ sequence: count,
133
+ index: count,
134
+ total: count,
135
+ stop: tourStopSchema,
136
+ }),
137
+ tourSuperseded,
138
+ ]),
139
+ 'tour.exit': z.union([
140
+ z.looseObject({ ok: z.boolean(), catalogEpoch: z.string(), boardGeneration: count, sequence: count }),
141
+ tourSuperseded,
142
+ ]),
128
143
  'camera.move': z.looseObject({ ok: z.boolean(), stop: tourStopSchema, viewport }),
129
144
  arrange: z.looseObject({ ok: z.boolean(), layout: z.enum(['grid', 'column', 'flow']) }),
130
145
  'node.focus': z.looseObject({ ok: z.boolean(), focused: id, panned: z.boolean() }),
@@ -12,11 +12,10 @@
12
12
  *
13
13
  * This module must never import server.ts or index.ts.
14
14
  */
15
- import { randomUUID } from 'node:crypto';
16
15
  import { z } from 'zod';
17
16
  import { normalizeBoardFolder } from '../../../shared/boards.js';
18
17
  import { relationVocabularySchema } from '../../../shared/relations.js';
19
- import { canvasState, type CanvasLayout } from '../../canvas-state.js';
18
+ import { boardCatalogEpoch, canvasState, type CanvasLayout } from '../../canvas-state.js';
20
19
  import { openCanvasBoard } from '../../canvas-operations.js';
21
20
  import { summarizeNodeForAgentContext } from '../../../shared/agent-context.js';
22
21
  import { BRIEF_SUMMARY_LENGTH } from '../../context-brief.js';
@@ -25,7 +24,6 @@ import { defineOperation, OperationError, type Operation, type OperationContext
25
24
 
26
25
  /** The most cards a Home thumbnail draws (the largest first); the rest are noise at that size. */
27
26
  const PREVIEW_CARDS = 24;
28
- export const boardCatalogEpoch = randomUUID();
29
27
  let catalogRevision = 0;
30
28
  let reloadRevision = 0;
31
29
 
@@ -13,7 +13,7 @@
13
13
  * This module must never import server.ts or index.ts.
14
14
  */
15
15
  import { z } from 'zod';
16
- import { tourSchema, rectSchema, cameraSchema, tourStopSchema } from '../../../shared/tour.js';
16
+ import { tourSchema, rectSchema, cameraSchema, tourStopSchema, tourPresenterSchema } from '../../../shared/tour.js';
17
17
  import { canvasState } from '../../canvas-state.js';
18
18
  import {
19
19
  arrangeCanvasNodes,
@@ -339,6 +339,7 @@ const tourGetOperation = defineOperation({
339
339
  const tourGoShape = {
340
340
  stop: z.number().int().nonnegative().optional().describe('Zero-based stop index to go to'),
341
341
  step: z.enum(['next', 'previous']).optional().describe('Step relative to the current stop (clamped at the ends)'),
342
+ presenter: tourPresenterSchema.optional().describe('Document intent order and expected board activation'),
342
343
  present: z
343
344
  .boolean()
344
345
  .optional()
@@ -360,23 +361,24 @@ const tourGoOperation = defineOperation({
360
361
  if ((input.stop === undefined) === (input.step === undefined)) {
361
362
  throw new OperationError('Give exactly one of stop (index) or step ("next" | "previous").');
362
363
  }
363
- const result = goToTourStop(input.stop ?? (input.step as 'next' | 'previous'));
364
- ctx.emit('canvas-tour-step', { ...result, present: input.present !== false });
364
+ const result = goToTourStop(input.stop ?? (input.step as 'next' | 'previous'), input.presenter);
365
+ if (!('superseded' in result)) ctx.emit('canvas-tour-step', { ...result, present: input.present !== false });
365
366
  return { ok: true, ...result };
366
367
  },
367
368
  });
368
369
 
370
+ const tourExitShape = { presenter: tourGoShape.presenter };
369
371
  const tourExitOperation = defineOperation({
370
372
  name: 'tour.exit',
371
373
  mutates: false,
372
- input: z.object({}),
373
- inputShape: {},
374
+ input: z.object(tourExitShape),
375
+ inputShape: tourExitShape,
374
376
  http: { method: 'POST', path: '/api/canvas/tour/exit' },
375
377
  mcp: { toolName: 'canvas_tour_exit', description: 'Leave presentation in every viewer.' },
376
- handler: (_input, ctx) => {
377
- exitTour();
378
- ctx.emit('canvas-tour-exit', {});
379
- return { ok: true };
378
+ handler: (input, ctx) => {
379
+ const result = exitTour(input.presenter);
380
+ if (!('superseded' in result)) ctx.emit('canvas-tour-exit', { ...result });
381
+ return { ok: true, ...result };
380
382
  },
381
383
  });
382
384
 
@@ -47,8 +47,8 @@ import { basename, extname, isAbsolute, join, relative, resolve, sep } from 'nod
47
47
  import * as marked from 'marked';
48
48
  import { renderWikiMarkdown, resolveWikiLinks, wikiReferences } from './wiki-links.js';
49
49
  import { wikiLinksFromData } from '../shared/memory-graph.js';
50
- import { type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
51
- import { boardCatalogEpoch, boardsPayload } from './operations/ops/boards.js';
50
+ import { type CanvasNodeState, IMAGE_MIME_MAP, boardCatalogEpoch, canvasState } from './canvas-state.js';
51
+ import { boardsPayload } from './operations/ops/boards.js';
52
52
  import {
53
53
  buildAxBridge,
54
54
  buildAxStateBridge,
@@ -72,7 +72,7 @@ import { buildAgentContextPreamble } from '../shared/agent-context.js';
72
72
  import { buildCanvasAxSurfaceSnapshot } from './ax-context.js';
73
73
  import { resolveNodeAxCapabilities } from './ax-interaction.js';
74
74
  import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
75
- import { canvasThemeScheme, isCanvasTheme } from '../shared/themes.js';
75
+ import { isCanvasTheme } from '../shared/themes.js';
76
76
  import { canOpenNodeAsSurface } from '../shared/surface.js';
77
77
  import { validateLocalImageFile } from './image-source.js';
78
78
  import {
@@ -1504,44 +1504,6 @@ function handleIframeProbe(): Response {
1504
1504
  );
1505
1505
  }
1506
1506
 
1507
- // ── Ext-app recovery diagnostics (0.3.2 report Finding N) ──────
1508
- //
1509
- // The WebKit black-tile recovery trail is otherwise trapped inside host panels
1510
- // without devtools (the Copilot WKWebView). Clients mirror their bounded
1511
- // recovery log here; agents/testers read it back over HTTP. In-memory only,
1512
- // diagnostics-grade: no persistence, no snapshots.
1513
- const EXT_APP_RECOVERY_LOG_MAX = 500;
1514
- const EXT_APP_RECOVERY_FIELD_MAX = 300;
1515
- const extAppRecoveryEntries: Array<Record<string, unknown>> = [];
1516
-
1517
- /**
1518
- * Clamp a reported entry to the known trail shape ({ t, nodeId, event,
1519
- * visibility }) with bounded string lengths. The count trim below caps the
1520
- * number of entries; this caps bytes per entry, so the in-memory log stays
1521
- * diagnostics-sized no matter what a client posts.
1522
- */
1523
- function clampExtAppRecoveryEntry(entry: Record<string, unknown>): Record<string, unknown> {
1524
- const clamped: Record<string, unknown> = {};
1525
- if (typeof entry.t === 'number' && Number.isFinite(entry.t)) clamped.t = entry.t;
1526
- for (const key of ['nodeId', 'event', 'visibility'] as const) {
1527
- const value = entry[key];
1528
- if (typeof value === 'string') clamped[key] = value.slice(0, EXT_APP_RECOVERY_FIELD_MAX);
1529
- }
1530
- return clamped;
1531
- }
1532
-
1533
- function handleExtAppRecoveryReport(entries: unknown): Response {
1534
- if (!Array.isArray(entries)) return responseJson({ ok: false, error: 'entries must be an array' }, 400);
1535
- for (const entry of entries.slice(0, 100)) {
1536
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
1537
- extAppRecoveryEntries.push(clampExtAppRecoveryEntry(entry as Record<string, unknown>));
1538
- }
1539
- if (extAppRecoveryEntries.length > EXT_APP_RECOVERY_LOG_MAX) {
1540
- extAppRecoveryEntries.splice(0, extAppRecoveryEntries.length - EXT_APP_RECOVERY_LOG_MAX);
1541
- }
1542
- return responseJson({ ok: true, total: extAppRecoveryEntries.length });
1543
- }
1544
-
1545
1507
  // ── Node surfaces ("Open as site") ─────────────────────────────
1546
1508
  //
1547
1509
  // One stable, node-addressable URL — /api/canvas/surface/:nodeId — that serves
@@ -1841,14 +1803,7 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
1841
1803
  );
1842
1804
 
1843
1805
  const themeValue = url.searchParams.get('theme');
1844
- // The viewer bundle themes dark/light/high-contrast directly; other named
1845
- // canvas themes (midnight, sepia, …) collapse to their dark/light scheme.
1846
- const theme =
1847
- themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast'
1848
- ? themeValue
1849
- : isCanvasTheme(themeValue)
1850
- ? canvasThemeScheme(themeValue)
1851
- : undefined;
1806
+ const theme = themeValue === 'dark' || themeValue === 'light' || isCanvasTheme(themeValue) ? themeValue : 'harbor';
1852
1807
  const title = (node.data.title as string) || node.id;
1853
1808
  // Devtools panel is double-gated: the operator must opt in via the env flag
1854
1809
  // AND the request must carry ?devtools=1. Off by default in all normal runs.
@@ -2274,6 +2229,9 @@ function buildWorkbenchConnectSnapshot(
2274
2229
  timestamp: new Date().toISOString(),
2275
2230
  },
2276
2231
  });
2232
+ // Establish ownership in this ordered stream before layouts and live tour
2233
+ // commands. The separate catalog HTTP response may arrive after either.
2234
+ events.push({ event: 'boards-changed', payload: boardsPayload() });
2277
2235
  if (primaryWorkbenchPath) {
2278
2236
  events.push({
2279
2237
  event: 'workbench-open',
@@ -3571,16 +3529,6 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3571
3529
  return handleJsonRenderView(url);
3572
3530
  }
3573
3531
 
3574
- if (url.pathname === '/api/canvas/debug/ext-app-recovery' && req.method === 'POST') {
3575
- const body = await readJson(req);
3576
- if (body === null) return malformedJsonResponse();
3577
- return handleExtAppRecoveryReport(body.entries);
3578
- }
3579
-
3580
- if (url.pathname === '/api/canvas/debug/ext-app-recovery' && req.method === 'GET') {
3581
- return responseJson({ ok: true, entries: extAppRecoveryEntries });
3582
- }
3583
-
3584
3532
  if (url.pathname === '/api/canvas/frame-documents' && req.method === 'POST') {
3585
3533
  return handleCreateFrameDocument(req);
3586
3534
  }
@@ -8,19 +8,38 @@
8
8
  *
9
9
  * This module must never import server.ts or index.ts.
10
10
  */
11
- import { derivedTour, resolveStop, tourStopSchema, type Camera, type Tour, type TourStop } from '../shared/tour.js';
12
- import { canvasState } from './canvas-state.js';
11
+ import {
12
+ derivedTour,
13
+ resolveStop,
14
+ tourPresenterSchema,
15
+ tourStopSchema,
16
+ type Camera,
17
+ type Tour,
18
+ type TourPresenter,
19
+ type TourStop,
20
+ } from '../shared/tour.js';
21
+ import { boardCatalogEpoch, canvasState } from './canvas-state.js';
13
22
  import { getClientViewportSize } from './canvas-operations.js';
14
23
  import { OperationError } from './operations/types.js';
15
24
 
16
25
  export type TourStep = number | 'next' | 'previous';
17
26
 
18
- export interface TourStepResult {
27
+ export interface TourDelivery {
28
+ catalogEpoch: string;
29
+ boardGeneration: number;
30
+ sequence: number;
31
+ }
32
+
33
+ export interface TourStepResult extends TourDelivery {
19
34
  index: number;
20
35
  total: number;
21
36
  stop: TourStop;
22
37
  }
23
38
 
39
+ export interface TourSuperseded {
40
+ superseded: true;
41
+ }
42
+
24
43
  export interface TourState {
25
44
  tour: Tour;
26
45
  derived: boolean;
@@ -28,6 +47,35 @@ export interface TourState {
28
47
  }
29
48
 
30
49
  let cursor: { boardGeneration: number; tour: string; index: number } | null = null;
50
+ // One order for both HTTP acknowledgements and event delivery in this server run.
51
+ let sequence = 0;
52
+ let presenterGeneration = -1;
53
+ const presenterIntents = new Map<string, number>();
54
+
55
+ function claimPresenter(presenter: TourPresenter | undefined): boolean {
56
+ if (presenter === undefined) return true;
57
+ const { clientId, intent, catalogEpoch, boardGeneration } = tourPresenterSchema.parse(presenter);
58
+ if (catalogEpoch !== boardCatalogEpoch || boardGeneration !== canvasState.boardGeneration) return false;
59
+ if (presenterGeneration !== boardGeneration) {
60
+ presenterIntents.clear();
61
+ presenterGeneration = boardGeneration;
62
+ }
63
+ const previous = presenterIntents.get(clientId);
64
+ if (previous !== undefined && intent <= previous) return false;
65
+ // Never evict an identity: that would make its delayed requests valid again.
66
+ if (previous === undefined && presenterIntents.size >= 4096)
67
+ throw new OperationError(
68
+ 'Too many presentation documents on this board activation. Reopen the board to continue.',
69
+ 409,
70
+ );
71
+ // Even a later invalid stop supersedes older commands. Claim before resolving it.
72
+ presenterIntents.set(clientId, intent);
73
+ return true;
74
+ }
75
+
76
+ function nextDelivery(): TourDelivery {
77
+ return { catalogEpoch: boardCatalogEpoch, boardGeneration: canvasState.boardGeneration, sequence: ++sequence };
78
+ }
31
79
 
32
80
  export function currentTour(): TourState {
33
81
  const saved = canvasState.getTour();
@@ -44,7 +92,10 @@ function tourPosition(tour: Tour): number | null {
44
92
  }
45
93
 
46
94
  /** Move the shared cursor. `next`/`previous` clamp at the ends; an explicit index must exist. */
47
- export function goToTourStop(step: TourStep): TourStepResult {
95
+ export function goToTourStop(step: TourStep): TourStepResult;
96
+ export function goToTourStop(step: TourStep, presenter: TourPresenter | undefined): TourStepResult | TourSuperseded;
97
+ export function goToTourStop(step: TourStep, presenter?: TourPresenter): TourStepResult | TourSuperseded {
98
+ if (!claimPresenter(presenter)) return { superseded: true };
48
99
  const { tour, position: at } = currentTour();
49
100
  const { stops } = tour;
50
101
  if (stops.length === 0) {
@@ -56,11 +107,20 @@ export function goToTourStop(step: TourStep): TourStepResult {
56
107
  else if (Number.isInteger(step) && step >= 0 && step < stops.length) index = step;
57
108
  else throw new OperationError(`Tour stop ${step} does not exist (stops 0–${stops.length - 1}).`, 400);
58
109
  cursor = { boardGeneration: canvasState.boardGeneration, tour: JSON.stringify(tour), index };
59
- return { index, total: stops.length, stop: stops[index] as TourStop };
110
+ return {
111
+ ...nextDelivery(),
112
+ index,
113
+ total: stops.length,
114
+ stop: stops[index] as TourStop,
115
+ };
60
116
  }
61
117
 
62
- export function exitTour(): void {
118
+ export function exitTour(): TourDelivery;
119
+ export function exitTour(presenter: TourPresenter | undefined): TourDelivery | TourSuperseded;
120
+ export function exitTour(presenter?: TourPresenter): TourDelivery | TourSuperseded {
121
+ if (!claimPresenter(presenter)) return { superseded: true };
63
122
  cursor = null;
123
+ return nextDelivery();
64
124
  }
65
125
 
66
126
  /**
@@ -20,14 +20,3 @@ export const HTML_SURFACE_PUSH_SOURCE = 'pmx-canvas-html-node';
20
20
 
21
21
  /** Tab-local JSON viewer state handoff, independent of AX capabilities. */
22
22
  export const JSON_VIEWER_STATE_SOURCE = 'pmx-canvas-json-state';
23
-
24
- /** Ext-app boot beacon: the iframe's scripts executed (WebKit watchdog liveness). */
25
- export const EXT_APP_BOOT_BEACON_SOURCE = 'pmx-canvas-ext-app-alive';
26
-
27
- /**
28
- * Parent→ext-app paint probe (Finding N): the parent asks the app document to
29
- * answer with a double-rAF `paint-tick` beacon. A frame whose rendering
30
- * pipeline is live answers within a frame or two; silence past the timeout is
31
- * the paint-fail signal that drives the recovery ladder.
32
- */
33
- export const EXT_APP_PAINT_PROBE_SOURCE = 'pmx-canvas-ext-app-paint-probe';
@@ -22,11 +22,12 @@ export function sanitizeFrameToken(token: string): string {
22
22
  export function contentHeightReporterSource(frameToken: string): string {
23
23
  const token = JSON.stringify(sanitizeFrameToken(frameToken));
24
24
  return (
25
- `(function(){var T=${token};var last=0,timer=null;` +
25
+ `(function(){var T=${token};var last=0,lastViewport=0,timer=null;` +
26
26
  `function m(){var d=document.documentElement;return Math.max(d?d.scrollHeight:0,document.body?document.body.scrollHeight:0);}` +
27
- `function r(){var h=m();if(Math.abs(h-last)<=4)return;last=h;window.parent.postMessage({source:'pmx-canvas-frame',type:'content-height',token:T,height:h},'*');}` +
27
+ `function r(force){var h=m(),v=window.innerHeight;if(!force&&Math.abs(h-last)<=4&&v===lastViewport)return;last=h;lastViewport=v;window.parent.postMessage({source:'pmx-canvas-frame',type:'content-height',token:T,height:h},'*');}` +
28
28
  `function s(){if(timer)return;timer=setTimeout(function(){timer=null;r();},100);}` +
29
- `if(document.readyState!=='loading')s();window.addEventListener('load',s,true);` +
29
+ `window.addEventListener('message',function(e){var d=e.data;if(e.source===window.parent&&d&&d.source==='pmx-canvas-frame'&&d.type==='measure-content'&&d.token===T)r(true);});` +
30
+ `if(document.readyState!=='loading')s();window.addEventListener('load',s,true);window.addEventListener('resize',s);` +
30
31
  `try{new ResizeObserver(s).observe(document.documentElement);}catch(e){}` +
31
32
  `new MutationObserver(s).observe(document.documentElement,{subtree:true,childList:true,characterData:true,attributes:true});` +
32
33
  `if(document.fonts)document.fonts.ready.then(s);setTimeout(s,60);})();`
@@ -21,6 +21,14 @@ export const tourStopSchema = z.object({
21
21
  pullback: z.number().min(0).max(4).optional(),
22
22
  });
23
23
  export const tourSchema = z.object({ stops: z.array(tourStopSchema).max(1000) });
24
+ /** Latest intent from one document, scoped to the server's board activation. */
25
+ export const tourPresenterSchema = z.object({
26
+ clientId: z.string().min(1).max(128),
27
+ intent: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
28
+ catalogEpoch: z.string().min(1).max(128),
29
+ boardGeneration: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
30
+ });
31
+ export type TourPresenter = z.infer<typeof tourPresenterSchema>;
24
32
  export type Tour = z.infer<typeof tourSchema>;
25
33
  export type TourStop = z.infer<typeof tourStopSchema>;
26
34
  export type Camera = z.infer<typeof cameraSchema>;