pmx-canvas 0.9.3 → 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 (237) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +9418 -2016
  3. package/CHANGELOG.md +54 -1
  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/mcp/canvas-access.d.ts +1 -12
  114. package/dist/types/mcp/resource-subscriptions.d.ts +7 -0
  115. package/dist/types/mcp/server.d.ts +2 -0
  116. package/dist/types/server/ax-wait.d.ts +1 -0
  117. package/dist/types/server/canvas-operations.d.ts +1 -0
  118. package/dist/types/server/canvas-state.d.ts +2 -0
  119. package/dist/types/server/index.d.ts +11 -6
  120. package/dist/types/server/operations/http.d.ts +10 -0
  121. package/dist/types/server/operations/index.d.ts +2 -2
  122. package/dist/types/server/operations/invoker.d.ts +4 -3
  123. package/dist/types/server/operations/mcp-contracts.d.ts +20 -0
  124. package/dist/types/server/operations/mcp.d.ts +2 -0
  125. package/dist/types/server/operations/ops/batch.d.ts +1 -0
  126. package/dist/types/server/operations/ops/boards.d.ts +0 -1
  127. package/dist/types/server/operations/registry.d.ts +2 -2
  128. package/dist/types/server/operations/types.d.ts +12 -2
  129. package/dist/types/server/server.d.ts +2 -0
  130. package/dist/types/server/tour-control.d.ts +13 -3
  131. package/dist/types/server/webpage-node.d.ts +1 -0
  132. package/dist/types/shared/ax-surface-protocol.d.ts +0 -9
  133. package/dist/types/shared/tour.d.ts +8 -0
  134. package/docs/api-reference.md +2 -2
  135. package/docs/cli.md +1 -1
  136. package/docs/design/Chrome.dc.html +32 -6
  137. package/docs/design/Types1.dc.html +3 -2
  138. package/docs/design/Types2.dc.html +1 -1
  139. package/docs/design/Types3.dc.html +1 -1
  140. package/docs/design/Zoom.dc.html +37 -60
  141. package/docs/design/canvas.json +1 -1
  142. package/docs/design.md +169 -4
  143. package/docs/http-api.md +82 -1
  144. package/docs/mcp.md +101 -21
  145. package/docs/node-types.md +38 -3
  146. package/docs/plans/plan-009-tech-debt-backlog.md +2 -2
  147. package/docs/plans/plan-019-post-0.9-roadmap.md +402 -13
  148. package/docs/product-vision-2026-09.md +9 -0
  149. package/docs/sdk.md +5 -2
  150. package/package.json +2 -2
  151. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  152. package/plugins/pmx-canvas/plugin.json +1 -1
  153. package/plugins/pmx-canvas/runtime.json +1 -1
  154. package/plugins/pmx-canvas/tools.json +9418 -2016
  155. package/skills/pmx-canvas/SKILL.md +19 -21
  156. package/skills/pmx-canvas/references/full-reference.md +44 -6
  157. package/skills/pmx-canvas/references/installing-pmx-canvas.md +7 -7
  158. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +3 -5
  159. package/src/client/canvas/BarHint.tsx +6 -3
  160. package/src/client/canvas/CanvasNode.tsx +34 -47
  161. package/src/client/canvas/CanvasViewport.tsx +6 -7
  162. package/src/client/canvas/ContextMenu.tsx +63 -3
  163. package/src/client/canvas/EdgeLayer.tsx +365 -169
  164. package/src/client/canvas/ExpandedNodeOverlay.tsx +1 -2
  165. package/src/client/canvas/Presentation.tsx +53 -3
  166. package/src/client/canvas/ToolRail.tsx +2 -0
  167. package/src/client/canvas/TopBar.tsx +24 -9
  168. package/src/client/canvas/auto-fit.ts +9 -11
  169. package/src/client/canvas/canvas-area.ts +1 -1
  170. package/src/client/canvas/use-focus-trap.ts +6 -1
  171. package/src/client/mermaid-entry.ts +6 -20
  172. package/src/client/mermaid-render.ts +39 -0
  173. package/src/client/nodes/AxStepControls.tsx +45 -69
  174. package/src/client/nodes/BoardNode.tsx +65 -9
  175. package/src/client/nodes/ContextNode.tsx +88 -396
  176. package/src/client/nodes/ExtAppFrame.tsx +283 -1462
  177. package/src/client/nodes/FileNode.tsx +92 -81
  178. package/src/client/nodes/HtmlNode.tsx +8 -36
  179. package/src/client/nodes/LedgerNode.tsx +31 -17
  180. package/src/client/nodes/MarkdownNode.tsx +8 -17
  181. package/src/client/nodes/McpAppNode.tsx +48 -154
  182. package/src/client/nodes/MermaidNode.tsx +80 -69
  183. package/src/client/nodes/PromptNode.tsx +12 -14
  184. package/src/client/nodes/ResponseNode.tsx +8 -6
  185. package/src/client/nodes/StatusNode.tsx +22 -7
  186. package/src/client/nodes/StatusSummary.tsx +5 -3
  187. package/src/client/nodes/TraceNode.tsx +8 -8
  188. package/src/client/nodes/ViewerFrame.tsx +262 -0
  189. package/src/client/nodes/ViewerHost.tsx +41 -0
  190. package/src/client/nodes/WebpageNode.tsx +22 -24
  191. package/src/client/nodes/iframe-document-url.ts +20 -27
  192. package/src/client/nodes/primitive-copy.ts +97 -0
  193. package/src/client/nodes/use-iframe-content-height.ts +34 -10
  194. package/src/client/nodes/use-surface-frame.ts +22 -11
  195. package/src/client/nodes/viewer-runtime-state.ts +41 -35
  196. package/src/client/state/canvas-store.ts +18 -12
  197. package/src/client/state/intent-bridge.ts +1 -0
  198. package/src/client/state/presentation.ts +135 -15
  199. package/src/client/state/sse-bridge.ts +18 -19
  200. package/src/client/theme/global.css +389 -229
  201. package/src/client/theme/surface-theme.css +6 -6
  202. package/src/json-render/charts/tufte-components.tsx +44 -2
  203. package/src/json-render/renderer/index.css +62 -2
  204. package/src/json-render/renderer/index.tsx +31 -7
  205. package/src/json-render/server.ts +3 -2
  206. package/src/mcp/canvas-access.ts +6 -47
  207. package/src/mcp/resource-subscriptions.ts +181 -0
  208. package/src/mcp/server.ts +185 -219
  209. package/src/server/ax-wait.ts +21 -6
  210. package/src/server/board-export.ts +35 -24
  211. package/src/server/canvas-geometry.ts +1 -0
  212. package/src/server/canvas-operations.ts +6 -2
  213. package/src/server/canvas-state.ts +5 -0
  214. package/src/server/demo-state.json +106 -100
  215. package/src/server/html-primitives.ts +3 -9
  216. package/src/server/html-surface.ts +8 -0
  217. package/src/server/index.ts +30 -12
  218. package/src/server/operations/http.ts +69 -21
  219. package/src/server/operations/index.ts +8 -1
  220. package/src/server/operations/invoker.ts +53 -5
  221. package/src/server/operations/mcp-contracts.ts +354 -0
  222. package/src/server/operations/mcp.ts +153 -18
  223. package/src/server/operations/ops/ax-await.ts +18 -10
  224. package/src/server/operations/ops/ax-read.ts +2 -0
  225. package/src/server/operations/ops/ax-timeline.ts +2 -2
  226. package/src/server/operations/ops/batch.ts +27 -8
  227. package/src/server/operations/ops/boards.ts +1 -3
  228. package/src/server/operations/ops/nodes.ts +18 -3
  229. package/src/server/operations/ops/viewport.ts +11 -9
  230. package/src/server/operations/registry.ts +24 -4
  231. package/src/server/operations/types.ts +16 -2
  232. package/src/server/server.ts +17 -59
  233. package/src/server/tour-control.ts +66 -6
  234. package/src/server/webpage-node.ts +9 -4
  235. package/src/shared/ax-surface-protocol.ts +0 -11
  236. package/src/shared/content-height-reporter.ts +4 -3
  237. package/src/shared/tour.ts +8 -0
