pmx-canvas 0.3.0 → 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 (156) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/canvas/index.js +2 -2
  3. package/dist/types/cli/daemon.d.ts +74 -0
  4. package/dist/types/cli/watch.d.ts +2 -2
  5. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  6. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  7. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  8. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  9. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  10. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  11. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  12. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  13. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  14. package/dist/types/server/canvas-schema.d.ts +1 -1
  15. package/dist/types/server/html-primitives.d.ts +1 -1
  16. package/dist/types/server/index.d.ts +4 -4
  17. package/dist/types/server/operations/index.d.ts +1 -1
  18. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  19. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  20. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  21. package/docs/http-api.md +28 -0
  22. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  23. package/docs/tech-debt-assessment-2026-07.md +2 -2
  24. package/package.json +5 -2
  25. package/skills/pmx-canvas/SKILL.md +3 -1
  26. package/skills/pmx-canvas/references/full-reference.md +10 -3
  27. package/src/cli/agent.ts +1861 -1548
  28. package/src/cli/daemon.ts +460 -0
  29. package/src/cli/index.ts +63 -326
  30. package/src/cli/watch.ts +2 -10
  31. package/src/client/App.tsx +48 -46
  32. package/src/client/canvas/AttentionHistory.tsx +11 -1
  33. package/src/client/canvas/CanvasNode.tsx +41 -29
  34. package/src/client/canvas/CanvasViewport.tsx +101 -66
  35. package/src/client/canvas/CommandPalette.tsx +61 -27
  36. package/src/client/canvas/ContextMenu.tsx +13 -20
  37. package/src/client/canvas/ContextPinBar.tsx +1 -5
  38. package/src/client/canvas/ContextPinHud.tsx +1 -6
  39. package/src/client/canvas/DockedNode.tsx +4 -4
  40. package/src/client/canvas/EdgeLayer.tsx +37 -36
  41. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  42. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  43. package/src/client/canvas/IntentLayer.tsx +31 -14
  44. package/src/client/canvas/Minimap.tsx +11 -16
  45. package/src/client/canvas/SelectionBar.tsx +4 -11
  46. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  47. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  48. package/src/client/canvas/auto-fit.ts +15 -14
  49. package/src/client/canvas/snap-guides.ts +12 -12
  50. package/src/client/canvas/use-node-resize.ts +1 -5
  51. package/src/client/canvas/use-pan-zoom.ts +25 -26
  52. package/src/client/ext-app/bridge.ts +3 -12
  53. package/src/client/icons.tsx +63 -20
  54. package/src/client/nodes/ContextNode.tsx +14 -25
  55. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  56. package/src/client/nodes/FileNode.tsx +74 -62
  57. package/src/client/nodes/GroupNode.tsx +4 -6
  58. package/src/client/nodes/HtmlNode.tsx +76 -46
  59. package/src/client/nodes/ImageNode.tsx +18 -27
  60. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  61. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  62. package/src/client/nodes/LedgerNode.tsx +10 -4
  63. package/src/client/nodes/MarkdownNode.tsx +3 -10
  64. package/src/client/nodes/McpAppNode.tsx +26 -22
  65. package/src/client/nodes/MdFormatBar.tsx +10 -7
  66. package/src/client/nodes/PromptNode.tsx +23 -51
  67. package/src/client/nodes/ResponseNode.tsx +3 -13
  68. package/src/client/nodes/StatusNode.tsx +5 -9
  69. package/src/client/nodes/StatusSummary.tsx +2 -8
  70. package/src/client/nodes/WebpageNode.tsx +20 -14
  71. package/src/client/nodes/iframe-document-url.ts +25 -16
  72. package/src/client/nodes/image-warnings.ts +1 -7
  73. package/src/client/nodes/md-format.ts +20 -5
  74. package/src/client/state/attention-bridge.ts +4 -9
  75. package/src/client/state/attention-store.ts +1 -7
  76. package/src/client/state/canvas-store.ts +52 -36
  77. package/src/client/state/intent-bridge.ts +176 -112
  78. package/src/client/state/intent-store.ts +4 -1
  79. package/src/client/state/sse-bridge.ts +53 -70
  80. package/src/json-render/catalog.ts +12 -16
  81. package/src/json-render/charts/components.tsx +16 -20
  82. package/src/json-render/charts/extra-components.tsx +8 -16
  83. package/src/json-render/charts/extra-definitions.ts +1 -2
  84. package/src/json-render/charts/tufte-components.tsx +37 -20
  85. package/src/json-render/charts/tufte-definitions.ts +8 -2
  86. package/src/json-render/renderer/index.tsx +42 -22
  87. package/src/json-render/schema.ts +6 -3
  88. package/src/json-render/server.ts +33 -39
  89. package/src/mcp/canvas-access.ts +35 -21
  90. package/src/mcp/server.ts +132 -70
  91. package/src/server/agent-context.ts +63 -36
  92. package/src/server/ax-context.ts +7 -5
  93. package/src/server/ax-interaction.ts +176 -43
  94. package/src/server/ax-state-manager.ts +182 -39
  95. package/src/server/ax-state.ts +142 -47
  96. package/src/server/canvas-db.ts +213 -95
  97. package/src/server/canvas-operations.ts +177 -120
  98. package/src/server/canvas-provenance.ts +1 -4
  99. package/src/server/canvas-schema.ts +454 -73
  100. package/src/server/canvas-serialization.ts +27 -35
  101. package/src/server/canvas-state.ts +150 -58
  102. package/src/server/chart-template.ts +4 -5
  103. package/src/server/code-graph.ts +19 -6
  104. package/src/server/diagram-presets.ts +28 -29
  105. package/src/server/ext-app-lookup.ts +3 -12
  106. package/src/server/html-node-summary.ts +19 -10
  107. package/src/server/html-primitives.ts +326 -97
  108. package/src/server/html-surface.ts +6 -9
  109. package/src/server/image-source.ts +6 -4
  110. package/src/server/index.ts +320 -217
  111. package/src/server/intent-registry.ts +2 -5
  112. package/src/server/mcp-app-candidate.ts +5 -10
  113. package/src/server/mcp-app-host.ts +14 -38
  114. package/src/server/mcp-app-runtime.ts +12 -20
  115. package/src/server/mutation-history.ts +15 -5
  116. package/src/server/operations/composites.ts +1 -3
  117. package/src/server/operations/http.ts +2 -3
  118. package/src/server/operations/index.ts +7 -1
  119. package/src/server/operations/invoker.ts +4 -3
  120. package/src/server/operations/mcp.ts +22 -30
  121. package/src/server/operations/ops/annotation.ts +122 -10
  122. package/src/server/operations/ops/app.ts +98 -73
  123. package/src/server/operations/ops/ax-await.ts +17 -10
  124. package/src/server/operations/ops/ax-read.ts +347 -0
  125. package/src/server/operations/ops/ax-shared.ts +2 -7
  126. package/src/server/operations/ops/ax-state.ts +32 -14
  127. package/src/server/operations/ops/ax-timeline.ts +32 -19
  128. package/src/server/operations/ops/ax-work.ts +54 -37
  129. package/src/server/operations/ops/batch.ts +39 -14
  130. package/src/server/operations/ops/canvas-wire.ts +91 -0
  131. package/src/server/operations/ops/edges.ts +37 -25
  132. package/src/server/operations/ops/ext-app.ts +346 -0
  133. package/src/server/operations/ops/groups.ts +49 -20
  134. package/src/server/operations/ops/intent.ts +18 -12
  135. package/src/server/operations/ops/json-render.ts +239 -98
  136. package/src/server/operations/ops/nodes.ts +298 -109
  137. package/src/server/operations/ops/query.ts +46 -28
  138. package/src/server/operations/ops/snapshots.ts +35 -26
  139. package/src/server/operations/ops/validate.ts +2 -1
  140. package/src/server/operations/ops/viewport.ts +60 -16
  141. package/src/server/operations/ops/webview.ts +44 -18
  142. package/src/server/operations/registry.ts +2 -3
  143. package/src/server/operations/types.ts +7 -5
  144. package/src/server/operations/webview-runner.ts +1 -3
  145. package/src/server/placement.ts +8 -18
  146. package/src/server/server.ts +108 -1027
  147. package/src/server/spatial-analysis.ts +39 -25
  148. package/src/server/trace-manager.ts +3 -8
  149. package/src/server/web-artifacts.ts +23 -27
  150. package/src/server/webpage-node.ts +5 -13
  151. package/src/shared/auto-arrange.ts +12 -5
  152. package/src/shared/content-height-reporter.ts +8 -6
  153. package/src/shared/ext-app-tool-result.ts +2 -6
  154. package/src/shared/placement.ts +1 -4
  155. package/src/shared/semantic-attention.ts +39 -37
  156. package/src/shared/surface.ts +8 -4
