pmx-canvas 0.2.7 → 0.3.1

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 (189) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/Readme.md +13 -11
  3. package/dist/canvas/index.js +2 -2
  4. package/dist/json-render/index.css +1 -1
  5. package/dist/types/cli/daemon.d.ts +74 -0
  6. package/dist/types/cli/watch.d.ts +2 -2
  7. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  8. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  9. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/mcp/canvas-access.d.ts +0 -7
  17. package/dist/types/server/ax-state-manager.d.ts +3 -2
  18. package/dist/types/server/bundled-skills.d.ts +3 -3
  19. package/dist/types/server/canvas-schema.d.ts +1 -1
  20. package/dist/types/server/canvas-state.d.ts +2 -0
  21. package/dist/types/server/ext-app-lookup.d.ts +1 -3
  22. package/dist/types/server/html-primitives.d.ts +1 -1
  23. package/dist/types/server/index.d.ts +4 -4
  24. package/dist/types/server/operations/composites.d.ts +29 -23
  25. package/dist/types/server/operations/http.d.ts +2 -1
  26. package/dist/types/server/operations/index.d.ts +1 -1
  27. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  28. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  29. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  30. package/dist/types/shared/ax-intent.d.ts +1 -1
  31. package/docs/RELEASE.md +23 -8
  32. package/docs/ax-host-adapter-contract.md +7 -7
  33. package/docs/ax-state-contract.md +7 -6
  34. package/docs/bun-webview-integration.md +5 -5
  35. package/docs/cli.md +1 -1
  36. package/docs/http-api.md +35 -3
  37. package/docs/mcp.md +121 -100
  38. package/docs/node-types.md +45 -35
  39. package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
  40. package/docs/plans/plan-008-registry-finish.md +2 -0
  41. package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
  42. package/docs/screenshot.png +0 -0
  43. package/docs/tech-debt-assessment-2026-06.md +1 -1
  44. package/docs/tech-debt-assessment-2026-07.md +135 -0
  45. package/package.json +5 -2
  46. package/skills/data-analysis/SKILL.md +3 -3
  47. package/skills/frontend-design/SKILL.md +3 -2
  48. package/skills/json-render-mcp/SKILL.md +4 -3
  49. package/skills/playwright-cli/SKILL.md +1 -1
  50. package/skills/pmx-canvas/SKILL.md +12 -7
  51. package/skills/pmx-canvas/evals/evals.json +2 -2
  52. package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
  53. package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
  54. package/skills/pmx-canvas/references/full-reference.md +58 -56
  55. package/skills/pmx-canvas/references/html-primitives.md +3 -3
  56. package/skills/tufte-viz/SKILL.md +5 -4
  57. package/skills/web-artifacts-builder/SKILL.md +4 -4
  58. package/src/cli/agent.ts +1861 -1548
  59. package/src/cli/daemon.ts +460 -0
  60. package/src/cli/index.ts +63 -326
  61. package/src/cli/watch.ts +2 -10
  62. package/src/client/App.tsx +48 -46
  63. package/src/client/canvas/AttentionHistory.tsx +11 -1
  64. package/src/client/canvas/CanvasNode.tsx +41 -29
  65. package/src/client/canvas/CanvasViewport.tsx +101 -66
  66. package/src/client/canvas/CommandPalette.tsx +61 -27
  67. package/src/client/canvas/ContextMenu.tsx +13 -20
  68. package/src/client/canvas/ContextPinBar.tsx +1 -5
  69. package/src/client/canvas/ContextPinHud.tsx +1 -6
  70. package/src/client/canvas/DockedNode.tsx +4 -4
  71. package/src/client/canvas/EdgeLayer.tsx +37 -36
  72. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  73. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  74. package/src/client/canvas/IntentLayer.tsx +31 -14
  75. package/src/client/canvas/Minimap.tsx +11 -16
  76. package/src/client/canvas/SelectionBar.tsx +4 -11
  77. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  78. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  79. package/src/client/canvas/auto-fit.ts +15 -14
  80. package/src/client/canvas/snap-guides.ts +12 -12
  81. package/src/client/canvas/use-node-resize.ts +1 -5
  82. package/src/client/canvas/use-pan-zoom.ts +25 -26
  83. package/src/client/ext-app/bridge.ts +3 -12
  84. package/src/client/icons.tsx +63 -20
  85. package/src/client/nodes/ContextNode.tsx +14 -25
  86. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  87. package/src/client/nodes/FileNode.tsx +74 -62
  88. package/src/client/nodes/GroupNode.tsx +4 -6
  89. package/src/client/nodes/HtmlNode.tsx +76 -46
  90. package/src/client/nodes/ImageNode.tsx +18 -27
  91. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  92. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  93. package/src/client/nodes/LedgerNode.tsx +10 -4
  94. package/src/client/nodes/MarkdownNode.tsx +3 -10
  95. package/src/client/nodes/McpAppNode.tsx +26 -22
  96. package/src/client/nodes/MdFormatBar.tsx +10 -7
  97. package/src/client/nodes/PromptNode.tsx +23 -51
  98. package/src/client/nodes/ResponseNode.tsx +3 -13
  99. package/src/client/nodes/StatusNode.tsx +5 -9
  100. package/src/client/nodes/StatusSummary.tsx +2 -8
  101. package/src/client/nodes/WebpageNode.tsx +20 -14
  102. package/src/client/nodes/iframe-document-url.ts +25 -16
  103. package/src/client/nodes/image-warnings.ts +1 -7
  104. package/src/client/nodes/md-format.ts +20 -5
  105. package/src/client/state/attention-bridge.ts +4 -9
  106. package/src/client/state/attention-store.ts +1 -7
  107. package/src/client/state/canvas-store.ts +52 -36
  108. package/src/client/state/intent-bridge.ts +176 -112
  109. package/src/client/state/intent-store.ts +4 -1
  110. package/src/client/state/sse-bridge.ts +53 -70
  111. package/src/json-render/catalog.ts +12 -16
  112. package/src/json-render/charts/components.tsx +16 -20
  113. package/src/json-render/charts/extra-components.tsx +8 -16
  114. package/src/json-render/charts/extra-definitions.ts +1 -2
  115. package/src/json-render/charts/tufte-components.tsx +37 -20
  116. package/src/json-render/charts/tufte-definitions.ts +8 -2
  117. package/src/json-render/renderer/index.tsx +42 -22
  118. package/src/json-render/schema.ts +6 -3
  119. package/src/json-render/server.ts +33 -39
  120. package/src/mcp/canvas-access.ts +40 -121
  121. package/src/mcp/server.ts +206 -271
  122. package/src/server/agent-context.ts +63 -36
  123. package/src/server/ax-context.ts +7 -5
  124. package/src/server/ax-interaction.ts +176 -43
  125. package/src/server/ax-state-manager.ts +185 -41
  126. package/src/server/ax-state.ts +142 -47
  127. package/src/server/bundled-skills.ts +3 -3
  128. package/src/server/canvas-db.ts +213 -95
  129. package/src/server/canvas-operations.ts +177 -120
  130. package/src/server/canvas-provenance.ts +1 -4
  131. package/src/server/canvas-schema.ts +500 -102
  132. package/src/server/canvas-serialization.ts +27 -35
  133. package/src/server/canvas-state.ts +155 -58
  134. package/src/server/chart-template.ts +4 -5
  135. package/src/server/code-graph.ts +20 -7
  136. package/src/server/diagram-presets.ts +28 -29
  137. package/src/server/ext-app-lookup.ts +4 -20
  138. package/src/server/html-node-summary.ts +19 -10
  139. package/src/server/html-primitives.ts +326 -97
  140. package/src/server/html-surface.ts +6 -9
  141. package/src/server/image-source.ts +6 -4
  142. package/src/server/index.ts +321 -218
  143. package/src/server/intent-registry.ts +3 -6
  144. package/src/server/mcp-app-candidate.ts +5 -10
  145. package/src/server/mcp-app-host.ts +14 -38
  146. package/src/server/mcp-app-runtime.ts +12 -20
  147. package/src/server/mutation-history.ts +15 -5
  148. package/src/server/operations/composites.ts +38 -40
  149. package/src/server/operations/http.ts +5 -5
  150. package/src/server/operations/index.ts +7 -1
  151. package/src/server/operations/invoker.ts +4 -3
  152. package/src/server/operations/mcp.ts +29 -36
  153. package/src/server/operations/ops/annotation.ts +122 -10
  154. package/src/server/operations/ops/app.ts +102 -77
  155. package/src/server/operations/ops/ax-await.ts +17 -10
  156. package/src/server/operations/ops/ax-read.ts +347 -0
  157. package/src/server/operations/ops/ax-shared.ts +2 -7
  158. package/src/server/operations/ops/ax-state.ts +32 -14
  159. package/src/server/operations/ops/ax-timeline.ts +32 -19
  160. package/src/server/operations/ops/ax-work.ts +54 -37
  161. package/src/server/operations/ops/batch.ts +39 -14
  162. package/src/server/operations/ops/canvas-wire.ts +91 -0
  163. package/src/server/operations/ops/edges.ts +37 -25
  164. package/src/server/operations/ops/ext-app.ts +346 -0
  165. package/src/server/operations/ops/groups.ts +49 -20
  166. package/src/server/operations/ops/intent.ts +18 -12
  167. package/src/server/operations/ops/json-render.ts +239 -98
  168. package/src/server/operations/ops/nodes.ts +300 -111
  169. package/src/server/operations/ops/query.ts +62 -33
  170. package/src/server/operations/ops/snapshots.ts +35 -26
  171. package/src/server/operations/ops/validate.ts +2 -1
  172. package/src/server/operations/ops/viewport.ts +60 -16
  173. package/src/server/operations/ops/webview.ts +44 -18
  174. package/src/server/operations/registry.ts +2 -3
  175. package/src/server/operations/types.ts +7 -5
  176. package/src/server/operations/webview-runner.ts +1 -3
  177. package/src/server/placement.ts +8 -18
  178. package/src/server/server.ts +121 -1012
  179. package/src/server/spatial-analysis.ts +39 -25
  180. package/src/server/trace-manager.ts +3 -8
  181. package/src/server/web-artifacts.ts +23 -27
  182. package/src/server/webpage-node.ts +5 -13
  183. package/src/shared/auto-arrange.ts +12 -5
  184. package/src/shared/ax-intent.ts +1 -1
  185. package/src/shared/content-height-reporter.ts +8 -6
  186. package/src/shared/ext-app-tool-result.ts +2 -6
  187. package/src/shared/placement.ts +1 -4
  188. package/src/shared/semantic-attention.ts +39 -37
  189. package/src/shared/surface.ts +8 -4