@@ -1,4 +1,5 @@
1
- import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
1
+ import { createPortal } from 'preact/compat';
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'preact/hooks';
2
3
  import { BarHint } from '../canvas/BarHint';
3
4
  import { IconPlusCircle, IconRefresh } from '../icons';
4
5
  import { delimiterForPath, parseDelimitedText } from '../../shared/delimited-text';
@@ -85,6 +86,10 @@ function TextFileNode({ node, expanded = false }: { node: CanvasNodeState; expan
85
86
  const [loading, setLoading] = useState(!cachedContent && !!filePath && !servesBytes);
86
87
  const [error, setError] = useState<string | null>(null);
87
88
  const [bytesVersion, setBytesVersion] = useState(0);
89
+ const [header, setHeader] = useState<Element | null>(null);
90
+ useLayoutEffect(() => {
91
+ setHeader(expanded ? null : document.querySelector(`[data-file-toolbar="${node.id}"]`));
92
+ }, [node.id, expanded]);
88
93
 
89
94
  // Load file content on mount or when path changes
90
95
  useEffect(() => {
@@ -202,106 +207,112 @@ function TextFileNode({ node, expanded = false }: { node: CanvasNodeState; expan
202
207
  return <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>No file path set</div>;
203
208
  }
204
209
 
205
- return (
210
+ const toolbar = (
206
211
  <div
207
212
  style={{
208
213
  display: 'flex',
209
- flexDirection: 'column',
210
- height: '100%',
211
- fontFamily: 'var(--font-code)',
212
- fontSize: expanded ? '13px' : '11px',
214
+ alignItems: 'center',
215
+ gap: '8px',
216
+ minWidth: 0,
217
+ padding: expanded ? '6px 10px' : 0,
218
+ flexShrink: 0,
213
219
  }}
214
220
  >
215
- {/* Header bar */}
216
- <div
217
- style={{
218
- display: 'flex',
219
- alignItems: 'center',
220
- gap: '8px',
221
- padding: '6px 10px',
222
- borderBottom: '1px solid var(--c-line)',
223
- flexShrink: 0,
224
- }}
225
- >
221
+ <div class="file-toolbar-metadata">
226
222
  <span
227
223
  style={{
228
- fontSize: '9px',
224
+ fontSize: 'var(--fs-meta)',
229
225
  padding: '1px 5px',
230
- background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
231
- color: 'var(--c-accent)',
232
- borderRadius: '3px',
226
+ background: 'var(--c-panel-soft)',
227
+ color: 'var(--c-muted)',
228
+ borderRadius: 'var(--r-control)',
233
229
  fontWeight: 600,
234
230
  }}
235
231
  >
236
232
  {lang}
237
233
  </span>
238
- <span
239
- style={{
240
- color: 'var(--c-text-soft)',
241
- fontSize: expanded ? '12px' : '10px',
242
- overflow: 'hidden',
243
- textOverflow: 'ellipsis',
244
- whiteSpace: 'nowrap',
245
- flex: 1,
246
- }}
247
- >
248
- {filePath}
249
- </span>
234
+ {(expanded || title !== filePath.split(/[\\/]/).pop()) && (
235
+ <span
236
+ style={{
237
+ color: 'var(--c-text-soft)',
238
+ fontSize: expanded ? 'var(--fs-ui)' : 'var(--fs-meta)',
239
+ overflow: 'hidden',
240
+ textOverflow: 'ellipsis',
241
+ whiteSpace: 'nowrap',
242
+ flex: 1,
243
+ }}
244
+ >
245
+ {expanded ? filePath : filePath.split(/[\\/]/).pop()}
246
+ </span>
247
+ )}
250
248
  {lineCount !== undefined && (
251
- <span style={{ color: 'var(--c-dim)', fontSize: '10px', flexShrink: 0 }}>{lineCount} lines</span>
249
+ <span style={{ color: 'var(--c-dim)', fontSize: 'var(--fs-meta)', flexShrink: 0 }}>{lineCount} lines</span>
252
250
  )}
253
- {updatedAt && (
254
- <span style={{ color: 'var(--c-dim)', fontSize: '10px', flexShrink: 0 }}>
251
+ {expanded && updatedAt && (
252
+ <span style={{ color: 'var(--c-dim)', fontSize: 'var(--fs-meta)', flexShrink: 0 }}>
255
253
  {new Date(updatedAt).toLocaleTimeString()}
256
254
  </span>
257
255
  )}
258
- <BarHint label="Mark this file as AX evidence" align="end" fitWithin=".node-content">
259
- <button
260
- type="button"
261
- class="ax-node-action"
262
- aria-label="Mark this file as AX evidence"
263
- onClick={(e) => {
264
- e.stopPropagation();
265
- void runNodeAxInteraction(
266
- node,
267
- 'ax.evidence.add',
268
- { kind: 'file', title: filePath.split('/').pop() || filePath, ref: filePath },
269
- 'Marked as evidence',
270
- );
271
- }}
272
- style={{
273
- background: 'none',
274
- border: 'none',
275
- color: 'var(--c-muted)',
276
- cursor: 'pointer',
277
- padding: '2px 4px',
278
- fontSize: '12px',
279
- flexShrink: 0,
280
- }}
281
- >
282
- <IconPlusCircle size={14} />
283
- </button>
284
- </BarHint>
285
- <BarHint label="Reload file" align="end" fitWithin=".node-content">
286
- <button
287
- type="button"
288
- onClick={handleReload}
289
- aria-label="Reload file"
290
- style={{
291
- background: 'none',
292
- border: 'none',
293
- color: 'var(--c-muted)',
294
- cursor: 'pointer',
295
- padding: '2px 4px',
296
- fontSize: '12px',
297
- flexShrink: 0,
298
- }}
299
- >
300
- <IconRefresh size={14} />
301
- </button>
302
- </BarHint>
303
256
  </div>
257
+ <BarHint label="Mark this file as AX evidence" align="end" fitWithin=".node-content">
258
+ <button
259
+ type="button"
260
+ class="ax-node-action"
261
+ aria-label="Mark this file as AX evidence"
262
+ onClick={(e) => {
263
+ e.stopPropagation();
264
+ void runNodeAxInteraction(
265
+ node,
266
+ 'ax.evidence.add',
267
+ { kind: 'file', title: filePath.split('/').pop() || filePath, ref: filePath },
268
+ 'Marked as evidence',
269
+ );
270
+ }}
271
+ style={{
272
+ background: 'none',
273
+ border: 'none',
274
+ color: 'var(--c-muted)',
275
+ cursor: 'pointer',
276
+ padding: '2px 4px',
277
+ fontSize: '12px',
278
+ flexShrink: 0,
279
+ }}
280
+ >
281
+ <IconPlusCircle size={14} />
282
+ </button>
283
+ </BarHint>
284
+ <BarHint label="Reload file" align="end" fitWithin=".node-content">
285
+ <button
286
+ type="button"
287
+ onClick={handleReload}
288
+ aria-label="Reload file"
289
+ style={{
290
+ background: 'none',
291
+ border: 'none',
292
+ color: 'var(--c-muted)',
293
+ cursor: 'pointer',
294
+ padding: '2px 4px',
295
+ fontSize: '12px',
296
+ flexShrink: 0,
297
+ }}
298
+ >
299
+ <IconRefresh size={14} />
300
+ </button>
301
+ </BarHint>
302
+ </div>
303
+ );
304
304
 
305
+ return (
306
+ <div
307
+ style={{
308
+ display: 'flex',
309
+ flexDirection: 'column',
310
+ height: '100%',
311
+ fontFamily: 'var(--font-code)',
312
+ fontSize: expanded ? '13px' : 'var(--fs-ui)',
313
+ }}
314
+ >
315
+ {header ? createPortal(toolbar, header) : toolbar}
305
316
  {/* Content area */}
306
317
  <div
307
318
  style={{
@@ -3,10 +3,11 @@ import { HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
3
3
  import { axSurfaceState, canvasTheme } from '../state/canvas-store';
4
4
  import type { CanvasNodeState } from '../types';
5
5
  import { nodeSurfaceUrl, surfaceContentHash } from './surface-url';
6
- import { RefreshingViewerFrame } from './McpAppNode';
6
+ import { RefreshingViewerFrame, waitForViewerPaint } from './ViewerFrame';
7
7
  import { useAxSurfaceBridge } from './use-ax-surface-bridge';
8
8
  import { useIframeContentHeight } from './use-iframe-content-height';
9
9
  import { useSurfaceFrame } from './use-surface-frame';
10
+ import { usePrimitiveJson } from './primitive-copy';
10
11
 
11
12
  export function shouldShowPresentationControls(node: CanvasNodeState): boolean {
12
13
  return node.type === 'html' && node.data.presentation === true;
@@ -36,8 +37,6 @@ export function HtmlNode({
36
37
  const iframeRef = useRef<HTMLIFrameElement>(null);
37
38
  const theme = canvasTheme.value;
38
39
  const axCapabilitySignature = htmlAxCapabilitySignature(node.data.axCapabilities);
39
- // Stable per-mount nonce that authorizes parent → iframe theme-update messages.
40
- const themeToken = useMemo(() => `theme-${crypto.randomUUID()}`, []);
41
40
  // Rotate the nonce when capabilities change: both the URL and injected srcdoc
42
41
  // change, rebootstrapping the bridge and rejecting emits from the old document.
43
42
  const axToken = useMemo(() => `ax-${crypto.randomUUID()}`, [axCapabilitySignature]);
@@ -50,6 +49,9 @@ export function HtmlNode({
50
49
  ? node.data.content
51
50
  : '';
52
51
  const v = useMemo(() => surfaceContentHash(html), [html]);
52
+ // A replacement must not answer a copy request with the old document's data.
53
+ const themeToken = useMemo(() => `theme-${crypto.randomUUID()}`, [html]);
54
+ usePrimitiveJson(node, themeToken, iframeRef, expanded);
53
55
 
54
56
  // The in-canvas iframe and the "Open as site" tab load the SAME server-rendered
55
57
  // surface URL (/api/canvas/surface/:id) — one render path, no content fork.
@@ -77,7 +79,7 @@ export function HtmlNode({
77
79
 
78
80
  // Grow the node to fit the surface's reported content height (grow-only, gated).
79
81
  // Never in the expanded overlay — there the surface fills the large overlay frame.
80
- useIframeContentHeight(node, iframeRef, expanded ? '' : frameToken);
82
+ const measureContent = useIframeContentHeight(node, iframeRef, expanded ? '' : frameToken);
81
83
 
82
84
  // Phase 3 HTML bridge — the shared sandboxed-surface trust boundary (M2).
83
85
  useAxSurfaceBridge({ enabled: true, token: axToken, nodeId: node.id, sourceSurface: 'html-node', iframeRef });
@@ -121,38 +123,8 @@ export function HtmlNode({
121
123
  return () => window.clearTimeout(id);
122
124
  }, [autoFocus, surfaceSrc]);
123
125
 
124
- const handleDocumentLoad = (frame: HTMLIFrameElement, signal: AbortSignal): Promise<boolean> => {
125
- if (signal.aborted) return Promise.resolve(false);
126
- return new Promise((resolve) => {
127
- const finish = (ready: boolean) => {
128
- clearTimeout(timer);
129
- window.removeEventListener('message', receive);
130
- signal.removeEventListener('abort', cancel);
131
- resolve(ready);
132
- };
133
- const cancel = () => finish(false);
134
- const receive = (event: MessageEvent) => {
135
- if (
136
- event.source !== frame.contentWindow ||
137
- event.data?.source !== HTML_SURFACE_PUSH_SOURCE ||
138
- event.data?.token !== themeToken
139
- )
140
- return;
141
- // A healthy offscreen frame can wait for paint without a deadline.
142
- if (event.data.type === 'mounted') clearTimeout(timer);
143
- if (event.data.type === 'ready') finish(true);
144
- };
145
- const timer = setTimeout(cancel, 10_000);
146
- window.addEventListener('message', receive);
147
- signal.addEventListener('abort', cancel, { once: true });
148
- frame.contentWindow?.postMessage(
149
- { source: HTML_SURFACE_PUSH_SOURCE, type: 'check-ready', token: themeToken },
150
- '*',
151
- );
152
- });
153
- };
154
-
155
126
  const handleFrameLoad = () => {
127
+ measureContent();
156
128
  iframeRef.current?.contentWindow?.postMessage(
157
129
  {
158
130
  source: HTML_SURFACE_PUSH_SOURCE,
@@ -205,7 +177,7 @@ export function HtmlNode({
205
177
  allow=""
206
178
  tabIndex={autoFocus ? 0 : undefined}
207
179
  onLoad={handleFrameLoad}
208
- onDocumentLoad={handleDocumentLoad}
180
+ onDocumentLoad={(frame, signal) => waitForViewerPaint(frame, themeToken, signal)}
209
181
  />
210
182
  );
211
183
  }
@@ -27,22 +27,36 @@ export function LedgerNode({ node }: { node: CanvasNodeState }) {
27
27
  <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', fontSize: '12px' }}>
28
28
  {lines.length > 0 && (
29
29
  <div style={{ display: 'flex', flexDirection: 'column' }}>
30
- {lines.map((line, i) => (
31
- <div
32
- key={i}
33
- style={{
34
- padding: '3px 0',
35
- borderBottom: i < lines.length - 1 ? '1px solid rgba(45,55,90,0.3)' : 'none',
36
- color: 'var(--c-text)',
37
- fontFamily: 'var(--font-code)',
38
- fontSize: '11px',
39
- whiteSpace: 'pre-wrap',
40
- wordBreak: 'break-word',
41
- }}
42
- >
43
- {line}
44
- </div>
45
- ))}
30
+ {lines.map((line, i) => {
31
+ const timed = /^(\d{2}:\d{2}(?::\d{2})?)\s+(.+)$/.exec(line);
32
+ return (
33
+ <div
34
+ key={i}
35
+ style={{
36
+ display: 'grid',
37
+ gridTemplateColumns: timed ? '44px 1fr' : '1fr',
38
+ gap: '10px',
39
+ padding: '3px 0',
40
+ color: 'var(--c-text)',
41
+ fontFamily: 'var(--font-ui)',
42
+ fontSize: 'var(--fs-ui)',
43
+ whiteSpace: 'pre-wrap',
44
+ wordBreak: 'break-word',
45
+ }}
46
+ >
47
+ {timed ? (
48
+ <>
49
+ <span style={{ fontFamily: 'var(--font-code)', fontSize: 'var(--fs-meta)', color: 'var(--c-dim)' }}>
50
+ {timed[1]}
51
+ </span>
52
+ <span>{timed[2]}</span>
53
+ </>
54
+ ) : (
55
+ line
56
+ )}
57
+ </div>
58
+ );
59
+ })}
46
60
  </div>
47
61
  )}
48
62
  {entries.map(([key, value]) => (
@@ -53,7 +67,7 @@ export function LedgerNode({ node }: { node: CanvasNodeState }) {
53
67
  justifyContent: 'space-between',
54
68
  gap: '8px',
55
69
  padding: '3px 0',
56
- borderBottom: '1px solid rgba(45,55,90,0.3)',
70
+ borderBottom: '1px solid var(--c-line)',
57
71
  }}
58
72
  >
59
73
  <span style={{ color: 'var(--c-muted)', fontSize: '11px', flexShrink: 0 }}>
@@ -6,7 +6,7 @@ import { IconArrowLeft } from '../icons';
6
6
  import { expandNode, nodes, updateNodeData } from '../state/canvas-store';
7
7
  import { fetchFile, renderMarkdown, updateNodeFromClient } from '../state/intent-bridge';
8
8
  import type { CanvasNodeState } from '../types';
9
- import { activeBoard, setBoardReadme, openMemoryTarget, boardList } from '../state/boards-store';
9
+ import { openMemoryTarget, boardList } from '../state/boards-store';
10
10
  import { showToast } from '../state/attention-bridge';
11
11
  import { MdFormatBar } from './MdFormatBar';
12
12
  import { handleFormatShortcut, handleTab } from './md-format';
@@ -189,7 +189,6 @@ export async function waitForMarkdownSave(boardId: string | null, nodeId: string
189
189
  }
190
190
 
191
191
  export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
192
- const board = activeBoard();
193
192
  const boardId = activeBoardId.value;
194
193
  const draftKey = JSON.stringify([boardId, node.id]);
195
194
  const draft = useMemo(() => {
@@ -523,7 +522,7 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
523
522
  <div
524
523
  style={{
525
524
  padding: '4px 8px',
526
- fontSize: '10px',
525
+ fontSize: 'var(--fs-meta)',
527
526
  background: 'color-mix(in srgb, var(--c-ok) 10%, transparent)',
528
527
  color: 'var(--c-ok)',
529
528
  borderBottom: '1px solid color-mix(in srgb, var(--c-ok) 20%, transparent)',
@@ -699,21 +698,13 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
699
698
  {String((node.data.source as Record<string, unknown>).reference ?? 'Location not supplied')}
700
699
  </div>
701
700
  )}
702
- <div class="md-card-actions">
703
- {board && (
704
- <button
705
- type="button"
706
- class="md-readme-btn"
707
- aria-pressed={board.readmeNodeId === node.id}
708
- onClick={() => void setBoardReadme(board.id, board.readmeNodeId === node.id ? null : node.id)}
709
- >
710
- {board.readmeNodeId === node.id ? 'README' : 'Set as README'}
701
+ {node.data.display !== 'heading' && (
702
+ <div class="md-card-actions">
703
+ <button type="button" class="md-card-edit" onClick={() => expandNode(node.id)}>
704
+ Edit
711
705
  </button>
712
- )}
713
- <button type="button" class="md-card-edit" onClick={() => expandNode(node.id)}>
714
- Edit
715
- </button>
716
- </div>
706
+ </div>
707
+ )}
717
708
  </div>
718
709
  );
719
710
  }