@@ -1,9 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
2
- import {
3
- attentionPrimaryNodeIds,
4
- attentionPulseNodeIds,
5
- attentionSecondaryNodeIds,
6
- } from '../state/attention-store';
2
+ import { attentionPrimaryNodeIds, attentionPulseNodeIds, attentionSecondaryNodeIds } from '../state/attention-store';
7
3
  import {
8
4
  activeNodeId,
9
5
  activeNeighborNodeIds,
@@ -55,16 +51,19 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
55
51
  const renameRef = useRef<HTMLInputElement>(null);
56
52
 
57
53
  // ── Drag (with snap alignment) ──────────────────────
58
- const handleMove = useCallback((id: string, x: number, y: number) => {
59
- const snap = snapToGuides(x, y, node.size.width, node.size.height);
60
- const current = nodes.value.get(id);
61
- if (current?.position.x === snap.x && current.position.y === snap.y) {
54
+ const handleMove = useCallback(
55
+ (id: string, x: number, y: number) => {
56
+ const snap = snapToGuides(x, y, node.size.width, node.size.height);
57
+ const current = nodes.value.get(id);
58
+ if (current?.position.x === snap.x && current.position.y === snap.y) {
59
+ activeGuides.value = snap.guides.length > 0 ? snap.guides : null;
60
+ return;
61
+ }
62
+ updateNode(id, { position: { x: snap.x, y: snap.y } });
62
63
  activeGuides.value = snap.guides.length > 0 ? snap.guides : null;
63
- return;
64
- }
65
- updateNode(id, { position: { x: snap.x, y: snap.y } });
66
- activeGuides.value = snap.guides.length > 0 ? snap.guides : null;
67
- }, [node.size.width, node.size.height]);
64
+ },
65
+ [node.size.width, node.size.height],
66
+ );
68
67
 
69
68
  const handleDragEnd = useCallback(() => {
70
69
  clearSnapCache();
@@ -196,7 +195,16 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
196
195
  autoFitPersistTimer.current = null;
197
196
  }
198
197
  };
199
- }, [node.id, node.type, node.data.mode, node.data.strictSize, node.collapsed, node.dockPosition, node.size.width, node.size.height]);
198
+ }, [
199
+ node.id,
200
+ node.type,
201
+ node.data.mode,
202
+ node.data.strictSize,
203
+ node.collapsed,
204
+ node.dockPosition,
205
+ node.size.width,
206
+ node.size.height,
207
+ ]);
200
208
 