@@ -82,8 +82,8 @@ interface EdgePathProps {
82
82
  edge: CanvasEdge;
83
83
  fromNode: CanvasNodeState;
84
84
  toNode: CanvasNodeState;
85
- focused: boolean; // connected to the active node
86
- dimmed: boolean; // active node exists but this edge is NOT connected
85
+ focused: boolean; // connected to the active node
86
+ dimmed: boolean; // active node exists but this edge is NOT connected
87
87
  }
88
88
 
89
89
  function EdgePath({ edge, fromNode, toNode, focused, dimmed }: EdgePathProps) {
@@ -108,22 +108,14 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed }: EdgePathProps) {
108
108
  const directed = DIRECTED_TYPES.has(edge.type);
109
109
  const dash = dashArray(edge);
110
110
 
111
- const mid = edge.label
112
- ? bezierMidpoint(start.x, start.y, cx1, cy1, cx2, cy2, end.x, end.y)
113
- : null;
111
+ const mid = edge.label ? bezierMidpoint(start.x, start.y, cx1, cy1, cx2, cy2, end.x, end.y) : null;
114
112
 
115
113
  const pathId = `edge-path-${edge.id}`;
116
114
 
117
115
  return (
118
116
  <g>
119
117
  {/* Invisible wide hitbox for hover/click */}
120
- <path
121
- d={d}
122
- fill="none"
123
- stroke="transparent"
124
- stroke-width="12"
125
- style={{ cursor: 'pointer' }}
126
- />
118
+ <path d={d} fill="none" stroke="transparent" stroke-width="12" style={{ cursor: 'pointer' }} />
127
119
 
128
120
  {/* Glow layer for focused edges */}
129
121
  {focused && (
@@ -171,13 +163,7 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed }: EdgePathProps) {
171
163
  height="20"
172
164
  rx="4"
173
165
  />
174
- <text
175
- class="edge-label"
176
- text-anchor="middle"
177
- dominant-baseline="central"
178
- fill="var(--c-text)"
179
- font-size="11"
180
- >
166
+ <text class="edge-label" text-anchor="middle" dominant-baseline="central" fill="var(--c-text)" font-size="11">
181
167
  {edge.label}
182
168
  </text>
183
169
  </g>
@@ -258,23 +244,38 @@ export function EdgeLayer({ nodes, edges }: EdgeLayerProps) {
258
244
  );
259
245
  })}
