pmx-canvas 0.4.8 → 0.5.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.
- package/.github/extensions/pmx-canvas/extension.mjs +154 -5
- package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/CHANGELOG.md +156 -0
- package/Readme.md +72 -23
- package/dist/canvas/global.css +3071 -796
- package/dist/canvas/index.js +122 -104
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/pump.d.ts +9 -0
- package/dist/types/client/App.d.ts +1 -1
- package/dist/types/client/canvas/AgentPresenceLayer.d.ts +11 -0
- package/dist/types/client/canvas/BarHint.d.ts +22 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +5 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +4 -4
- package/dist/types/client/canvas/CommandBar.d.ts +10 -0
- package/dist/types/client/canvas/ConnectionBanner.d.ts +13 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +6 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +2 -1
- package/dist/types/client/canvas/EmptyState.d.ts +11 -0
- package/dist/types/client/canvas/ExternalWriters.d.ts +5 -0
- package/dist/types/client/canvas/HumanPresenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/Minimap.d.ts +12 -1
- package/dist/types/client/canvas/ScopeFenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +6 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +1 -0
- package/dist/types/client/canvas/SessionReceipt.d.ts +17 -0
- package/dist/types/client/canvas/TextPrompt.d.ts +25 -0
- package/dist/types/client/canvas/ToolRail.d.ts +21 -0
- package/dist/types/client/canvas/TopBar.d.ts +7 -0
- package/dist/types/client/canvas/canvas-area.d.ts +39 -0
- package/dist/types/client/canvas/create-in-view.d.ts +18 -0
- package/dist/types/client/canvas/import-files.d.ts +8 -0
- package/dist/types/client/canvas/kind-colors.d.ts +3 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +14 -0
- package/dist/types/client/canvas/use-now.d.ts +6 -0
- package/dist/types/client/icons.d.ts +11 -4
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +2 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +3 -4
- package/dist/types/client/state/canvas-store.d.ts +49 -5
- package/dist/types/client/state/human-store.d.ts +26 -0
- package/dist/types/client/state/iframe-mode.d.ts +1 -0
- package/dist/types/client/state/intent-bridge.d.ts +37 -5
- package/dist/types/client/state/node-factory.d.ts +0 -1
- package/dist/types/client/state/presence-store.d.ts +78 -0
- package/dist/types/client/state/session-store.d.ts +176 -0
- package/dist/types/client/types.d.ts +2 -1
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/server/agent-presence.d.ts +192 -0
- package/dist/types/server/ax-gate-ttl.d.ts +6 -0
- package/dist/types/server/ax-interaction.d.ts +4 -10
- package/dist/types/server/ax-state-manager.d.ts +21 -2
- package/dist/types/server/ax-state.d.ts +49 -3
- package/dist/types/server/ax-wait.d.ts +13 -0
- package/dist/types/server/canvas-db.d.ts +9 -1
- package/dist/types/server/canvas-operations.d.ts +11 -0
- package/dist/types/server/canvas-state.d.ts +27 -13
- package/dist/types/server/canvas-validation.d.ts +0 -14
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/human-presence.d.ts +40 -0
- package/dist/types/server/index.d.ts +29 -3
- package/dist/types/server/intent-registry.d.ts +7 -0
- package/dist/types/server/mutation-history.d.ts +15 -2
- package/dist/types/server/operations/invoker.d.ts +13 -2
- package/dist/types/server/operations/ops/ax-presence.d.ts +2 -0
- package/dist/types/server/operations/ops/human-presence.d.ts +3 -0
- package/dist/types/server/operations/registry.d.ts +11 -0
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/scope-fence.d.ts +62 -0
- package/dist/types/server/server.d.ts +0 -12
- package/dist/types/shared/agent-presence.d.ts +130 -0
- package/dist/types/shared/approval-gates.d.ts +19 -0
- package/dist/types/shared/auto-arrange.d.ts +0 -1
- package/dist/types/shared/ax-kinds.d.ts +11 -0
- package/dist/types/shared/human-presence.d.ts +31 -0
- package/dist/types/shared/iframe-probe.d.ts +8 -0
- package/dist/types/shared/scope-fence.d.ts +21 -0
- package/docs/RELEASE.md +20 -0
- package/docs/ax-host-adapter-contract.md +30 -14
- package/docs/cli.md +37 -1
- package/docs/environment.md +3 -0
- package/docs/http-api.md +222 -5
- package/docs/mcp.md +6 -5
- package/docs/node-types.md +3 -1
- package/docs/plans/plan-009-tech-debt-backlog.md +13 -0
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/multi-agent-live.png +0 -0
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +1 -1
- package/package.json +4 -4
- package/skills/pmx-canvas/SKILL.md +112 -17
- package/skills/pmx-canvas/references/codex-app-adapter.md +28 -0
- package/skills/pmx-canvas/references/full-reference.md +55 -16
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +41 -24
- package/skills/pmx-canvas-orchestration/SKILL.md +56 -2
- package/skills/pmx-canvas-testing/SKILL.md +17 -0
- package/src/cli/agent.ts +1 -0
- package/src/cli/commands/copilot.ts +15 -8
- package/src/cli/commands/nodes.ts +1 -19
- package/src/cli/commands/pump.ts +263 -0
- package/src/cli/index.ts +2 -5
- package/src/cli/shared.ts +1 -1
- package/src/client/App.tsx +209 -585
- package/src/client/canvas/AgentPresenceLayer.tsx +74 -0
- package/src/client/canvas/AttentionHistory.tsx +0 -10
- package/src/client/canvas/BarHint.tsx +49 -0
- package/src/client/canvas/CanvasNode.tsx +488 -136
- package/src/client/canvas/CanvasViewport.tsx +190 -94
- package/src/client/canvas/CommandBar.tsx +140 -0
- package/src/client/canvas/CommandPalette.tsx +133 -58
- package/src/client/canvas/ConnectionBanner.tsx +65 -0
- package/src/client/canvas/ContextMenu.tsx +88 -27
- package/src/client/canvas/ContextPinBar.tsx +2 -2
- package/src/client/canvas/EdgeLayer.tsx +121 -38
- package/src/client/canvas/EmptyState.tsx +93 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +64 -118
- package/src/client/canvas/ExternalWriters.tsx +308 -0
- package/src/client/canvas/FocusFieldLayer.tsx +12 -1
- package/src/client/canvas/HumanPresenceLayer.tsx +45 -0
- package/src/client/canvas/IntentLayer.tsx +1 -1
- package/src/client/canvas/Minimap.tsx +117 -190
- package/src/client/canvas/ScopeFenceLayer.tsx +38 -0
- package/src/client/canvas/SelectionBar.tsx +175 -23
- package/src/client/canvas/SessionPanel.tsx +481 -0
- package/src/client/canvas/SessionReceipt.tsx +141 -0
- package/src/client/canvas/ShortcutOverlay.tsx +21 -6
- package/src/client/canvas/SnapshotPanel.tsx +166 -80
- package/src/client/canvas/TextPrompt.tsx +128 -0
- package/src/client/canvas/ToolRail.tsx +447 -0
- package/src/client/canvas/TopBar.tsx +308 -0
- package/src/client/canvas/auto-fit.ts +1 -5
- package/src/client/canvas/canvas-area.ts +74 -0
- package/src/client/canvas/create-in-view.ts +48 -0
- package/src/client/canvas/import-files.ts +64 -0
- package/src/client/canvas/kind-colors.ts +22 -0
- package/src/client/canvas/snap-guides.ts +1 -1
- package/src/client/canvas/use-focus-trap.ts +54 -0
- package/src/client/canvas/use-now.ts +17 -0
- package/src/client/canvas/use-pan-zoom.ts +13 -2
- package/src/client/icons.tsx +57 -18
- package/src/client/nodes/ExtAppFrame.tsx +86 -20
- package/src/client/nodes/GroupNode.tsx +9 -28
- package/src/client/nodes/InlineFormatBar.tsx +4 -3
- package/src/client/nodes/InlineMarkdownEditor.tsx +27 -0
- package/src/client/nodes/MarkdownNode.tsx +59 -24
- package/src/client/nodes/MdFormatBar.tsx +2 -4
- package/src/client/nodes/inline-editor-commands.ts +17 -6
- package/src/client/nodes/use-surface-frame.ts +3 -1
- package/src/client/state/attention-bridge.ts +3 -0
- package/src/client/state/canvas-store.ts +285 -90
- package/src/client/state/human-store.ts +175 -0
- package/src/client/state/iframe-mode.ts +33 -9
- package/src/client/state/intent-bridge.ts +75 -13
- package/src/client/state/node-factory.ts +2 -8
- package/src/client/state/presence-store.ts +168 -0
- package/src/client/state/session-store.ts +419 -0
- package/src/client/state/sse-bridge.ts +93 -30
- package/src/client/theme/global.css +3071 -796
- package/src/client/types.ts +3 -1
- package/src/client/utils/platform.ts +6 -1
- package/src/mcp/canvas-access.ts +2 -2
- package/src/server/agent-presence.ts +755 -0
- package/src/server/ax-context.ts +45 -0
- package/src/server/ax-gate-ttl.ts +61 -0
- package/src/server/ax-interaction.ts +29 -1
- package/src/server/ax-state-manager.ts +78 -6
- package/src/server/ax-state.ts +82 -15
- package/src/server/ax-wait.ts +33 -0
- package/src/server/canvas-db.ts +103 -33
- package/src/server/canvas-operations.ts +58 -14
- package/src/server/canvas-state.ts +131 -58
- package/src/server/canvas-validation.ts +3 -53
- package/src/server/demo-state.json +2 -70
- package/src/server/html-surface.ts +11 -2
- package/src/server/human-presence.ts +160 -0
- package/src/server/index.ts +111 -6
- package/src/server/intent-registry.ts +13 -0
- package/src/server/mutation-history.ts +47 -2
- package/src/server/operations/composites.ts +6 -3
- package/src/server/operations/http.ts +10 -1
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/invoker.ts +33 -6
- package/src/server/operations/ops/ax-presence.ts +62 -0
- package/src/server/operations/ops/ax-read.ts +8 -0
- package/src/server/operations/ops/ax-state.ts +106 -1
- package/src/server/operations/ops/ax-timeline.ts +54 -5
- package/src/server/operations/ops/ax-work.ts +46 -0
- package/src/server/operations/ops/batch.ts +3 -1
- package/src/server/operations/ops/edges.ts +88 -2
- package/src/server/operations/ops/groups.ts +4 -3
- package/src/server/operations/ops/human-presence.ts +35 -0
- package/src/server/operations/ops/json-render.ts +11 -1
- package/src/server/operations/ops/nodes.ts +77 -16
- package/src/server/operations/ops/query.ts +1 -0
- package/src/server/operations/ops/snapshots.ts +7 -4
- package/src/server/operations/registry.ts +196 -1
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +207 -0
- package/src/server/server.ts +177 -79
- package/src/server/trace-manager.ts +0 -2
- package/src/server/web-artifacts.ts +0 -1
- package/src/shared/agent-presence.ts +162 -0
- package/src/shared/approval-gates.ts +35 -0
- package/src/shared/auto-arrange.ts +121 -13
- package/src/shared/ax-kinds.ts +23 -0
- package/src/shared/human-presence.ts +30 -0
- package/src/shared/iframe-probe.ts +8 -0
- package/src/shared/scope-fence.ts +31 -0
- package/dist/types/client/canvas/DockedNode.d.ts +0 -4
- package/src/client/canvas/DockedNode.tsx +0 -165
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
import { agentIdentityHue } from '../../shared/agent-presence.js';
|
|
1
2
|
import type { Signal } from '@preact/signals';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { useCallback, useRef } from 'preact/hooks';
|
|
4
|
+
import { fenceRectFromNodes } from '../../shared/scope-fence.js';
|
|
5
|
+
import { selectedNodeIds } from '../state/canvas-store';
|
|
6
|
+
import { agentPresences, presenceWorldPosition } from '../state/presence-store';
|
|
7
|
+
import { scopeFence } from '../state/session-store';
|
|
6
8
|
import type { CanvasEdge, CanvasNodeState, ViewportState } from '../types';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
import { KIND_COLOR } from './kind-colors';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Minimap v2 (rail-chrome-v2 phase 7, design item 19): a true-scale node map
|
|
13
|
+
* rendered from the store — each node a scaled rect in its kind color, groups
|
|
14
|
+
* and the scope fence as dashed outlines, the viewport frame with a grab
|
|
15
|
+
* cursor, the zoom % in the corner, selection outlines mirrored, and a pulsing
|
|
16
|
+
* violet dot where an attached agent is. 168×112 at rest; hovering magnifies
|
|
17
|
+
* the whole map ×1.7 from the bottom-right corner (CSS). Click jumps the
|
|
18
|
+
* viewport; dragging pans.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const MINIMAP_W = 168;
|
|
22
|
+
export const MINIMAP_H = 112;
|
|
10
23
|
const PADDING = 20;
|
|
11
24
|
|
|
12
25
|
interface MinimapBounds {
|
|
@@ -21,39 +34,6 @@ interface MinimapFrame {
|
|
|
21
34
|
scale: number;
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
function getNodeColors(): Record<CanvasNodeState['type'], string> {
|
|
25
|
-
const t = getCanvasTokens();
|
|
26
|
-
return {
|
|
27
|
-
markdown: t.accent,
|
|
28
|
-
'mcp-app': t.ok,
|
|
29
|
-
webpage: t.warn,
|
|
30
|
-
'json-render': t.ok,
|
|
31
|
-
graph: t.purple,
|
|
32
|
-
prompt: t.accent,
|
|
33
|
-
response: t.ok,
|
|
34
|
-
status: t.warn,
|
|
35
|
-
context: t.muted,
|
|
36
|
-
ledger: t.dim,
|
|
37
|
-
trace: t.purple,
|
|
38
|
-
file: t.accent,
|
|
39
|
-
diff: t.ok,
|
|
40
|
-
mermaid: t.purple,
|
|
41
|
-
image: t.ok,
|
|
42
|
-
html: t.warn,
|
|
43
|
-
group: t.dim,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getEdgeColors(): Record<CanvasEdge['type'], string> {
|
|
48
|
-
const t = getCanvasTokens();
|
|
49
|
-
return {
|
|
50
|
-
relation: t.muted,
|
|
51
|
-
'depends-on': t.warn,
|
|
52
|
-
flow: t.accent,
|
|
53
|
-
references: t.dim,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
37
|
export function computeMinimapFrame(
|
|
58
38
|
nodeMap: Map<string, CanvasNodeState>,
|
|
59
39
|
currentViewport: ViewportState,
|
|
@@ -110,139 +90,32 @@ interface MinimapProps {
|
|
|
110
90
|
containerHeight: number;
|
|
111
91
|
}
|
|
112
92
|
|
|
113
|
-
export function Minimap({ viewport, nodes,
|
|
114
|
-
const
|
|
93
|
+
export function Minimap({ viewport, nodes, onNavigate, containerWidth, containerHeight }: MinimapProps) {
|
|
94
|
+
const mapRef = useRef<HTMLDivElement>(null);
|
|
115
95
|
const isDragging = useRef(false);
|
|
116
|
-
const frameRef = useRef<MinimapFrame | null>(null);
|
|
117
|
-
const drawRafId = useRef<number | null>(null);
|
|
118
|
-
|
|
119
|
-
const draw = useCallback(() => {
|
|
120
|
-
const canvas = canvasRef.current;
|
|
121
|
-
if (!canvas) return;
|
|
122
|
-
const ctx = canvas.getContext('2d');
|
|
123
|
-
if (!ctx) return;
|
|
124
|
-
const nodeMap = nodes.value;
|
|
125
|
-
const edgeMap = edges.value;
|
|
126
|
-
const currentViewport = viewport.value;
|
|
127
|
-
|
|
128
|
-
const dpr = window.devicePixelRatio || 1;
|
|
129
|
-
canvas.width = MINIMAP_W * dpr;
|
|
130
|
-
canvas.height = MINIMAP_H * dpr;
|
|
131
|
-
ctx.scale(dpr, dpr);
|
|
132
|
-
|
|
133
|
-
// Clear
|
|
134
|
-
ctx.clearRect(0, 0, MINIMAP_W, MINIMAP_H);
|
|
135
|
-
const t = getCanvasTokens();
|
|
136
|
-
ctx.fillStyle = t.panel + 'd9'; // panel color with ~85% alpha
|
|
137
|
-
ctx.fillRect(0, 0, MINIMAP_W, MINIMAP_H);
|
|
138
|
-
|
|
139
|
-
const frame = computeMinimapFrame(nodeMap, currentViewport, containerWidth, containerHeight);
|
|
140
|
-
frameRef.current = frame;
|
|
141
|
-
const { bounds, scale } = frame;
|
|
142
|
-
|
|
143
|
-
const toMiniX = (x: number) => (x - bounds.minX) * scale;
|
|
144
|
-
const toMiniY = (y: number) => (y - bounds.minY) * scale;
|
|
145
|
-
|
|
146
|
-
// Draw nodes
|
|
147
|
-
const all = Array.from(nodeMap.values());
|
|
148
|
-
const nodeColors = getNodeColors();
|
|
149
|
-
for (const n of all) {
|
|
150
|
-
ctx.fillStyle = nodeColors[n.type] ?? t.muted;
|
|
151
|
-
ctx.globalAlpha = 0.6;
|
|
152
|
-
ctx.fillRect(
|
|
153
|
-
toMiniX(n.position.x),
|
|
154
|
-
toMiniY(n.position.y),
|
|
155
|
-
Math.max(4, n.size.width * scale),
|
|
156
|
-
Math.max(3, n.size.height * scale),
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const edgeColors = getEdgeColors();
|
|
161
|
-
for (const edge of edgeMap.values()) {
|
|
162
|
-
const fromNode = nodeMap.get(edge.from);
|
|
163
|
-
const toNode = nodeMap.get(edge.to);
|
|
164
|
-
if (!fromNode || !toNode) continue;
|
|
165
|
-
const fromCx = toMiniX(fromNode.position.x + fromNode.size.width / 2);
|
|
166
|
-
const fromCy = toMiniY(fromNode.position.y + fromNode.size.height / 2);
|
|
167
|
-
const toCx = toMiniX(toNode.position.x + toNode.size.width / 2);
|
|
168
|
-
const toCy = toMiniY(toNode.position.y + toNode.size.height / 2);
|
|
169
|
-
ctx.beginPath();
|
|
170
|
-
ctx.moveTo(fromCx, fromCy);
|
|
171
|
-
ctx.lineTo(toCx, toCy);
|
|
172
|
-
ctx.strokeStyle = edgeColors[edge.type] ?? t.muted;
|
|
173
|
-
ctx.globalAlpha = 0.3;
|
|
174
|
-
ctx.lineWidth = 0.5;
|
|
175
|
-
ctx.stroke();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Draw viewport rectangle
|
|
179
|
-
const vpLeft = -currentViewport.x / currentViewport.scale;
|
|
180
|
-
const vpTop = -currentViewport.y / currentViewport.scale;
|
|
181
|
-
const vpW = containerWidth / currentViewport.scale;
|
|
182
|
-
const vpH = containerHeight / currentViewport.scale;
|
|
183
|
-
|
|
184
|
-
ctx.globalAlpha = 1;
|
|
185
|
-
ctx.strokeStyle = t.accent;
|
|
186
|
-
ctx.lineWidth = 1.5;
|
|
187
|
-
ctx.strokeRect(toMiniX(vpLeft), toMiniY(vpTop), vpW * scale, vpH * scale);
|
|
188
|
-
}, [nodes, edges, viewport, containerWidth, containerHeight]);
|
|
189
|
-
|
|
190
|
-
const drawRef = useRef(draw);
|
|
191
|
-
drawRef.current = draw;
|
|
192
|
-
|
|
193
|
-
const scheduleDraw = useCallback(() => {
|
|
194
|
-
if (drawRafId.current !== null) return;
|
|
195
|
-
drawRafId.current = window.requestAnimationFrame(() => {
|
|
196
|
-
drawRafId.current = null;
|
|
197
|
-
drawRef.current();
|
|
198
|
-
});
|
|
199
|
-
}, []);
|
|
200
96
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
scheduleDraw();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
useEffect(() => {
|
|
211
|
-
scheduleDraw();
|
|
212
|
-
}, [containerWidth, containerHeight, scheduleDraw]);
|
|
213
|
-
|
|
214
|
-
useEffect(
|
|
215
|
-
() => () => {
|
|
216
|
-
if (drawRafId.current !== null) {
|
|
217
|
-
window.cancelAnimationFrame(drawRafId.current);
|
|
218
|
-
drawRafId.current = null;
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
[],
|
|
222
|
-
);
|
|
97
|
+
const nodeMap = nodes.value;
|
|
98
|
+
const v = viewport.value;
|
|
99
|
+
const frame = computeMinimapFrame(nodeMap, v, containerWidth, containerHeight);
|
|
100
|
+
const { bounds, scale } = frame;
|
|
101
|
+
const toX = (x: number) => (x - bounds.minX) * scale;
|
|
102
|
+
const toY = (y: number) => (y - bounds.minY) * scale;
|
|
223
103
|
|
|
224
104
|
const handleNavigateFromEvent = useCallback(
|
|
225
105
|
(e: MouseEvent | PointerEvent) => {
|
|
226
|
-
const
|
|
227
|
-
if (!
|
|
228
|
-
|
|
229
|
-
const rect =
|
|
230
|
-
const mx = e.clientX - rect.left;
|
|
231
|
-
const my = e.clientY - rect.top;
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const vpW = containerWidth / v.scale;
|
|
240
|
-
const vpH = containerHeight / v.scale;
|
|
241
|
-
|
|
242
|
-
// Center viewport on clicked point
|
|
243
|
-
const worldX = mx / scale + bounds.minX;
|
|
244
|
-
const worldY = my / scale + bounds.minY;
|
|
245
|
-
onNavigate(-(worldX - vpW / 2) * v.scale, -(worldY - vpH / 2) * v.scale);
|
|
106
|
+
const map = mapRef.current;
|
|
107
|
+
if (!map) return;
|
|
108
|
+
// The map may be magnified (hover): read the rendered size, not the logical one.
|
|
109
|
+
const rect = map.getBoundingClientRect();
|
|
110
|
+
const mx = ((e.clientX - rect.left) / rect.width) * MINIMAP_W;
|
|
111
|
+
const my = ((e.clientY - rect.top) / rect.height) * MINIMAP_H;
|
|
112
|
+
const current = computeMinimapFrame(nodes.value, viewport.value, containerWidth, containerHeight);
|
|
113
|
+
const vp = viewport.value;
|
|
114
|
+
const vpW = containerWidth / vp.scale;
|
|
115
|
+
const vpH = containerHeight / vp.scale;
|
|
116
|
+
const worldX = mx / current.scale + current.bounds.minX;
|
|
117
|
+
const worldY = my / current.scale + current.bounds.minY;
|
|
118
|
+
onNavigate(-(worldX - vpW / 2) * vp.scale, -(worldY - vpH / 2) * vp.scale);
|
|
246
119
|
},
|
|
247
120
|
[nodes, viewport, containerWidth, containerHeight, onNavigate],
|
|
248
121
|
);
|
|
@@ -250,50 +123,104 @@ export function Minimap({ viewport, nodes, edges, onNavigate, containerWidth, co
|
|
|
250
123
|
const handlePointerDown = useCallback(
|
|
251
124
|
(e: PointerEvent) => {
|
|
252
125
|
e.stopPropagation();
|
|
126
|
+
e.preventDefault();
|
|
253
127
|
isDragging.current = true;
|
|
254
128
|
handleNavigateFromEvent(e);
|
|
255
|
-
|
|
256
129
|
const onPointerMove = (ev: PointerEvent) => {
|
|
257
130
|
if (isDragging.current) handleNavigateFromEvent(ev);
|
|
258
131
|
};
|
|
259
|
-
|
|
260
132
|
const onPointerUp = () => {
|
|
261
133
|
isDragging.current = false;
|
|
262
134
|
document.removeEventListener('pointermove', onPointerMove);
|
|
263
135
|
document.removeEventListener('pointerup', onPointerUp);
|
|
264
136
|
};
|
|
265
|
-
|
|
266
137
|
document.addEventListener('pointermove', onPointerMove);
|
|
267
138
|
document.addEventListener('pointerup', onPointerUp);
|
|
268
139
|
},
|
|
269
140
|
[handleNavigateFromEvent],
|
|
270
141
|
);
|
|
271
142
|
|
|
143
|
+
const selected = selectedNodeIds.value;
|
|
144
|
+
const fence = scopeFence.value;
|
|
145
|
+
const fenceRect = fence
|
|
146
|
+
? fenceRectFromNodes(
|
|
147
|
+
fence.nodeIds.map((id) => nodeMap.get(id)).filter((node): node is CanvasNodeState => node !== undefined),
|
|
148
|
+
fence.padding,
|
|
149
|
+
)
|
|
150
|
+
: null;
|
|
151
|
+
// Every live writer shows on the map — attached sessions, external writers,
|
|
152
|
+
// and fleet workers alike (amended presence contract): the minimap is where
|
|
153
|
+
// the human looks first to find WHERE an agent is working.
|
|
154
|
+
const presenceDots = agentPresences.value
|
|
155
|
+
.map((presence) => ({
|
|
156
|
+
id: presence.sessionId,
|
|
157
|
+
phase: presence.phase,
|
|
158
|
+
worker: presence.parentAgentId != null,
|
|
159
|
+
external: !presence.attached,
|
|
160
|
+
at: presenceWorldPosition(presence, (id) => nodeMap.get(id)),
|
|
161
|
+
}))
|
|
162
|
+
.filter((dot): dot is typeof dot & { at: { x: number; y: number } } => dot.at !== null);
|
|
163
|
+
|
|
164
|
+
const vpLeft = -v.x / v.scale;
|
|
165
|
+
const vpTop = -v.y / v.scale;
|
|
166
|
+
|
|
272
167
|
return (
|
|
273
168
|
<div
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
border: '1px solid var(--c-line)',
|
|
280
|
-
borderRadius: 'var(--radius-sm)',
|
|
281
|
-
overflow: 'hidden',
|
|
282
|
-
boxShadow: '0 4px 16px var(--c-shadow)',
|
|
283
|
-
}}
|
|
169
|
+
ref={mapRef}
|
|
170
|
+
class="minimap"
|
|
171
|
+
data-testid="minimap"
|
|
172
|
+
title="Click to jump · drag the frame to pan"
|
|
173
|
+
onPointerDown={handlePointerDown}
|
|
284
174
|
>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
175
|
+
{Array.from(nodeMap.values()).map((node) => {
|
|
176
|
+
const isGroup = node.type === 'group';
|
|
177
|
+
return (
|
|
178
|
+
<span
|
|
179
|
+
key={node.id}
|
|
180
|
+
class={`minimap-node${isGroup ? ' is-group' : ''}${selected.has(node.id) ? ' is-selected' : ''}`}
|
|
181
|
+
data-kind={node.type}
|
|
182
|
+
style={{
|
|
183
|
+
left: `${toX(node.position.x)}px`,
|
|
184
|
+
top: `${toY(node.position.y)}px`,
|
|
185
|
+
width: `${Math.max(isGroup ? 6 : 4, node.size.width * scale)}px`,
|
|
186
|
+
height: `${Math.max(isGroup ? 5 : 3, node.size.height * scale)}px`,
|
|
187
|
+
'--kind': KIND_COLOR[node.type],
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
})}
|
|
192
|
+
{fenceRect && (
|
|
193
|
+
<span
|
|
194
|
+
class="minimap-fence"
|
|
195
|
+
style={{
|
|
196
|
+
left: `${toX(fenceRect.x)}px`,
|
|
197
|
+
top: `${toY(fenceRect.y)}px`,
|
|
198
|
+
width: `${fenceRect.width * scale}px`,
|
|
199
|
+
height: `${fenceRect.height * scale}px`,
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
{presenceDots.map((dot) => (
|
|
204
|
+
<span
|
|
205
|
+
key={dot.id}
|
|
206
|
+
class={`minimap-presence phase-${dot.phase}${dot.worker ? ' is-worker' : ''}${dot.external ? ' is-external' : ''}`}
|
|
207
|
+
style={{
|
|
208
|
+
left: `${toX(dot.at.x) - 4}px`,
|
|
209
|
+
top: `${toY(dot.at.y) - 4}px`,
|
|
210
|
+
'--identity-color': `hsl(${agentIdentityHue(dot.id)} 65% 62%)`,
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
))}
|
|
214
|
+
<span
|
|
215
|
+
class="minimap-frame"
|
|
289
216
|
style={{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
217
|
+
left: `${toX(vpLeft)}px`,
|
|
218
|
+
top: `${toY(vpTop)}px`,
|
|
219
|
+
width: `${(containerWidth / v.scale) * scale}px`,
|
|
220
|
+
height: `${(containerHeight / v.scale) * scale}px`,
|
|
294
221
|
}}
|
|
295
|
-
onPointerDown={handlePointerDown}
|
|
296
222
|
/>
|
|
223
|
+
<span class="minimap-zoom">{Math.round(v.scale * 100)}%</span>
|
|
297
224
|
</div>
|
|
298
225
|
);
|
|
299
226
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { fenceRectFromNodes } from '../../shared/scope-fence.js';
|
|
2
|
+
import { nodes, viewport } from '../state/canvas-store';
|
|
3
|
+
import { sessionActive } from '../state/presence-store';
|
|
4
|
+
import { scopeFence } from '../state/session-store';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scope fence (rail-chrome-v2 phase 4, design item 4): the region the human
|
|
8
|
+
* granted the attached agent. Drawn in the world layer from the same geometry
|
|
9
|
+
* the server enforces (fenced nodes' bounding box + padding), so what the
|
|
10
|
+
* human sees is exactly what the agent is held to. Mounts only while a
|
|
11
|
+
* session is attached and a fence is set.
|
|
12
|
+
*/
|
|
13
|
+
export function ScopeFenceLayer() {
|
|
14
|
+
if (!sessionActive.value) return null;
|
|
15
|
+
const fence = scopeFence.value;
|
|
16
|
+
if (!fence) return null;
|
|
17
|
+
const nodeMap = nodes.value;
|
|
18
|
+
const fenced = fence.nodeIds.map((id) => nodeMap.get(id)).filter((node) => node !== undefined);
|
|
19
|
+
const rect = fenceRectFromNodes(fenced, fence.padding);
|
|
20
|
+
if (!rect) return null;
|
|
21
|
+
const counterScale = 1 / Math.max(0.05, viewport.value.scale);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
class="scope-fence"
|
|
26
|
+
data-fenced-count={fenced.length}
|
|
27
|
+
style={{ transform: `translate(${rect.x}px, ${rect.y}px)`, width: `${rect.width}px`, height: `${rect.height}px` }}
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
>
|
|
30
|
+
<span class="scope-fence-pill scope-fence-pill-top" style={{ transform: `scale(${counterScale})` }}>
|
|
31
|
+
Agent scope · {fenced.length} node{fenced.length === 1 ? '' : 's'}
|
|
32
|
+
</span>
|
|
33
|
+
<span class="scope-fence-pill scope-fence-pill-bottom" style={{ transform: `scale(${counterScale})` }}>
|
|
34
|
+
writes outside are blocked · reads allowed
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import { useCallback } from 'preact/hooks';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { IconArrange } from '../icons';
|
|
3
|
+
import { attentionHistoryOpen } from '../state/attention-store';
|
|
4
|
+
import {
|
|
5
|
+
addContextPins,
|
|
6
|
+
alignSelection,
|
|
7
|
+
arrangeSelection,
|
|
8
|
+
clearSelection,
|
|
9
|
+
contextPinnedNodeIds,
|
|
10
|
+
distributeSelection,
|
|
11
|
+
removeNode,
|
|
12
|
+
selectedNodeIds,
|
|
13
|
+
} from '../state/canvas-store';
|
|
14
|
+
import { createEdgeFromClient, createGroupFromClient, removeNodeFromClient } from '../state/intent-bridge';
|
|
15
|
+
import { sessionActive } from '../state/presence-store';
|
|
16
|
+
import { BarHint } from './BarHint';
|
|
4
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Floating bottom-center bar for a multi-selection (rail-chrome-v2 phase 7,
|
|
20
|
+
* design item 13): count, align left/top, distribute, auto-arrange, Group (G),
|
|
21
|
+
* Connect, Pin to agent context, delete. Restyle over the existing handlers —
|
|
22
|
+
* the geometry actions live in the store and persist like a drag does.
|
|
23
|
+
*/
|
|
5
24
|
export function SelectionBar() {
|
|
6
25
|
const count = selectedNodeIds.value.size;
|
|
7
26
|
|
|
@@ -29,34 +48,167 @@ export function SelectionBar() {
|
|
|
29
48
|
clearSelection();
|
|
30
49
|
}, []);
|
|
31
50
|
|
|
51
|
+
const handleDelete = useCallback(() => {
|
|
52
|
+
const ids = Array.from(selectedNodeIds.value);
|
|
53
|
+
for (const id of ids) {
|
|
54
|
+
removeNode(id);
|
|
55
|
+
void removeNodeFromClient(id);
|
|
56
|
+
}
|
|
57
|
+
clearSelection();
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
32
60
|
if (count === 0) return null;
|
|
61
|
+
const many = count >= 2;
|
|
62
|
+
// The quiet board's pin bar shares the bottom-center slot (same rule as
|
|
63
|
+
// `ContextPinBar`'s own visibility) — sit above it rather than under it.
|
|
64
|
+
const abovePinBar = !sessionActive.value && contextPinnedNodeIds.value.size > 0 && !attentionHistoryOpen.value;
|
|
33
65
|
|
|
34
66
|
return (
|
|
35
|
-
<div class=
|
|
67
|
+
<div class={`selection-bar${abovePinBar ? ' is-above-pin-bar' : ''}`} role="toolbar" aria-label="Selection">
|
|
36
68
|
<span class="selection-bar-count">
|
|
37
|
-
{
|
|
69
|
+
{count} node{count !== 1 ? 's' : ''} selected
|
|
38
70
|
</span>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
{many && (
|
|
72
|
+
<>
|
|
73
|
+
<span class="selection-bar-sep" />
|
|
74
|
+
<BarHint label="Align left" side="up">
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
class="selection-bar-icon"
|
|
78
|
+
aria-label="Align left"
|
|
79
|
+
onClick={() => alignSelection('left')}
|
|
80
|
+
>
|
|
81
|
+
<svg
|
|
82
|
+
width="14"
|
|
83
|
+
height="14"
|
|
84
|
+
viewBox="0 0 16 16"
|
|
85
|
+
fill="none"
|
|
86
|
+
stroke="currentColor"
|
|
87
|
+
stroke-width="1.5"
|
|
88
|
+
stroke-linecap="round"
|
|
89
|
+
aria-hidden="true"
|
|
90
|
+
>
|
|
91
|
+
<line x1="2.5" y1="2" x2="2.5" y2="14" />
|
|
92
|
+
<rect x="5" y="3.5" width="8" height="3.5" rx="1" />
|
|
93
|
+
<rect x="5" y="9" width="5" height="3.5" rx="1" />
|
|
94
|
+
</svg>
|
|
95
|
+
</button>
|
|
96
|
+
</BarHint>
|
|
97
|
+
<BarHint label="Align top" side="up">
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="selection-bar-icon"
|
|
101
|
+
aria-label="Align top"
|
|
102
|
+
onClick={() => alignSelection('top')}
|
|
103
|
+
>
|
|
104
|
+
<svg
|
|
105
|
+
width="14"
|
|
106
|
+
height="14"
|
|
107
|
+
viewBox="0 0 16 16"
|
|
108
|
+
fill="none"
|
|
109
|
+
stroke="currentColor"
|
|
110
|
+
stroke-width="1.5"
|
|
111
|
+
stroke-linecap="round"
|
|
112
|
+
aria-hidden="true"
|
|
113
|
+
>
|
|
114
|
+
<line x1="2" y1="2.5" x2="14" y2="2.5" />
|
|
115
|
+
<rect x="3.5" y="5" width="3.5" height="8" rx="1" />
|
|
116
|
+
<rect x="9" y="5" width="3.5" height="5" rx="1" />
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
119
|
+
</BarHint>
|
|
120
|
+
<BarHint
|
|
121
|
+
label="Distribute horizontally"
|
|
122
|
+
body="Evens the gaps. A selection that cannot fit flows into a clean row instead of overlapping."
|
|
123
|
+
side="up"
|
|
124
|
+
>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
class="selection-bar-icon"
|
|
128
|
+
aria-label="Distribute"
|
|
129
|
+
disabled={count < 3}
|
|
130
|
+
onClick={() => distributeSelection()}
|
|
131
|
+
>
|
|
132
|
+
<svg
|
|
133
|
+
width="14"
|
|
134
|
+
height="14"
|
|
135
|
+
viewBox="0 0 16 16"
|
|
136
|
+
fill="none"
|
|
137
|
+
stroke="currentColor"
|
|
138
|
+
stroke-width="1.5"
|
|
139
|
+
aria-hidden="true"
|
|
140
|
+
>
|
|
141
|
+
<rect x="1.5" y="5" width="3.5" height="6" rx="1" />
|
|
142
|
+
<rect x="6.25" y="5" width="3.5" height="6" rx="1" />
|
|
143
|
+
<rect x="11" y="5" width="3.5" height="6" rx="1" />
|
|
144
|
+
</svg>
|
|
145
|
+
</button>
|
|
146
|
+
</BarHint>
|
|
147
|
+
<BarHint label="Auto-arrange selection" side="up">
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
class="selection-bar-icon"
|
|
151
|
+
aria-label="Auto-arrange"
|
|
152
|
+
onClick={() => arrangeSelection()}
|
|
153
|
+
>
|
|
154
|
+
<IconArrange size={14} />
|
|
155
|
+
</button>
|
|
156
|
+
</BarHint>
|
|
157
|
+
<span class="selection-bar-sep" />
|
|
158
|
+
<BarHint label="Group the selection" shortcut="G" side="up">
|
|
159
|
+
<button type="button" class="selection-bar-action" onClick={handleGroup}>
|
|
160
|
+
<svg
|
|
161
|
+
width="13"
|
|
162
|
+
height="13"
|
|
163
|
+
viewBox="0 0 16 16"
|
|
164
|
+
fill="none"
|
|
165
|
+
stroke="currentColor"
|
|
166
|
+
stroke-width="1.5"
|
|
167
|
+
stroke-dasharray="3 2.5"
|
|
168
|
+
aria-hidden="true"
|
|
169
|
+
>
|
|
170
|
+
<rect x="1.5" y="1.5" width="13" height="13" rx="2" />
|
|
171
|
+
</svg>
|
|
172
|
+
Group
|
|
173
|
+
</button>
|
|
174
|
+
</BarHint>
|
|
175
|
+
<BarHint label="Connect every pair" side="up">
|
|
176
|
+
<button type="button" class="selection-bar-action" onClick={handleConnect}>
|
|
177
|
+
Connect
|
|
178
|
+
</button>
|
|
179
|
+
</BarHint>
|
|
180
|
+
</>
|
|
46
181
|
)}
|
|
47
|
-
|
|
48
|
-
<button type="button" class="selection-bar-
|
|
49
|
-
|
|
182
|
+
<BarHint label="Pin to agent context" side="up">
|
|
183
|
+
<button type="button" class="selection-bar-action is-pin" onClick={handlePinContext}>
|
|
184
|
+
<span aria-hidden="true">✦</span>
|
|
185
|
+
Pin as context
|
|
50
186
|
</button>
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
class="selection-bar-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
187
|
+
</BarHint>
|
|
188
|
+
<span class="selection-bar-sep" />
|
|
189
|
+
<BarHint label="Delete selection" side="up">
|
|
190
|
+
<button type="button" class="selection-bar-icon is-danger" aria-label="Delete selection" onClick={handleDelete}>
|
|
191
|
+
<svg
|
|
192
|
+
width="14"
|
|
193
|
+
height="14"
|
|
194
|
+
viewBox="0 0 16 16"
|
|
195
|
+
fill="none"
|
|
196
|
+
stroke="currentColor"
|
|
197
|
+
stroke-width="1.5"
|
|
198
|
+
stroke-linecap="round"
|
|
199
|
+
aria-hidden="true"
|
|
200
|
+
>
|
|
201
|
+
<path d="M2.5 4 H13.5" />
|
|
202
|
+
<path d="M5.5 4 V2.5 H10.5 V4" />
|
|
203
|
+
<path d="M4 4 L4.8 13.5 H11.2 L12 4" />
|
|
204
|
+
</svg>
|
|
205
|
+
</button>
|
|
206
|
+
</BarHint>
|
|
207
|
+
<BarHint label="Clear selection" shortcut="Esc" side="up">
|
|
208
|
+
<button type="button" class="selection-bar-icon" onClick={clearSelection} aria-label="Clear selection">
|
|
209
|
+
×
|
|
210
|
+
</button>
|
|
211
|
+
</BarHint>
|
|
60
212
|
</div>
|
|
61
213
|
);
|
|
62
214
|
}
|