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,11 +1,9 @@
1
- import type { RefObject } from 'preact';
2
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
1
+ import { useEffect, useMemo, useRef } from 'preact/hooks';
3
2
  import { HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
4
3
  import { canvasThemeScheme } from '../../shared/themes.js';
5
4
  import type { CanvasNodeState } from '../types';
6
5
  import { axSurfaceState, canvasTheme } from '../state/canvas-store';
7
- import { showToast } from '../state/attention-bridge';
8
- import { iframeMode } from '../state/iframe-mode';
6
+ import { RefreshingViewerFrame } from './ViewerFrame';
9
7
  import { shouldContentFitIframeNode } from '../canvas/auto-fit';
10
8
  import { ExtAppFrame } from './ExtAppFrame';
11
9
  import { useAxSurfaceBridge } from './use-ax-surface-bridge';
@@ -16,6 +14,7 @@ import { isHostedWorkbench } from '../state/workbench-transport';
16
14
 
17
15
  function withViewerParams(
18
16
  url: string,
17
+ theme: string,
19
18
  expanded: boolean,
20
19
  specVersion?: number,
21
20
  axToken?: string,
@@ -28,9 +27,7 @@ function withViewerParams(
28
27
  try {
29
28
  const hosted = isHostedWorkbench() && url.startsWith('/');
30
29
  const resolved = new URL(url, hosted ? 'https://pmx.invalid' : window.location.origin);
31
- // The json-render viewer + artifacts understand dark/light — collapse named
32
- // themes (sepia → light, midnight/arctic/ember/forest → dark) to a scheme.
33
- resolved.searchParams.set('theme', canvasThemeScheme(canvasTheme.value));
30
+ resolved.searchParams.set('theme', theme);
34
31
  if (expanded) resolved.searchParams.set('display', 'expanded');
35
32
  // Streaming json-render nodes bump specVersion as patches accumulate; including
36
33
  // it in the src reloads the iframe so it re-reads the latest accumulated spec.
@@ -61,139 +58,6 @@ export function isSameOriginFrameDocumentUrl(url: string, origin = window.locati
61
58
  }
62
59
  }
63
60
 
64
- type ViewerFrameSource = { src?: string; srcdoc?: string };
65
-
66
- /**
67
- * Keep the last painted viewer in front while its replacement navigates. Viewer
68
- * specs are immutable documents, so updates normally change `?v=` and reload the
69
- * iframe; putting the new document behind the old one conceals Chromium's white
70
- * navigation paint without suppressing rendering. Wait for restore and render commit,
71
- * then two animation frames before the old viewer is removed.
72
- */
73
- export function RefreshingViewerFrame({
74
- source,
75
- iframeRef,
76
- onLoad,
77
- onDocumentLoad,
78
- title,
79
- className = 'mcp-app-frame',
80
- sandbox = 'allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox',
81
- allow = 'clipboard-read; clipboard-write',
82
- tabIndex,
83
- }: {
84
- source: ViewerFrameSource;
85
- iframeRef: RefObject<HTMLIFrameElement>;
86
- onLoad: () => void;
87
- onDocumentLoad?: (frame: HTMLIFrameElement, signal: AbortSignal) => void | Promise<boolean | 'cancelled' | void>;
88
- title: string;
89
- className?: string;
90
- sandbox?: string;
91
- allow?: string;
92
- tabIndex?: number;
93
- }) {
94
- const sourceKey = source.src ?? source.srcdoc ?? '';
95
- const [painted, setPainted] = useState({ key: sourceKey, source, attempt: 0 });
96
- const [pending, setPending] = useState<{ key: string; source: ViewerFrameSource; attempt: number } | null>(null);
97
- const promotionRef = useRef(0);
98
- const readiness = useRef(new Set<AbortController>());
99
-
100
- useLayoutEffect(() => {
101
- promotionRef.current += 1;
102
- for (const controller of readiness.current) controller.abort();
103
- readiness.current.clear();
104
- if (!sourceKey || sourceKey === painted.key) {
105
- setPending(null);
106
- return;
107
- }
108
- if (!painted.key) {
109
- setPainted({ key: sourceKey, source, attempt: 0 });
110
- return;
111
- }
112
- setPending({ key: sourceKey, source, attempt: 0 });
113
- }, [sourceKey, painted.key]);
114
-
115
- useEffect(
116
- () => () => {
117
- promotionRef.current += 1;
118
- for (const controller of readiness.current) controller.abort();
119
- readiness.current.clear();
120
- },
121
- [],
122
- );
123
-
124
- const frame = (entry: { key: string; source: ViewerFrameSource; attempt: number }, isPending: boolean) => (
125
- <iframe
126
- key={`${entry.key}:${entry.attempt}`}
127
- ref={isPending ? undefined : iframeRef}
128
- {...entry.source}
129
- class={className}
130
- sandbox={sandbox}
131
- allow={allow}
132
- tabIndex={tabIndex}
133
- inert={isPending}
134
- aria-hidden={isPending ? true : undefined}
135
- loading={iframeMode.value === 'srcdoc' ? undefined : 'lazy'}
136
- onLoad={async (event) => {
137
- // Seed runtime state while a replacement is still behind the painted
138
- // frame, and wait until that document has committed its rendered UI.
139
- const token = isPending ? ++promotionRef.current : promotionRef.current;
140
- const loadedFrame = event.currentTarget;
141
- const controller = new AbortController();
142
- readiness.current.add(controller);
143
- const ready = onDocumentLoad?.(loadedFrame, controller.signal);
144
- const loaded = ready ? await ready : undefined;
145
- readiness.current.delete(controller);
146
- if (promotionRef.current !== token || loaded === 'cancelled') return;
147
- if (loaded === false) {
148
- if (entry.attempt === 0) {
149
- const retry = { ...entry, attempt: 1 };
150
- if (isPending) setPending(retry);
151
- else setPainted(retry);
152
- } else {
153
- if (isPending) setPending(null);
154
- showToast(
155
- 'remove',
156
- 'Viewer could not load',
157
- `${isPending ? 'Keeping the last view. ' : ''}Reload to retry.`,
158
- );
159
- }
160
- return;
161
- }
162
- if (!isPending) {
163
- onLoad();
164
- return;
165
- }
166
- requestAnimationFrame(() =>
167
- requestAnimationFrame(() => {
168
- if (promotionRef.current !== token) return;
169
- iframeRef.current = loadedFrame;
170
- setPainted(entry);
171
- setPending(null);
172
- onLoad();
173
- }),
174
- );
175
- }}
176
- style={{
177
- position: 'absolute',
178
- inset: 0,
179
- width: '100%',
180
- height: '100%',
181
- visibility: 'visible',
182
- zIndex: isPending ? 0 : 1,
183
- background: 'var(--c-panel-soft)',
184
- }}
185
- title={title}
186
- />
187
- );
188
-
189
- return (
190
- <div class="mcp-app-frame-stack">
191
- {frame(painted, false)}
192
- {pending && frame(pending, true)}
193
- </div>
194
- );
195
- }
196
-
197
61
  export function McpAppNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
198
62
  if (node.data.mode === 'ext-app') {
199
63
  return <ExtAppFrame node={node} expanded={expanded} />;
@@ -223,7 +87,7 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
223
87
  // overlay frame (fill-down), not sit at its intrinsic in-canvas height.
224
88
  const contentFit = shouldContentFitIframeNode(node) && !expanded;
225
89
  const frameToken = useMemo(() => (contentFit ? `frame-${crypto.randomUUID()}` : ''), [contentFit]);
226
- useIframeContentHeight(node, iframeRef, frameToken);
90
+ const measureContent = useIframeContentHeight(node, iframeRef, frameToken);
227
91
 
228
92
  // AX emits forwarded by the json-render viewer — the shared sandboxed-surface
229
93
  // trust boundary (M2).
@@ -257,15 +121,38 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
257
121
  () => (isJsonViewer ? `ui-${crypto.randomUUID()}` : ''),
258
122
  [isJsonViewer, specVersion, node.data.url],
259
123
  );
260
- const url = withViewerParams(
261
- (node.data.url as string) || '',
262
- expanded,
263
- specVersion,
264
- axToken || undefined,
265
- isAxViewer ? node.id : undefined,
266
- frameToken || undefined,
267
- contentFit,
268
- stateToken || undefined,
124
+ const theme = canvasTheme.value;
125
+ const urlTheme = isJsonViewer ? null : canvasThemeScheme(theme);
126
+ const pushTheme = () => {
127
+ if (!isJsonViewer) return;
128
+ iframeRef.current?.contentWindow?.postMessage(
129
+ {
130
+ source: HTML_SURFACE_PUSH_SOURCE,
131
+ type: 'theme-update',
132
+ token: stateToken,
133
+ theme: node.data.theme ?? theme,
134
+ title: node.data.title,
135
+ },
136
+ '*',
137
+ );
138
+ };
139
+ useEffect(pushTheme, [theme, stateToken, node.data.theme, node.data.title]);
140
+ // Only JSON/graph understand theme messages and retain drafts across themes.
141
+ // Generic viewers consume a dark/light query and navigate when it changes.
142
+ const url = useMemo(
143
+ () =>
144
+ withViewerParams(
145
+ (node.data.url as string) || '',
146
+ urlTheme ?? theme,
147
+ expanded,
148
+ specVersion,
149
+ axToken || undefined,
150
+ isAxViewer ? node.id : undefined,
151
+ frameToken || undefined,
152
+ contentFit,
153
+ stateToken || undefined,
154
+ ),
155
+ [node.data.url, urlTheme, expanded, specVersion, axToken, node.id, frameToken, contentFit, stateToken],
269
156
  );
270
157
  // src vs fetch()+srcdoc, decided by the boot-wide embed probe (Amp portals).
271
158
  // External viewer URLs always stay src — the hook passes them through.
@@ -295,7 +182,9 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
295
182
  >
296
183
  {url}
297
184
  </a>
298
- {sourceServer && <div style={{ color: 'var(--c-dim)', fontSize: '10px' }}>Source: {sourceServer}</div>}
185
+ {sourceServer && (
186
+ <div style={{ color: 'var(--c-dim)', fontSize: 'var(--fs-meta)' }}>Source: {sourceServer}</div>
187
+ )}
299
188
  </div>
300
189
  );
301
190
  }
@@ -313,7 +202,7 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
313
202
  <div
314
203
  style={{
315
204
  padding: '4px 8px',
316
- fontSize: '10px',
205
+ fontSize: 'var(--fs-meta)',
317
206
  background: 'color-mix(in srgb, var(--c-warn) 10%, transparent)',
318
207
  color: 'var(--c-warn)',
319
208
  borderBottom: '1px solid color-mix(in srgb, var(--c-warn) 15%, transparent)',
@@ -328,11 +217,16 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
328
217
  <RefreshingViewerFrame
329
218
  source={surfaceFrame}
330
219
  iframeRef={iframeRef}
220
+ allow="clipboard-read; clipboard-write"
331
221
  onDocumentLoad={(frame, signal) => {
332
222
  if (stateToken && frame.contentWindow)
333
- return restoreViewerRuntimeState(node.id, stateToken, frame.contentWindow, signal);
223
+ return restoreViewerRuntimeState(node, stateToken, frame.contentWindow, signal);
224
+ }}
225
+ onLoad={() => {
226
+ measureContent();
227
+ pushAxState();
228
+ pushTheme();
334
229
  }}
335
- onLoad={pushAxState}
336
230
  title={`MCP App: ${sourceServer}`}
337
231
  />
338
232
  </div>
@@ -1,82 +1,93 @@
1
- import { useEffect, useMemo, useRef } from 'preact/hooks';
2
- import { HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
3
- import { canvasTheme } from '../state/canvas-store';
1
+ import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
2
+ import { renderMermaid } from '../mermaid-render';
3
+ import { canvasTheme, nodes, updateNode } from '../state/canvas-store';
4
+ import { pushCanvasUpdate } from '../state/intent-bridge';
5
+ import { computeContentGrowHeight, contentFitPosition } from '../canvas/auto-fit';
4
6
  import type { CanvasNodeState } from '../types';
5
- import { nodeSurfaceUrl, surfaceContentHash } from './surface-url';
6
- import { useIframeContentHeight } from './use-iframe-content-height';
7
- import { useSurfaceFrame } from './use-surface-frame';
8
7
 
9
- /**
10
- * Mermaid diagram node — a display-only iframe surface. The server renders the
11
- * surface document (escaped diagram source + /canvas/mermaid-entry.js) at
12
- * /api/canvas/surface/:id; the iframe and "Open as site" share that one render
13
- * path, exactly like html nodes. Minimal subset of HtmlNode: theme push and
14
- * content-height growth, no AX bridge, no presentation mode.
15
- */
16
8
  export function MermaidNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
17
- const iframeRef = useRef<HTMLIFrameElement>(null);
18
- const theme = canvasTheme.value;
19
- // Stable per-mount nonce that authorizes parent → iframe theme-update messages.
20
- const themeToken = useMemo(() => `theme-${crypto.randomUUID()}`, []);
21
- // Per-mount nonce for the content-height reporter (node grows to fit content).
22
- const frameToken = useMemo(() => `frame-${crypto.randomUUID()}`, []);
9
+ const container = useRef<HTMLDivElement>(null);
10
+ const [error, setError] = useState('');
11
+ const [attempt, setAttempt] = useState(0);
23
12
  const source = typeof node.data.content === 'string' ? node.data.content : '';
24
- const fit = node.data.fit === 'none' ? 'none' : 'contain';
25
- const v = useMemo(() => surfaceContentHash(`${fit}:${source}`), [source, fit]);
26
-
27
- // `theme` is intentionally excluded from the deps: live theme changes are
28
- // pushed via postMessage below (no reload), while `v` reloads the frame when
29
- // the diagram source changes.
30
- const surfaceSrc = useMemo(
31
- () => (source ? nodeSurfaceUrl(node.id, { theme, themeToken, v, frameToken }) : ''),
32
- [source, themeToken, v, node.id, frameToken],
33
- );
34
-
35
- // src vs fetch()+srcdoc, decided by the boot-wide embed probe (Amp portals).
36
- const surfaceFrame = useSurfaceFrame(surfaceSrc);
13
+ const theme = canvasTheme.value;
14
+ const fit = node.data.fit !== 'none';
15
+ // Fit is layout, not content: it resizes the painted SVG in place. Re-rendering
16
+ // on a fit change replaced the element and left a detached SVG mid-update.
17
+ const fitRef = useRef(fit);
18
+ fitRef.current = fit;
37
19
 
38
- // Grow the node to fit the surface's reported content height (grow-only, gated).
39
- // Never in the expanded overlay — there the surface fills the large overlay frame.
40
- useIframeContentHeight(node, iframeRef, expanded ? '' : frameToken);
20
+ const fitDiagram = () => {
21
+ const element = container.current;
22
+ const svg = element?.querySelector('svg');
23
+ const box = svg?.viewBox.baseVal;
24
+ if (!element || !svg || !box?.width || !box.height) return;
25
+ const scale = fitRef.current ? Math.min(1, element.clientWidth / box.width, element.clientHeight / box.height) : 1;
26
+ svg.style.maxWidth = 'none';
27
+ svg.style.width = `${box.width * scale}px`;
28
+ svg.style.height = `${box.height * scale}px`;
29
+ svg.style.margin = fitRef.current ? '0 auto' : '0';
30
+ };
41
31
 
42
- const pushTheme = () => {
43
- iframeRef.current?.contentWindow?.postMessage(
44
- {
45
- source: HTML_SURFACE_PUSH_SOURCE,
46
- type: 'theme-update',
47
- token: themeToken,
48
- theme,
49
- },
50
- '*',
51
- );
32
+ const growToDiagram = () => {
33
+ const element = container.current;
34
+ const box = element?.querySelector('svg')?.viewBox.baseVal;
35
+ const current = nodes.value.get(node.id);
36
+ if (expanded || !element || !box?.width || !current) return;
37
+ const naturalHeight = box.height * (fitRef.current ? Math.min(1, element.clientWidth / box.width) : 1);
38
+ const card = element.closest<HTMLElement>('.canvas-node')!;
39
+ const height = computeContentGrowHeight(current, naturalHeight, card.offsetHeight - element.clientHeight);
40
+ if (height === null) return;
41
+ const position = contentFitPosition(current, height, Array.from(nodes.value.values()));
42
+ const patch = { size: { width: current.size.width, height }, position };
43
+ updateNode(node.id, patch);
44
+ void pushCanvasUpdate([{ id: node.id, ...patch }], { recordHistory: false });
52
45
  };
53
46
 
54
- useEffect(pushTheme, [theme, themeToken]);
47
+ useEffect(() => {
48
+ let cancelled = false;
49
+ const element = container.current;
50
+ if (!element || !source) return;
51
+ const observer = new ResizeObserver(fitDiagram);
52
+ observer.observe(element);
53
+ const render = async () => {
54
+ try {
55
+ const svg = await renderMermaid(source);
56
+ if (cancelled) return;
57
+ element.innerHTML = svg;
58
+ setError('');
59
+ element.querySelector('svg')?.setAttribute('preserveAspectRatio', 'xMidYMid meet');
60
+ fitDiagram();
61
+ growToDiagram();
62
+ } catch (failure) {
63
+ if (!cancelled) setError(failure instanceof Error ? failure.message : String(failure));
64
+ }
65
+ };
66
+ void render();
67
+ return () => {
68
+ cancelled = true;
69
+ observer.disconnect();
70
+ };
71
+ }, [source, theme, expanded, attempt]);
55
72
 
56
- if (!source) {
57
- return <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>No diagram source set</div>;
58
- }
73
+ // Same commit as data-fit, so the attribute and the painted size never disagree.
74
+ useLayoutEffect(() => {
75
+ fitDiagram();
76
+ growToDiagram();
77
+ }, [fit]);
59
78
 
60
- // SECURITY: sandbox stays `allow-scripts` ONLY — same tier as html surfaces
61
- // (see HtmlNode for the full rationale); the surface route sends a matching
62
- // CSP sandbox header.
79
+ if (!source) return <div class="viewer-placeholder">No diagram source set</div>;
63
80
  return (
64
- <iframe
65
- ref={iframeRef}
66
- class="html-node-frame"
67
- title={typeof node.data.title === 'string' ? node.data.title : 'Mermaid diagram'}
68
- sandbox="allow-scripts"
69
- {...surfaceFrame}
70
- onLoad={pushTheme}
71
- style={{
72
- width: '100%',
73
- height: '100%',
74
- minHeight: expanded ? '70vh' : '0',
75
- border: 'none',
76
- background: 'var(--c-bg)',
77
- borderRadius: '6px',
78
- display: 'block',
79
- }}
80
- />
81
+ <div class="mermaid-viewer" data-fit={fit ? 'contain' : 'none'}>
82
+ <div ref={container} class="mermaid-diagram" />
83
+ {error && (
84
+ <div class="viewer-fallback" role="status">
85
+ <span>{error}</span>
86
+ <button type="button" onClick={() => setAttempt(attempt + 1)}>
87
+ Retry
88
+ </button>
89
+ </div>
90
+ )}
91
+ </div>
81
92
  );
82
93
  }
@@ -57,7 +57,7 @@ function ContextBadge({ count }: { count: number }) {
57
57
  padding: '4px 8px',
58
58
  fontSize: '11px',
59
59
  color: 'var(--c-accent)',
60
- background: 'rgba(70,182,255,0.08)',
60
+ background: 'color-mix(in srgb, var(--c-pin) 8%, transparent)',
61
61
  borderRadius: 'var(--radius-sm)',
62
62
  flexShrink: 0,
63
63
  }}
@@ -75,7 +75,7 @@ function ErrorBanner({ message, onDismiss }: { message: string | null; onDismiss
75
75
  padding: '4px 8px',
76
76
  fontSize: '11px',
77
77
  color: 'var(--c-danger)',
78
- background: 'rgba(255,80,80,0.08)',
78
+ background: 'color-mix(in srgb, var(--c-danger) 8%, transparent)',
79
79
  borderRadius: 'var(--radius-sm)',
80
80
  display: 'flex',
81
81
  alignItems: 'center',
@@ -312,7 +312,7 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
312
312
  style={{
313
313
  flex: 1,
314
314
  resize: 'none',
315
- background: 'rgba(10,14,30,0.4)',
315
+ background: 'var(--c-input-bg)',
316
316
  border: '1px solid var(--c-line)',
317
317
  borderRadius: 'var(--radius-sm)',
318
318
  color: 'var(--c-text)',
@@ -324,7 +324,7 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
324
324
  }}
325
325
  />
326
326
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
327
- <span style={{ fontSize: '10px', color: 'var(--c-muted)' }}>{'\u2318'}+Enter to send</span>
327
+ <kbd class="prompt-key-hint">{'\u2318'}+Enter to send</kbd>
328
328
  <button
329
329
  type="button"
330
330
  onClick={handleSubmit}
@@ -377,10 +377,9 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
377
377
  >
378
378
  <span
379
379
  style={{
380
- fontSize: '10px',
381
- textTransform: 'uppercase',
380
+ fontSize: 'var(--fs-meta)',
382
381
  fontWeight: 600,
383
- color: isPending ? 'var(--c-warn)' : isAnswered ? 'var(--c-ok)' : 'var(--c-muted)',
382
+ color: isPending ? 'var(--c-agent)' : isAnswered ? 'var(--c-ok)' : 'var(--c-muted)',
384
383
  }}
385
384
  >
386
385
  {isPending ? 'Sending…' : isAnswered ? 'Answered' : legacyStatus}
@@ -481,13 +480,12 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
481
480
  >
482
481
  <span
483
482
  style={{
484
- fontSize: '10px',
485
- textTransform: 'uppercase',
483
+ fontSize: 'var(--fs-meta)',
486
484
  fontWeight: 600,
487
485
  color: isStreaming
488
- ? 'var(--c-accent)'
486
+ ? 'var(--c-agent)'
489
487
  : isPending
490
- ? 'var(--c-warn)'
488
+ ? 'var(--c-agent)'
491
489
  : isAnswered
492
490
  ? 'var(--c-ok)'
493
491
  : 'var(--c-muted)',
@@ -495,7 +493,7 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
495
493
  >
496
494
  {isStreaming ? 'Streaming…' : isPending ? 'Sending…' : isAnswered ? 'Answered' : threadStatus}
497
495
  </span>
498
- <span style={{ fontSize: '10px', color: 'var(--c-muted)' }}>
496
+ <span style={{ fontSize: 'var(--fs-meta)', color: 'var(--c-muted)' }}>
499
497
  {turns.length} turn{turns.length !== 1 ? 's' : ''}
500
498
  </span>
501
499
  </div>
@@ -513,7 +511,7 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
513
511
  style={{
514
512
  width: '100%',
515
513
  resize: 'none',
516
- background: 'rgba(10,14,30,0.4)',
514
+ background: 'var(--c-input-bg)',
517
515
  border: '1px solid var(--c-line)',
518
516
  borderRadius: 'var(--radius-sm)',
519
517
  color: 'var(--c-text)',
@@ -532,7 +530,7 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
532
530
  marginTop: '4px',
533
531
  }}
534
532
  >
535
- <span style={{ fontSize: '10px', color: 'var(--c-muted)' }}>{'\u2318'}+Enter</span>
533
+ <kbd class="prompt-key-hint">{'\u2318'}+Enter</kbd>
536
534
  <button
537
535
  type="button"
538
536
  onClick={handleReplySubmit}
@@ -49,13 +49,16 @@ export function ResponseNode({ node, expanded = false }: { node: CanvasNodeState
49
49
 
50
50
  return (
51
51
  <div class="response-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
52
+ <div class="thread-turn-role" style={{ color: 'var(--c-agent)' }}>
53
+ ✦ Agent
54
+ </div>
52
55
  {/* Streaming indicator */}
53
56
  {isStreaming && (
54
57
  <div
55
58
  class="response-streaming-bar"
56
59
  style={{
57
60
  height: '2px',
58
- background: 'linear-gradient(90deg, transparent, var(--c-accent), transparent)',
61
+ background: 'linear-gradient(90deg, transparent, var(--c-agent), transparent)',
59
62
  animation: 'response-stream-pulse 1.5s ease-in-out infinite',
60
63
  borderRadius: '1px',
61
64
  marginBottom: '4px',
@@ -100,10 +103,9 @@ export function ResponseNode({ node, expanded = false }: { node: CanvasNodeState
100
103
  >
101
104
  <span
102
105
  style={{
103
- fontSize: '10px',
104
- textTransform: 'uppercase',
106
+ fontSize: 'var(--fs-meta)',
105
107
  fontWeight: 600,
106
- color: isStreaming ? 'var(--c-accent)' : isComplete ? 'var(--c-ok)' : 'var(--c-muted)',
108
+ color: isStreaming ? 'var(--c-agent)' : isComplete ? 'var(--c-ok)' : 'var(--c-muted)',
107
109
  }}
108
110
  >
109
111
  {isStreaming ? 'Streaming…' : isComplete ? 'Complete' : status}
@@ -115,8 +117,8 @@ export function ResponseNode({ node, expanded = false }: { node: CanvasNodeState
115
117
  style={{
116
118
  padding: '3px 10px',
117
119
  fontSize: '11px',
118
- background: 'rgba(70,182,255,0.1)',
119
- border: '1px solid rgba(70,182,255,0.3)',
120
+ background: 'var(--c-panel-soft)',
121
+ border: '1px solid var(--c-line)',
120
122
  borderRadius: 'var(--radius-sm)',
121
123
  color: 'var(--c-accent)',
122
124
  cursor: 'pointer',
@@ -19,6 +19,7 @@ export function StatusNode({ node }: { node: CanvasNodeState }) {
19
19
  const level = (node.data.level as string) || 'ok';
20
20
  const activeTool = node.data.activeTool as string | null;
21
21
  const subagent = node.data.subagent as { state: string; name: string } | undefined;
22
+ const progress = typeof node.data.progress === 'number' ? Math.max(0, Math.min(100, node.data.progress)) : null;
22
23
 
23
24
  const phaseColor = PHASE_COLORS[phase] ?? 'var(--c-muted)';
24
25
  const isActive = phase !== 'idle';
@@ -33,7 +34,6 @@ export function StatusNode({ node }: { node: CanvasNodeState }) {
33
34
  height: '8px',
34
35
  borderRadius: '50%',
35
36
  background: phaseColor,
36
- boxShadow: isActive ? `0 0 8px ${phaseColor}` : 'none',
37
37
  animation: isActive ? 'pulse 1.5s infinite' : 'none',
38
38
  flexShrink: 0,
39
39
  }}
@@ -42,11 +42,13 @@ export function StatusNode({ node }: { node: CanvasNodeState }) {
42
42
  style={{
43
43
  fontWeight: 600,
44
44
  color: phaseColor,
45
- textTransform: 'uppercase',
46
- letterSpacing: '0.05em',
45
+ fontSize: 'var(--fs-meta)',
46
+ padding: '2px 8px',
47
+ borderRadius: 'var(--r-pill)',
48
+ background: `color-mix(in srgb, ${phaseColor} 12%, transparent)`,
47
49
  }}
48
50
  >
49
- {phase}
51
+ {phase.charAt(0).toUpperCase() + phase.slice(1)}
50
52
  </span>
51
53
  {detail && (
52
54
  <span
@@ -62,10 +64,23 @@ export function StatusNode({ node }: { node: CanvasNodeState }) {
62
64
  )}
63
65
  </div>
64
66
 
67
+ {progress !== null && (
68
+ <div
69
+ class="status-progress"
70
+ role="progressbar"
71
+ aria-label="Progress"
72
+ aria-valuemin={0}
73
+ aria-valuemax={100}
74
+ aria-valuenow={progress}
75
+ >
76
+ <div style={{ width: `${progress}%`, background: phaseColor }} />
77
+ </div>
78
+ )}
79
+
65
80
  {/* Active tool */}
66
81
  {activeTool && (
67
82
  <div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--c-agent)' }}>
68
- <span style={{ fontSize: '10px' }}>⚙</span>
83
+ <span style={{ fontSize: 'var(--fs-meta)' }}>⚙</span>
69
84
  <span style={{ fontFamily: 'var(--font-code)', fontSize: '11px' }}>{activeTool}</span>
70
85
  </div>
71
86
  )}
@@ -73,9 +88,9 @@ export function StatusNode({ node }: { node: CanvasNodeState }) {
73
88
  {/* Sub-agent */}
74
89
  {subagent && subagent.state !== 'completed' && (
75
90
  <div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--c-subagent)' }}>
76
- <span style={{ fontSize: '10px' }}>⠉</span>
91
+ <span style={{ fontSize: 'var(--fs-meta)' }}>⠉</span>
77
92
  <span>{subagent.name}</span>
78
- <span style={{ color: 'var(--c-muted)', fontSize: '10px' }}>({subagent.state})</span>
93
+ <span style={{ color: 'var(--c-muted)', fontSize: 'var(--fs-meta)' }}>({subagent.state})</span>
79
94
  </div>
80
95
  )}
81
96
 
@@ -21,14 +21,16 @@ export function StatusSummary({ node }: { node: CanvasNodeState }) {
21
21
  flexShrink: 0,
22
22
  }}
23
23
  />
24
- <span style={{ color: phaseColor, fontSize: '10px', textTransform: 'uppercase', fontWeight: 600 }}>{phase}</span>
24
+ <span style={{ color: phaseColor, fontSize: 'var(--fs-meta)', fontWeight: 600 }}>
25
+ {phase.charAt(0).toUpperCase() + phase.slice(1)}
26
+ </span>
25
27
  {activeTool && (
26
- <span style={{ color: 'var(--c-agent)', fontSize: '10px', fontFamily: 'var(--font-code)' }}>
28
+ <span style={{ color: 'var(--c-agent)', fontSize: 'var(--fs-meta)', fontFamily: 'var(--font-code)' }}>
27
29
  ⚙ {activeTool}
28
30
  </span>
29
31
  )}
30
32
  {subagent && subagent.state !== 'completed' && (
31
- <span style={{ color: 'var(--c-subagent)', fontSize: '10px' }}>⠉ {subagent.name}</span>
33
+ <span style={{ color: 'var(--c-subagent)', fontSize: 'var(--fs-meta)' }}>⠉ {subagent.name}</span>
32
34
  )}
33
35
  </span>
34
36
  );