260
246
  {/* Live preview edge while drag-connecting */}
261
- {draggingEdge.value && (() => {
262
- const de = draggingEdge.value;
263
- const dx = de.cursorX - de.fromX;
264
- const dy = de.cursorY - de.fromY;
265
- const dist = Math.sqrt(dx * dx + dy * dy);
266
- const curve = Math.min(dist * 0.25, 80);
267
- const nx = dx / (dist || 1);
268
- const ny = dy / (dist || 1);
269
- const previewD = `M ${de.fromX} ${de.fromY} C ${de.fromX + nx * curve} ${de.fromY + ny * curve}, ${de.cursorX - nx * curve} ${de.cursorY - ny * curve}, ${de.cursorX} ${de.cursorY}`;
270
- return (
271
- <g>
272
- <path d={previewD} fill="none" stroke="var(--c-accent)" stroke-width="6" opacity="0.1" style={{ filter: 'blur(3px)' }} />
273
- <path d={previewD} fill="none" stroke="var(--c-accent)" stroke-width="2" stroke-dasharray="6 4" opacity="0.8" />
274
- <circle cx={de.cursorX} cy={de.cursorY} r="5" fill="var(--c-accent)" opacity="0.5" />
275
- </g>
276
- );
277
- })()}
247
+ {draggingEdge.value &&
248
+ (() => {
249
+ const de = draggingEdge.value;
250
+ const dx = de.cursorX - de.fromX;
251
+ const dy = de.cursorY - de.fromY;
252
+ const dist = Math.sqrt(dx * dx + dy * dy);
253
+ const curve = Math.min(dist * 0.25, 80);
254
+ const nx = dx / (dist || 1);
255
+ const ny = dy / (dist || 1);
256
+ const previewD = `M ${de.fromX} ${de.fromY} C ${de.fromX + nx * curve} ${de.fromY + ny * curve}, ${de.cursorX - nx * curve} ${de.cursorY - ny * curve}, ${de.cursorX} ${de.cursorY}`;
257
+ return (
258
+ <g>
259
+ <path
260
+ d={previewD}
261
+ fill="none"
262
+ stroke="var(--c-accent)"
263
+ stroke-width="6"
264
+ opacity="0.1"
265
+ style={{ filter: 'blur(3px)' }}
266
+ />
267
+ <path
268
+ d={previewD}
269
+ fill="none"
270
+ stroke="var(--c-accent)"
271
+ stroke-width="2"
272
+ stroke-dasharray="6 4"
273
+ opacity="0.8"
274
+ />
275
+ <circle cx={de.cursorX} cy={de.cursorY} r="5" fill="var(--c-accent)" opacity="0.5" />
276
+ </g>
277
+ );
278
+ })()}
278
279
  </svg>
