pmx-canvas 0.3.0 → 0.3.2
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.
- package/CHANGELOG.md +73 -0
- package/dist/canvas/index.js +65 -65
- package/dist/types/cli/agent.d.ts +9 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/server/server.d.ts +8 -0
- package/docs/cli.md +10 -1
- package/docs/http-api.md +28 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-07.md +2 -2
- package/package.json +5 -2
- package/skills/pmx-canvas/SKILL.md +15 -10
- package/skills/pmx-canvas/references/full-reference.md +17 -3
- package/src/cli/agent.ts +1951 -1571
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +80 -323
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +194 -80
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +35 -21
- package/src/mcp/server.ts +132 -70
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +182 -39
- package/src/server/ax-state.ts +142 -47
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +180 -123
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +454 -73
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +150 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +19 -6
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +3 -12
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +320 -217
- package/src/server/intent-registry.ts +2 -5
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +1 -3
- package/src/server/operations/http.ts +2 -3
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +22 -30
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +98 -73
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +41 -15
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +298 -109
- package/src/server/operations/ops/query.ts +46 -28
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +122 -1028
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -42,7 +42,17 @@ export function AttentionHistory() {
|
|
|
42
42
|
aria-label={unread > 0 ? `Recent updates — ${unread} new` : 'Recent updates'}
|
|
43
43
|
title={unread > 0 ? `${unread} new updates since last viewed` : 'Recent updates'}
|
|
44
44
|
>
|
|
45
|
-
<svg
|
|
45
|
+
<svg
|
|
46
|
+
width="14"
|
|
47
|
+
height="14"
|
|
48
|
+
viewBox="0 0 16 16"
|
|
49
|
+
fill="none"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
stroke-width="1.5"
|
|
52
|
+
stroke-linecap="round"
|
|
53
|
+
stroke-linejoin="round"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
46
56
|
<rect x="1.5" y="2.5" width="13" height="11" rx="1.5" />
|
|
47
57
|
<circle cx="4.5" cy="8" r="1.1" fill="currentColor" stroke="none" />
|
|
48
58
|
<line x1="6.5" y1="6.5" x2="12.5" y2="6.5" />
|
|
@@ -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(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
}, [
|
|
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':
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
)
|
|
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({
|
|
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(
|
|
633
|
-
e
|
|
634
|
-
|
|
635
|
-
|
|
631
|
+
const handleDrop = useCallback(
|
|
632
|
+
async (e: DragEvent) => {
|
|
633
|
+
e.preventDefault();
|
|
634
|
+
setDropActive(false);
|
|
635
|
+
dropCounter.current = 0;
|
|
636
636
|
|
|
637
|
-
|
|
638
|
-
|
|
637
|
+
const container = containerRef.current;
|
|
638
|
+
if (!container || !e.dataTransfer) return;
|
|
639
639
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
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:
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
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 &&
|
|
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 +=
|
|
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
|
-
{
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 () => {
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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(
|
|
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={
|
|
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
|
}
|