201
209
  const isPinned = node.pinned;
202
210
  const isTrace = node.type === 'trace';
@@ -237,12 +245,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
237
245
  .join(' ');
238
246
 
239
247
  return (
240
- <div
241
- class={nodeClass}
242
- style={nodeStyle}
243
- onPointerDown={handlePointerDown}
244
- onContextMenu={handleContextMenuEvent}
245
- >
248
+ <div class={nodeClass} style={nodeStyle} onPointerDown={handlePointerDown} onContextMenu={handleContextMenuEvent}>
246
249
  <div class="node-titlebar" onPointerDown={handleTitlePointerDown}>
247
250
  <span class="node-type-icon" aria-hidden="true">
248
251
  {(() => {
@@ -345,10 +348,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
345
348
  </div>
346
349
  )}
347
350
  {!node.collapsed && (
348
- <div
349
- class="node-resize-handle"
350
- onPointerDown={(e) => startResize(e, node.size.width, node.size.height)}
351
- />
351
+ <div class="node-resize-handle" onPointerDown={(e) => startResize(e, node.size.width, node.size.height)} />
352
352
  )}
353
353
  {/* Connection port handles — visible on hover, drag to connect */}
354
354
  {(['top', 'right', 'bottom', 'left'] as const).map((side) => (
@@ -364,10 +364,22 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
364
364
  const hh = node.size.height / 2;
365
365
  let px: number, py: number;
366
366
  switch (side) {
367
- case 'top': px = cx; py = cy - hh; break;
368
- case 'bottom': px = cx; py = cy + hh; break;
369
- case 'left': px = cx - hw; py = cy; break;
370
- case 'right': px = cx + hw; py = cy; break;
367
+ case 'top':
368
+ px = cx;
369
+ py = cy - hh;
370
+ break;
371
+ case 'bottom':
372
+ px = cx;
373
+ py = cy + hh;
374
+ break;
375
+ case 'left':
376
+ px = cx - hw;
377
+ py = cy;
378
+ break;
379
+ case 'right':
380
+ px = cx + hw;
381
+ py = cy;
382
+ break;
371
383
  }
372
384
  draggingEdge.value = {
373
385
  fromId: node.id,
@@ -102,7 +102,8 @@ function findAnnotationAtPoint(
102
102
  point.x > annotation.bounds.x + annotation.bounds.width + pad ||
103
103
  point.y < annotation.bounds.y - pad ||
104
104
  point.y > annotation.bounds.y + annotation.bounds.height + pad
105
- ) continue;
105
+ )
106
+ continue;
106
107
  if (annotation.type === 'text') return annotation;
107
108
  for (let index = 1; index < annotation.points.length; index++) {
108
109
  const start = annotation.points[index - 1];
@@ -175,9 +176,7 @@ function extractUrlsFromText(text: string): string[] {
175
176
  const trimmed = text.trim();
176
177
  if (!trimmed) return [];
177
178
 
178
- const rawCandidates = trimmed.includes('\n')
179
- ? trimmed.split(/\r?\n/)
180
- : trimmed.split(/\s+/);
179
+ const rawCandidates = trimmed.includes('\n') ? trimmed.split(/\r?\n/) : trimmed.split(/\s+/);
181
180
  const seen = new Set<string>();
182
181
  const urls: string[] = [];
183
182
 
@@ -236,7 +235,12 @@ export function getRenderableWorldNodes(
236
235
  return worldNodes;
237
236
  }
238
237
 
239
- export function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotationMode = false, annotationTool = null }: CanvasViewportProps) {
238
+ export function CanvasViewport({
239
+ onNodeContextMenu,
240
+ onCanvasContextMenu,
241
+ annotationMode = false,
242
+ annotationTool = null,
243
+ }: CanvasViewportProps) {
240
244
  const v = viewport.value;
241
245
  const isLassoing = useRef(false);
242
246
  const isAnnotating = useRef(false);
@@ -405,7 +409,7 @@ export function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotat
405
409
  const previous = annotationPoints.current.at(-1);
406
410
  if (previous && Math.hypot(point.x - previous.x, point.y - previous.y) < 2) return;
407
411
  annotationPoints.current = [...annotationPoints.current, point];
408
- setDraftAnnotation((draft) => draft ? { ...draft, points: annotationPoints.current } : null);
412
+ setDraftAnnotation((draft) => (draft ? { ...draft, points: annotationPoints.current } : null));
409
413
  return;
410
414
  }
411
415
 
@@ -465,12 +469,7 @@ export function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotat
465
469
  if (node.dockPosition !== null) continue;
466
470
  const nx = node.position.x;
467
471
  const ny = node.position.y;
468
- if (
469
- nx + node.size.width > worldMinX &&
470
- nx < worldMaxX &&
471
- ny + node.size.height > worldMinY &&
472
- ny < worldMaxY
473
- ) {
472
+ if (nx + node.size.width > worldMinX && nx < worldMaxX && ny + node.size.height > worldMinY && ny < worldMaxY) {
474
473
  hits.push(node.id);
475
474
  }
476
475
  }
@@ -626,58 +625,77 @@ export function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotat
626
625
  if (dropCounter.current <= 0) {
627
626
  dropCounter.current = 0;
628
627
  setDropActive(false);
629
- }
628
+ }
630
629
  }, []);
631
630
 
632
- const handleDrop = useCallback(async (e: DragEvent) => {
633
- e.preventDefault();
634
- setDropActive(false);
635
- dropCounter.current = 0;
631
+ const handleDrop = useCallback(
632
+ async (e: DragEvent) => {
633
+ e.preventDefault();
634
+ setDropActive(false);
635
+ dropCounter.current = 0;
636
636
 
637
- const container = containerRef.current;
638
- if (!container || !e.dataTransfer) return;
637
+ const container = containerRef.current;
638
+ if (!container || !e.dataTransfer) return;
639
639
 
640
- const rect = container.getBoundingClientRect();
641
- const vp = viewport.value;
642
- const baseWx = (e.clientX - rect.left - vp.x) / vp.scale;
643
- const baseWy = (e.clientY - rect.top - vp.y) / vp.scale;
640
+ const rect = container.getBoundingClientRect();
641
+ const vp = viewport.value;
642
+ const baseWx = (e.clientX - rect.left - vp.x) / vp.scale;
643
+ const baseWy = (e.clientY - rect.top - vp.y) / vp.scale;
644
644
 
645
- const files = Array.from(e.dataTransfer.files);
646
- if (files.length === 0) {
647
- const urls = getTransferUrls(e.dataTransfer);
648
- await createWebpageNodes(urls, baseWx, baseWy);
649
- return;
650
- }
645
+ const files = Array.from(e.dataTransfer.files);
646
+ if (files.length === 0) {
647
+ const urls = getTransferUrls(e.dataTransfer);
648
+ await createWebpageNodes(urls, baseWx, baseWy);
649
+ return;
650
+ }
651
651
 
652
- const nodeW = 400;
653
- const nodeH = 300;
654
- const spacing = 20;
655
- const cols = Math.ceil(Math.sqrt(files.length));
656
-
657
- for (let i = 0; i < files.length; i++) {
658
- const file = files[i];
659
- const col = i % cols;
660
- const row = Math.floor(i / cols);
661
- const wx = baseWx - (cols * (nodeW + spacing)) / 2 + col * (nodeW + spacing);
662
- const wy = baseWy - nodeH / 2 + row * (nodeH + spacing);
663
-
664
- const type = nodeTypeFromFilename(file.name);
665
- const fileName = file.name;
666
-
667
- if (type === 'image') {
668
- const reader = new FileReader();
669
- const dataUri: string = await new Promise((resolve) => {
670
- reader.onload = () => resolve(reader.result as string);
671
- reader.readAsDataURL(file);
672
- });
673
- await createNodeFromClient({ type: 'image', title: fileName, content: dataUri, x: wx, y: wy, width: nodeW, height: nodeH });
674
- } else {
675
- const text = await file.text();
676
- const isWide = type === 'markdown' || type === 'file';
677
- await createNodeFromClient({ type, title: fileName, content: text, x: wx, y: wy, width: isWide ? 720 : nodeW, height: isWide ? 500 : nodeH });
652
+ const nodeW = 400;
653
+ const nodeH = 300;
654
+ const spacing = 20;
655
+ const cols = Math.ceil(Math.sqrt(files.length));
656
+
657
+ for (let i = 0; i < files.length; i++) {
658
+ const file = files[i];
659
+ const col = i % cols;
660
+ const row = Math.floor(i / cols);
661
+ const wx = baseWx - (cols * (nodeW + spacing)) / 2 + col * (nodeW + spacing);
662
+ const wy = baseWy - nodeH / 2 + row * (nodeH + spacing);
663
+
664
+ const type = nodeTypeFromFilename(file.name);
665
+ const fileName = file.name;
666
+
667
+ if (type === 'image') {
668
+ const reader = new FileReader();
669
+ const dataUri: string = await new Promise((resolve) => {
670
+ reader.onload = () => resolve(reader.result as string);
671
+ reader.readAsDataURL(file);
672
+ });
673
+ await createNodeFromClient({
674
+ type: 'image',
675
+ title: fileName,
676
+ content: dataUri,
677
+ x: wx,
678
+ y: wy,
679
+ width: nodeW,
680
+ height: nodeH,
681
+ });
682
+ } else {
683
+ const text = await file.text();
684
+ const isWide = type === 'markdown' || type === 'file';
685
+ await createNodeFromClient({
686
+ type,
687
+ title: fileName,
688
+ content: text,
689
+ x: wx,
690
+ y: wy,
691
+ width: isWide ? 720 : nodeW,
692
+ height: isWide ? 500 : nodeH,
693
+ });
694
+ }
678
695
  }
679
- }
680
- }, [containerRef, createWebpageNodes]);
696
+ },
697
+ [containerRef, createWebpageNodes],
698
+ );
681
699
 
682
700
  useEffect(() => {
683
701
  const handlePaste = async (e: ClipboardEvent) => {
@@ -750,13 +768,14 @@ export function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotat
750
768
  height: '100%',
751
769
  position: 'relative',
752
770
  overflow: 'hidden',
753
- cursor: annotationTool === 'eraser'
754
- ? 'cell'
755
- : annotationTool === 'text'
756
- ? 'text'
757
- : annotationMode || draggingEdge.value || isLassoing.current
758
- ? 'crosshair'
759
- : 'grab',
771
+ cursor:
772
+ annotationTool === 'eraser'
773
+ ? 'cell'
774
+ : annotationTool === 'text'
775
+ ? 'text'
776
+ : annotationMode || draggingEdge.value || isLassoing.current
777
+ ? 'crosshair'
778
+ : 'grab',
760
779
  }}
761
780
  >
762
781
  {/* D4: CSS matrix(a,b,c,d,tx,ty) — scale uniformly (a=d=scale, b=c=0)
@@ -784,7 +803,18 @@ export function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotat
784
803
  ))}
785
804
  {/* Snap alignment guide lines */}
786
805
  {activeGuides.value && (
787
- <svg class="snap-guides-svg" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none', overflow: 'visible' }}>
806
+ <svg
807
+ class="snap-guides-svg"
808
+ style={{
809
+ position: 'absolute',
810
+ top: 0,
811
+ left: 0,
812
+ width: '100%',
813
+ height: '100%',
814
+ pointerEvents: 'none',
815
+ overflow: 'visible',
816
+ }}
817
+ >
788
818
  {activeGuides.value.map((g, i) =>
789
819
  g.axis === 'x' ? (
790
820
  <line key={i} x1={g.pos} y1={g.from - 20} x2={g.pos} y2={g.to + 20} class="snap-guide-line" />
@@ -795,7 +825,12 @@ export function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotat
795
825
  </svg>
796
826
  )}
797
827
  </div>
798
- {annotationMode && <div class={`annotation-capture-layer${annotationTool === 'eraser' ? ' erasing' : ''}${annotationTool === 'text' ? ' text' : ''}`} aria-hidden="true" />}
828
+ {annotationMode && (
829
+ <div
830
+ class={`annotation-capture-layer${annotationTool === 'eraser' ? ' erasing' : ''}${annotationTool === 'text' ? ' text' : ''}`}
831
+ aria-hidden="true"
832
+ />
833
+ )}
799
834
  {textDraft && (
800
835
  <input
801
836
  class="annotation-text-input"
@@ -39,7 +39,7 @@ function fuzzyMatch(query: string, text: string): { match: boolean; score: numbe
39
39
  if (t[ti] === q[qi]) {
40
40
  indices.push(ti);
41
41
  // Consecutive chars bonus
42
- score += (lastIdx === ti - 1) ? 10 : 1;
42
+ score += lastIdx === ti - 1 ? 10 : 1;
43
43
  // Start-of-word bonus
44
44
  if (ti === 0 || t[ti - 1] === ' ' || t[ti - 1] === '-' || t[ti - 1] === '_') score += 5;
45
45
  lastIdx = ti;
@@ -94,13 +94,7 @@ function parseTypeFilter(query: string): { typeFilter: CanvasNodeState['type'] |
94
94
  }
95
95
 
96
96
  // ── Component ───────────────────────────────────────────────
97
- export function CommandPalette({
98
- onClose,
99
- onToggleMinimap,
100
- }: {
101
- onClose: () => void;
102
- onToggleMinimap: () => void;
103
- }) {
97
+ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => void; onToggleMinimap: () => void }) {
104
98
  const [query, setQuery] = useState('');
105
99
  const [selectedIndex, setSelectedIndex] = useState(0);
106
100
  const inputRef = useRef<HTMLInputElement>(null);
@@ -135,11 +129,46 @@ export function CommandPalette({
135
129
 
136
130
  // Action items
137
131
  const actions: Array<{ label: string; badge: string; action: () => void }> = [
138
- { label: 'New note (markdown node)', badge: 'CREATE', action: () => { createNodeFromClient({ type: 'markdown', title: 'New note', width: 520, height: 360 }); onClose(); } },
139
- { label: 'Fit all nodes', badge: 'VIEW', action: () => { fitAll(window.innerWidth, window.innerHeight); onClose(); } },
140
- { label: 'Auto-arrange (grid)', badge: 'LAYOUT', action: () => { autoArrange(); onClose(); } },
141
- { label: 'Auto-arrange (graph-aware)', badge: 'LAYOUT', action: () => { forceDirectedArrange(); onClose(); } },
142
- { label: 'Toggle minimap', badge: 'VIEW', action: () => { onToggleMinimap(); onClose(); } },
132
+ {
133
+ label: 'New note (markdown node)',
134
+ badge: 'CREATE',
135
+ action: () => {
136
+ createNodeFromClient({ type: 'markdown', title: 'New note', width: 520, height: 360 });
137
+ onClose();
138
+ },
139
+ },
140
+ {
141
+ label: 'Fit all nodes',
142
+ badge: 'VIEW',
143
+ action: () => {
144
+ fitAll(window.innerWidth, window.innerHeight);
145
+ onClose();
146
+ },
147
+ },
148
+ {
149
+ label: 'Auto-arrange (grid)',
150
+ badge: 'LAYOUT',
151
+ action: () => {
152
+ autoArrange();
153
+ onClose();
154
+ },
155
+ },
156
+ {
157
+ label: 'Auto-arrange (graph-aware)',
158
+ badge: 'LAYOUT',
159
+ action: () => {
160
+ forceDirectedArrange();
161
+ onClose();
162
+ },
163
+ },
164
+ {
165
+ label: 'Toggle minimap',
166
+ badge: 'VIEW',
167
+ action: () => {
168
+ onToggleMinimap();
169
+ onClose();
170
+ },
171
+ },
143
172
  {
144
173
  label: 'Toggle theme (dark/light)',
145
174
  badge: 'THEME',
@@ -215,7 +244,9 @@ export function CommandPalette({
215
244
 
216
245
  // Clear highlights on unmount (palette close)
217
246
  useEffect(() => {
218
- return () => { searchHighlightIds.value = null; };
247
+ return () => {
248
+ searchHighlightIds.value = null;
249
+ };
219
250
  }, []);
220
251
 
221
252
  // Clamp selected index
@@ -268,15 +299,22 @@ export function CommandPalette({
268
299
  placeholder={`Search nodes and actions... (${MOD_KEY}+K)`}
269
300
  />
270
301
  <div class="command-palette-hint">
271
- <span><kbd>{'\u2191'}</kbd><kbd>{'\u2193'}</kbd> navigate</span>
272
- <span><kbd>{'\u21B5'}</kbd> select</span>
273
- <span><kbd>esc</kbd> close</span>
274
- <span><kbd>type:</kbd> filter by type</span>
302
+ <span>
303
+ <kbd>{'\u2191'}</kbd>
304
+ <kbd>{'\u2193'}</kbd> navigate
305
+ </span>
306
+ <span>
307
+ <kbd>{'\u21B5'}</kbd> select
308
+ </span>
309
+ <span>
310
+ <kbd>esc</kbd> close
311
+ </span>
312
+ <span>
313
+ <kbd>type:</kbd> filter by type
314
+ </span>
275
315
  </div>
276
316
  <div class="command-palette-results" ref={listRef}>
277
- {filtered.length === 0 && (
278
- <div class="command-palette-empty">No matching nodes or actions</div>
279
- )}
317
+ {filtered.length === 0 && <div class="command-palette-empty">No matching nodes or actions</div>}
280
318
  {filtered.map((item, i) => (
281
319
  <button
282
320
  key={item.id}
@@ -285,15 +323,11 @@ export function CommandPalette({
285
323
  onMouseEnter={() => setSelectedIndex(i)}
286
324
  onClick={() => item.action()}
287
325
  >
288
- <span class={`command-palette-badge${item.badgeClass ? ` ${item.badgeClass}` : ''}`}>
289
- {item.badge}
290
- </span>
326
+ <span class={`command-palette-badge${item.badgeClass ? ` ${item.badgeClass}` : ''}`}>{item.badge}</span>
291
327
  <span class="command-palette-label">
292
328
  {item.indices.length > 0 ? highlightMatch(item.label, item.indices) : item.label}
293
329
  </span>
294
- {item.description && (
295
- <span class="command-palette-desc">{item.description}</span>
296
- )}
330
+ {item.description && <span class="command-palette-desc">{item.description}</span>}
297
331
  </button>
298
332
  ))}
299
333
  </div>
@@ -103,9 +103,7 @@ export function ContextMenu({ menu, onClose }: ContextMenuProps) {
103
103
  items = buildCanvasMenuItems(menu.canvasX, menu.canvasY);
104
104
  }
105
105
  const keyCounts = new Map<string, number>();
106
- const estimatedHeight = items.some((item) => item.render)
107
- ? items.length * 32 + 168
108
- : items.length * 32 + 8;
106
+ const estimatedHeight = items.some((item) => item.render) ? items.length * 32 + 168 : items.length * 32 + 8;
109
107
 
110
108
  // Keep menu on screen
111
109
  const adjustedX = Math.min(menu.x, Math.max(12, window.innerWidth - 240));
@@ -128,7 +126,7 @@ export function ContextMenu({ menu, onClose }: ContextMenuProps) {
128
126
  ? 'separator'
129
127
  : item.render
130
128
  ? 'custom'
131
- : `${item.label ?? 'item'}:${item.shortcut ?? ''}`;
129
+ : `${item.label ?? 'item'}:${item.shortcut ?? ''}`;
132
130
  const nextCount = (keyCounts.get(baseKey) ?? 0) + 1;
133
131
  keyCounts.set(baseKey, nextCount);
134
132
  const itemKey = `${baseKey}:${nextCount}`;
@@ -239,10 +237,7 @@ function renderGroupColorSection(node: CanvasNodeState, onClose: () => void): Co
239
237
  onClose();
240
238
  }}
241
239
  >
242
- <span
243
- class="context-menu-color-dot"
244
- style={{ '--swatch-color': preset.value }}
245
- />
240
+ <span class="context-menu-color-dot" style={{ '--swatch-color': preset.value }} />
246
241
  <span>{preset.label}</span>
247
242
  </button>
248
243
  );
@@ -267,7 +262,12 @@ function renderGroupColorSection(node: CanvasNodeState, onClose: () => void): Co
267
262
  );
268
263
  }
269
264
 
270
- function centeredPosition(canvasX: number, canvasY: number, width: number, height: number): {
265
+ function centeredPosition(
266
+ canvasX: number,
267
+ canvasY: number,
268
+ width: number,
269
+ height: number,
270
+ ): {
271
271
  x: number;
272
272
  y: number;
273
273
  } {
@@ -452,9 +452,7 @@ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
452
452
  const pending = pendingConnection.value;
453
453
  if (pending && pending.from !== node.id) {
454
454
  const sourceNode = nodes.value.get(pending.from);
455
- const sourceTitle = sourceNode
456
- ? ((sourceNode.data.title as string) || sourceNode.id).slice(0, 20)
457
- : pending.from;
455
+ const sourceTitle = sourceNode ? ((sourceNode.data.title as string) || sourceNode.id).slice(0, 20) : pending.from;
458
456
  items.push({
459
457
  label: `Connect from "${sourceTitle}"`,
460
458
  action: () => {
@@ -472,9 +470,7 @@ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
472
470
  });
473
471
 
474
472
  // Show edge count
475
- const edgeCount = Array.from(edges.value.values()).filter(
476
- (e) => e.from === node.id || e.to === node.id,
477
- ).length;
473
+ const edgeCount = Array.from(edges.value.values()).filter((e) => e.from === node.id || e.to === node.id).length;
478
474
  if (edgeCount > 0) {
479
475
  items.push({
480
476
  label: `${edgeCount} edge${edgeCount !== 1 ? 's' : ''} connected`,
@@ -502,8 +498,7 @@ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
502
498
  if (node.type === 'mcp-app' || node.type === 'json-render' || node.type === 'graph') {
503
499
  if (node.data.chartConfig) {
504
500
  // Chart ext-app node — chart-specific actions
505
- const chartTitle =
506
- ((node.data.chartConfig as Record<string, unknown>).title as string) || 'chart';
501
+ const chartTitle = ((node.data.chartConfig as Record<string, unknown>).title as string) || 'chart';
507
502
  items.push({
508
503
  label: 'Copy chart data',
509
504
  action: () => {
@@ -527,9 +522,7 @@ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
527
522
  items.push({
528
523
  label: 'Copy spec',
529
524
  action: () => {
530
- navigator.clipboard.writeText(
531
- JSON.stringify(node.data.spec ?? node.data.graphConfig ?? {}, null, 2),
532
- );
525
+ navigator.clipboard.writeText(JSON.stringify(node.data.spec ?? node.data.graphConfig ?? {}, null, 2));
533
526
  },
534
527
  });
535
528
  }
@@ -1,8 +1,4 @@
1
- import {
2
- clearContextPins,
3
- contextPinnedNodeIds,
4
- hasOpenDockedContextPanel,
5
- } from '../state/canvas-store';
1
+ import { clearContextPins, contextPinnedNodeIds, hasOpenDockedContextPanel } from '../state/canvas-store';
6
2
  import { attentionHistoryOpen } from '../state/attention-store';
7
3
 
8
4
  export function ContextPinBar() {
@@ -9,12 +9,7 @@ export function ContextPinHud() {
9
9
  <span class="context-pin-hud-label">
10
10
  {'\u2726'} {count} in context
11
11
  </span>
12
- <button
13
- type="button"
14
- class="context-pin-hud-clear"
15
- onClick={clearContextPins}
16
- title="Clear all context pins"
17
- >
12
+ <button type="button" class="context-pin-hud-clear" onClick={clearContextPins} title="Clear all context pins">
18
13
  ×
19
14
  </button>
20
15
  </div>
@@ -59,7 +59,9 @@ function ContextDockedNode({ node }: { node: CanvasNodeState }) {
59
59
  e.stopPropagation();
60
60
  expand();
61
61
  }}
62
- title={hasItems ? `${count} item${count === 1 ? '' : 's'} in agent context — expand` : 'Expand agent context'}
62
+ title={
63
+ hasItems ? `${count} item${count === 1 ? '' : 's'} in agent context — expand` : 'Expand agent context'
64
+ }
63
65
  aria-label={hasItems ? `Context — ${count} item${count === 1 ? '' : 's'}` : 'Expand agent context'}
64
66
  >
65
67
  {'▸'}
@@ -157,9 +159,7 @@ export function DockedNode({ node }: { node: CanvasNodeState }) {
157
159
  </button>
158
160
  </div>
159
161
  </div>
160
- {!node.collapsed && (
161
- <div class="docked-node-body">{renderDockedContent(node)}</div>
162
- )}
162
+ {!node.collapsed && <div class="docked-node-body">{renderDockedContent(node)}</div>}
163
163
  </div>
164
164
  );
165
165
  }