279
280
  );
280
281
  }
@@ -83,15 +83,25 @@ function wordCount(text: string): number {
83
83
  }
84
84
 
85
85
  function isPresentationExitMessage(value: unknown, token: string): boolean {
86
- return value !== null &&
86
+ return (
87
+ value !== null &&
87
88
  typeof value === 'object' &&
88
89
  (value as { source?: unknown }).source === 'pmx-canvas-html-node' &&
89
90
  (value as { type?: unknown }).type === 'presentation-exit' &&
90
- (value as { token?: unknown }).token === token;
91
+ (value as { token?: unknown }).token === token
92
+ );
91
93
  }
92
94
 
93
95
  function isPresentationNavigationKey(key: string): boolean {
94
- return key === 'ArrowRight' || key === 'PageDown' || key === ' ' || key === 'ArrowLeft' || key === 'PageUp' || key === 'Home' || key === 'End';
96
+ return (
97
+ key === 'ArrowRight' ||
98
+ key === 'PageDown' ||
99
+ key === ' ' ||
100
+ key === 'ArrowLeft' ||
101
+ key === 'PageUp' ||
102
+ key === 'Home' ||
103
+ key === 'End'
104
+ );
95
105
  }
96
106
 
97
107
  function isPresentationExitButtonTarget(target: EventTarget | null): boolean {
@@ -117,38 +127,49 @@ export function ExpandedNodeOverlay() {
117
127
  setPresenting(true);
118
128
  }, []);
119
129
 
120
- const postPresentationMessage = useCallback((message: Record<string, unknown>) => {
121
- const frame = document.querySelector<HTMLIFrameElement>('.html-presentation-overlay iframe.html-node-frame-presentation');
122
- frame?.contentWindow?.postMessage({
123
- source: 'pmx-canvas-html-node',
124
- token: presentationExitToken,
125
- ...message,
126
- }, '*');
127
- }, [presentationExitToken]);
130
+ const postPresentationMessage = useCallback(
131
+ (message: Record<string, unknown>) => {
132
+ const frame = document.querySelector<HTMLIFrameElement>(
133
+ '.html-presentation-overlay iframe.html-node-frame-presentation',
134
+ );
135
+ frame?.contentWindow?.postMessage(
136
+ {
137
+ source: 'pmx-canvas-html-node',
138
+ token: presentationExitToken,
139
+ ...message,
140
+ },
141
+ '*',
142
+ );
143
+ },
144
+ [presentationExitToken],
145
+ );
128
146
 
129
147
  const handleExitPresentation = useCallback(() => {
130
148
  setPresenting(false);
131
149
  }, []);
132
150
 
133
- const handlePresentationKeyDown = useCallback((event: KeyboardEvent) => {
134
- if (event.key === 'Escape') {
135
- event.preventDefault();
136
- event.stopPropagation();
137
- setPresenting(false);
138
- return;
139
- }
140
- if (event.key === 'Tab' && !isPresentationExitButtonTarget(event.target)) {
151
+ const handlePresentationKeyDown = useCallback(
152
+ (event: KeyboardEvent) => {
153
+ if (event.key === 'Escape') {
154
+ event.preventDefault();
155
+ event.stopPropagation();
156
+ setPresenting(false);
157
+ return;
158
+ }
159
+ if (event.key === 'Tab' && !isPresentationExitButtonTarget(event.target)) {
160
+ event.preventDefault();
161
+ event.stopPropagation();
162
+ presentationExitButtonRef.current?.focus();
163
+ return;
164
+ }
165
+ if ((event.key === ' ' || event.key === 'Enter') && isPresentationExitButtonTarget(event.target)) return;
166
+ if (!isPresentationNavigationKey(event.key)) return;
141
167
  event.preventDefault();
142
168
  event.stopPropagation();
143
- presentationExitButtonRef.current?.focus();
144
- return;
145
- }
146
- if ((event.key === ' ' || event.key === 'Enter') && isPresentationExitButtonTarget(event.target)) return;
147
- if (!isPresentationNavigationKey(event.key)) return;
148
- event.preventDefault();
149
- event.stopPropagation();
150
- postPresentationMessage({ type: 'presentation-key', key: event.key });
151
- }, [postPresentationMessage]);
169
+ postPresentationMessage({ type: 'presentation-key', key: event.key });
170
+ },
171
+ [postPresentationMessage],
172
+ );
152
173
 
153
174
  const handleBackdropPointerDown = useCallback((e: PointerEvent) => {
154
175
  if ((e.target as HTMLElement).classList.contains('expanded-overlay-backdrop')) {
@@ -190,7 +211,9 @@ export function ExpandedNodeOverlay() {
190
211
  document.addEventListener('keydown', handlePresentationKeyDown, true);
191
212
  window.addEventListener('message', handleMessage);
192
213
  return () => {
193
- focusTimers.forEach((timer) => window.clearTimeout(timer));
214
+ focusTimers.forEach((timer) => {
215
+ window.clearTimeout(timer);
216
+ });
194
217
  document.removeEventListener('keydown', handlePresentationKeyDown, true);
195
218
  window.removeEventListener('message', handleMessage);
196
219
  };
@@ -198,16 +221,14 @@ export function ExpandedNodeOverlay() {
198
221
 
199
222
  if (!node) return null;
200
223
 
201
- const title =
202
- (node.data.title as string) ||
203
- (node.data.path as string)?.split('/').pop() ||
204
- TYPE_LABELS[node.type];
224
+ const title = (node.data.title as string) || (node.data.path as string)?.split('/').pop() || TYPE_LABELS[node.type];
205
225
  const textContent = getNodeTextContent(node);
206
226
  const words = wordCount(textContent);
207
227
  const isCtxPinned = nodeId ? contextPinnedNodeIds.value.has(nodeId) : false;
208
228
  const hasText = textContent.length > 0;
209
229
  const pendingClose = pendingExpandedNodeCloseId.value === nodeId;
210
- const isEmbeddedViewer = node.type === 'mcp-app' || node.type === 'webpage' || node.type === 'json-render' || node.type === 'graph';
230
+ const isEmbeddedViewer =
231
+ node.type === 'mcp-app' || node.type === 'webpage' || node.type === 'json-render' || node.type === 'graph';
211
232
  const canPresent = shouldShowPresentationControls(node);
212
233
 
213
234
  return (
@@ -294,12 +315,7 @@ export function ExpandedNodeOverlay() {
294
315
 
295
316
  {/* Copy content */}
296
317
  {hasText && (
297
- <button
298
- type="button"
299
- class="expanded-action-btn"
300
- onClick={handleCopy}
301
- title="Copy content to clipboard"
302
- >
318
+ <button type="button" class="expanded-action-btn" onClick={handleCopy} title="Copy content to clipboard">
303
319
  {copied ? 'Copied!' : 'Copy'}
304
320
  </button>
305
321
  )}
@@ -373,13 +389,21 @@ export function ExpandedNodeOverlay() {
373
389
  }}
374
390
  >
375
391
  {isEmbeddedViewer ? (
376
- <div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
377
- {renderContent(node, true)}
378
- </div>
379
- ) : renderContent(node, true)}
392
+ <div style={{ flex: 1, minHeight: 0, display: 'flex' }}>{renderContent(node, true)}</div>
393
+ ) : (
394
+ renderContent(node, true)
395
+ )}
380
396
  </div>
381
397
  {canPresent && presenting && (
382
- <div ref={presentationOverlayRef} class="html-presentation-overlay" role="dialog" aria-modal="true" aria-label={`Present ${title}`} tabIndex={-1} onKeyDownCapture={handlePresentationKeyDown}>
398
+ <div
399
+ ref={presentationOverlayRef}
400
+ class="html-presentation-overlay"
401
+ role="dialog"
402
+ aria-modal="true"
403
+ aria-label={`Present ${title}`}
404
+ tabIndex={-1}
405
+ onKeyDownCapture={handlePresentationKeyDown}
406
+ >
383
407
  <button
384
408
  ref={presentationExitButtonRef}
385
409
  type="button"
@@ -20,9 +20,7 @@ function getNodeRect(nodeId: string): Rect | null {
20
20
  }
21
21
 
22
22
  function getRegionRect(nodeIds: string[]): Rect | null {
23
- const rects = nodeIds
24
- .map((nodeId) => getNodeRect(nodeId))
25
- .filter((rect): rect is Rect => rect !== null);
23
+ const rects = nodeIds.map((nodeId) => getNodeRect(nodeId)).filter((rect): rect is Rect => rect !== null);
26
24
  if (rects.length === 0) return null;
27
25
 
28
26
  const minLeft = Math.min(...rects.map((rect) => rect.left));
@@ -62,13 +60,7 @@ export function FocusFieldLayer() {
62
60
  {regions.map((region) => {
63
61
  const rect = getRegionRect(region.nodeIds);
64
62
  if (!rect) return null;
65
- return (
66
- <div
67
- key={region.id}
68
- class="attention-field-region"
69
- style={rectStyle(rect, 42)}
70
- />
71
- );
63
+ return <div key={region.id} class="attention-field-region" style={rectStyle(rect, 42)} />;
72
64
  })}
73
65
  {secondaryNodeIds.map((nodeId) => {
74
66
  const rect = getNodeRect(nodeId);
@@ -77,12 +69,15 @@ export function FocusFieldLayer() {
77
69
  <div
78
70
  key={`secondary-${nodeId}`}
79
71
  class="attention-field-node attention-field-secondary"
80
- style={rectStyle({
81
- left: rect.left - 18,
82
- top: rect.top - 18,
83
- width: rect.width + 36,
84
- height: rect.height + 36,
85
- }, 28)}
72
+ style={rectStyle(
73
+ {
74
+ left: rect.left - 18,
75
+ top: rect.top - 18,
76
+ width: rect.width + 36,
77
+ height: rect.height + 36,
78
+ },
79
+ 28,
80
+ )}
86
81
  />
87
82
  );
88
83
  })}
@@ -93,12 +88,15 @@ export function FocusFieldLayer() {
93
88
  <div
94
89
  key={`primary-${nodeId}`}
95
90
  class="attention-field-node attention-field-primary"
96
- style={rectStyle({
97
- left: rect.left - 24,
98
- top: rect.top - 24,
99
- width: rect.width + 48,
100
- height: rect.height + 48,
101
- }, 30)}
91
+ style={rectStyle(
92
+ {
93
+ left: rect.left - 24,
94
+ top: rect.top - 24,
95
+ width: rect.width + 48,
96
+ height: rect.height + 48,
97
+ },
98
+ 30,
99
+ )}
102
100
  />
103
101
  );
104
102
  })}
@@ -1,10 +1,6 @@
1
1
  import { useEffect } from 'preact/hooks';
2
2
  import { nodes } from '../state/canvas-store';
3
- import {
4
- hoveredIntentId,
5
- intents,
6
- type ClientIntent,
7
- } from '../state/intent-store';
3
+ import { hoveredIntentId, intents, type ClientIntent } from '../state/intent-store';
8
4
  import { vetoGhostIntent } from '../state/intent-bridge';
9
5
  import { getNodeIcon } from '../icons';
10
6
  import { TYPE_LABELS } from '../types';
@@ -75,8 +71,7 @@ function bezierPath(a: { x: number; y: number }, b: { x: number; y: number }): s
75
71
  function GhostInfo({ intent }: { intent: ClientIntent }) {
76
72
  const NodeIcon = isKnownNodeType(intent.nodeType) ? getNodeIcon(intent.nodeType) : null;
77
73
  const label = intent.label || intent.kind;
78
- const confidencePct =
79
- typeof intent.confidence === 'number' ? `${Math.round(intent.confidence * 100)}%` : null;
74
+ const confidencePct = typeof intent.confidence === 'number' ? `${Math.round(intent.confidence * 100)}%` : null;
80
75
  return (
81
76
  <div
82
77
  class="intent-info"
@@ -160,9 +155,7 @@ function GhostOverlay({ intent, rect, variant }: { intent: ClientIntent; rect: R
160
155
  }
161
156
 
162
157
  function renderGhost(intent: ClientIntent) {
163
- const settledRect = intent.phase === 'settling'
164
- ? getNodeRect(intent.settledNodeId)
165
- : null;
158
+ const settledRect = intent.phase === 'settling' ? getNodeRect(intent.settledNodeId) : null;
166
159
  switch (intent.kind) {
167
160
  case 'create': {
168
161
  if (!intent.position) return null;
@@ -174,7 +167,12 @@ function renderGhost(intent: ClientIntent) {
174
167
  if (!intent.position) return null;
175
168
  const source = getNodeRect(intent.nodeId);
176
169
  const size = source ?? DEFAULT_GHOST_SIZE;
177
- const rect: Rect = settledRect ?? { left: intent.position.x, top: intent.position.y, width: size.width, height: size.height };
170
+ const rect: Rect = settledRect ?? {
171
+ left: intent.position.x,
172
+ top: intent.position.y,
173
+ width: size.width,
174
+ height: size.height,
175
+ };
178
176
  return <GhostBox key={intent.id} intent={intent} rect={rect} />;
179
177
  }
180
178
  case 'remove': {
@@ -192,7 +190,10 @@ function renderGhost(intent: ClientIntent) {
192
190
  const from = getNodeRect(intent.edge.from);
193
191
  const to = getNodeRect(intent.edge.to);
194
192
  if (!from || !to) return null;
195
- const mid = { x: (from.left + from.width / 2 + to.left + to.width / 2) / 2, y: (from.top + from.height / 2 + to.top + to.height / 2) / 2 };
193
+ const mid = {
194
+ x: (from.left + from.width / 2 + to.left + to.width / 2) / 2,
195
+ y: (from.top + from.height / 2 + to.top + to.height / 2) / 2,
196
+ };
196
197
  const rect: Rect = { left: mid.x - 110, top: mid.y - 18, width: 220, height: 36 };
197
198
  // The bezier itself is drawn in the shared SVG layer; here we anchor the info card.
198
199
  return (
@@ -200,7 +201,12 @@ function renderGhost(intent: ClientIntent) {
200
201
  key={intent.id}
201
202
  class={`intent-ghost intent-ghost-connect is-${intent.phase}`}
202
203
  data-intent-id={intent.id}
203
- style={{ left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, opacity: ghostOpacity(intent) }}
204
+ style={{
205
+ left: `${rect.left}px`,
206
+ top: `${rect.top}px`,
207
+ width: `${rect.width}px`,
208
+ opacity: ghostOpacity(intent),
209
+ }}
204
210
  >
205
211
  <GhostInfo intent={intent} />
206
212
  </div>
@@ -234,7 +240,18 @@ export function IntentLayer() {
234
240
 
235
241
  return (
236
242
  <div class="intent-layer">
237
- <svg class="intent-line-layer" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', overflow: 'visible', pointerEvents: 'none' }}>
243
+ <svg
244
+ class="intent-line-layer"
245
+ style={{
246
+ position: 'absolute',
247
+ top: 0,
248
+ left: 0,
249
+ width: '100%',
250
+ height: '100%',
251
+ overflow: 'visible',
252
+ pointerEvents: 'none',
253
+ }}
254
+ >
238
255
  <defs>
239
256
  <marker id="intent-arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
240
257
  <path d="M0,0 L6,3 L0,6 Z" class="intent-arrow-head" />
@@ -108,14 +108,7 @@ interface MinimapProps {
108
108
  containerHeight: number;
109
109
  }
110
110
 
111
- export function Minimap({
112
- viewport,
113
- nodes,
114
- edges,
115
- onNavigate,
116
- containerWidth,
117
- containerHeight,
118
- }: MinimapProps) {
111
+ export function Minimap({ viewport, nodes, edges, onNavigate, containerWidth, containerHeight }: MinimapProps) {
119
112
  const canvasRef = useRef<HTMLCanvasElement>(null);
120
113
  const isDragging = useRef(false);
121
114
  const frameRef = useRef<MinimapFrame | null>(null);
@@ -216,12 +209,15 @@ export function Minimap({
216
209
  scheduleDraw();
217
210
  }, [containerWidth, containerHeight, scheduleDraw]);
218
211
 
219
- useEffect(() => () => {
220
- if (drawRafId.current !== null) {
221
- window.cancelAnimationFrame(drawRafId.current);
222
- drawRafId.current = null;
223
- }
224
- }, []);
212
+ useEffect(
213
+ () => () => {
214
+ if (drawRafId.current !== null) {
215
+ window.cancelAnimationFrame(drawRafId.current);
216
+ drawRafId.current = null;
217
+ }
218
+ },
219
+ [],
220
+ );
225
221
 
226
222
  const handleNavigateFromEvent = useCallback(
227
223
  (e: MouseEvent | PointerEvent) => {
@@ -233,8 +229,7 @@ export function Minimap({
233
229
  const my = e.clientY - rect.top;
234
230
 
235
231
  const frame =
236
- frameRef.current
237
- ?? computeMinimapFrame(nodes.value, viewport.value, containerWidth, containerHeight);
232
+ frameRef.current ?? computeMinimapFrame(nodes.value, viewport.value, containerWidth, containerHeight);
238
233
  frameRef.current = frame;
239
234
  const { bounds, scale } = frame;
240
235
 
@@ -1,14 +1,9 @@
1
1
  import { useCallback } from 'preact/hooks';
2
- import {
3
- addContextPins,
4
- clearSelection,
5
- selectedNodeIds,
6
- } from '../state/canvas-store';
2
+ import { addContextPins, clearSelection, selectedNodeIds } from '../state/canvas-store';
7
3
  import { createEdgeFromClient, createGroupFromClient } from '../state/intent-bridge';
8
4
 
9
5
  export function SelectionBar() {
10
6
  const count = selectedNodeIds.value.size;
11
- if (count === 0) return null;
12
7
 
13
8
  const handlePinContext = useCallback(() => {
14
9
  const ids = Array.from(selectedNodeIds.value);
@@ -34,16 +29,14 @@ export function SelectionBar() {
34
29
  clearSelection();
35
30
  }, []);
36
31
 
32
+ if (count === 0) return null;
33
+
37
34
  return (
38
35
  <div class="selection-bar">
39
36
  <span class="selection-bar-count">
40
37
  {'\u2726'} {count} node{count !== 1 ? 's' : ''} selected
41
38
  </span>
42
- <button
43
- type="button"
44
- class="selection-bar-btn selection-bar-pin-ctx"
45
- onClick={handlePinContext}
46
- >
39
+ <button type="button" class="selection-bar-btn selection-bar-pin-ctx" onClick={handlePinContext}>
47
40
  Pin as context
48
41
  </button>
49
42
  {count >= 2 && (
@@ -48,7 +48,9 @@ export function ShortcutOverlay({ onClose }: { onClose: () => void }) {
48
48
  <div class="shortcut-overlay" onMouseDown={(e) => e.stopPropagation()}>
49
49
  <div class="shortcut-overlay-header">
50
50
  <span class="shortcut-overlay-title">Keyboard Shortcuts</span>
51
- <span class="shortcut-overlay-hint">Press <kbd>?</kbd> or <kbd>Esc</kbd> to close</span>
51
+ <span class="shortcut-overlay-hint">
52
+ Press <kbd>?</kbd> or <kbd>Esc</kbd> to close
53
+ </span>
52
54
  </div>
53
55
  <div class="shortcut-overlay-body">
54
56
  {GROUPS.map((group) => (