pmx-canvas 0.4.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/extension.mjs +154 -5
- package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/CHANGELOG.md +139 -0
- package/Readme.md +72 -23
- package/dist/canvas/global.css +3069 -796
- package/dist/canvas/index.js +104 -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 +25 -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/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 +1 -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 +6 -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 +1 -1
- package/skills/pmx-canvas/SKILL.md +103 -16
- 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 +252 -0
- package/src/cli/index.ts +1 -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 +41 -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/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 +273 -84
- 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 +3069 -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 +17 -8
- 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 +107 -4
- 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 +65 -16
- package/src/server/operations/ops/query.ts +1 -0
- package/src/server/operations/ops/snapshots.ts +16 -0
- 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,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import { canvasArea } from './canvas-area';
|
|
2
3
|
import {
|
|
3
4
|
autoArrange,
|
|
4
5
|
canvasTheme,
|
|
@@ -8,12 +9,17 @@ import {
|
|
|
8
9
|
nodes,
|
|
9
10
|
searchHighlightIds,
|
|
10
11
|
} from '../state/canvas-store';
|
|
11
|
-
import {
|
|
12
|
+
import { saveCanvasTheme } from '../state/intent-bridge';
|
|
13
|
+
import { createNodeInView } from './create-in-view';
|
|
14
|
+
import { sessionActive } from '../state/presence-store';
|
|
15
|
+
import { startSession } from '../state/session-store';
|
|
12
16
|
import { TYPE_LABELS, type CanvasNodeState } from '../types';
|
|
13
17
|
import { invalidateTokenCache } from '../theme/tokens';
|
|
14
18
|
import { clearThemeOverride } from '../state/theme-override';
|
|
19
|
+
import { getNodeIcon, IconArrange, IconFitAll, IconMinimap, IconMoon, IconNodeMarkdown, IconSteer } from '../icons';
|
|
15
20
|
|
|
16
|
-
import {
|
|
21
|
+
import { modChord } from '../utils/platform';
|
|
22
|
+
import { useFocusTrap } from './use-focus-trap';
|
|
17
23
|
|
|
18
24
|
// ── Types ───────────────────────────────────────────────────
|
|
19
25
|
interface PaletteItem {
|
|
@@ -21,8 +27,10 @@ interface PaletteItem {
|
|
|
21
27
|
kind: 'node' | 'action';
|
|
22
28
|
label: string;
|
|
23
29
|
description?: string;
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
/** Keyboard shortcut shown as a kbd on action rows. */
|
|
31
|
+
shortcut?: string;
|
|
32
|
+
icon: (p: { size?: number; class?: string }) => preact.JSX.Element;
|
|
33
|
+
iconTone?: 'accent' | 'purple' | 'muted';
|
|
26
34
|
nodeType?: CanvasNodeState['type'];
|
|
27
35
|
action: () => void;
|
|
28
36
|
}
|
|
@@ -100,6 +108,8 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
100
108
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
101
109
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
102
110
|
const listRef = useRef<HTMLDivElement>(null);
|
|
111
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
112
|
+
useFocusTrap(dialogRef, true, { initial: inputRef });
|
|
103
113
|
|
|
104
114
|
// Autofocus
|
|
105
115
|
useEffect(() => {
|
|
@@ -114,12 +124,20 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
114
124
|
for (const node of nodes.value.values()) {
|
|
115
125
|
const title = (node.data.title as string) || TYPE_LABELS[node.type];
|
|
116
126
|
const content = (node.data.content as string) || (node.data.path as string) || '';
|
|
127
|
+
const childCount = node.type === 'group' && Array.isArray(node.data.children) ? node.data.children.length : 0;
|
|
117
128
|
items.push({
|
|
118
129
|
id: `node:${node.id}`,
|
|
119
130
|
kind: 'node',
|
|
120
131
|
label: title,
|
|
121
|
-
description:
|
|
122
|
-
|
|
132
|
+
description:
|
|
133
|
+
node.type === 'group'
|
|
134
|
+
? `group · ${childCount} node${childCount === 1 ? '' : 's'}`
|
|
135
|
+
: content.length > 60
|
|
136
|
+
? `${TYPE_LABELS[node.type].toLowerCase()} · ${content.slice(0, 60)}…`
|
|
137
|
+
: content
|
|
138
|
+
? `${TYPE_LABELS[node.type].toLowerCase()} · ${content}`
|
|
139
|
+
: TYPE_LABELS[node.type].toLowerCase(),
|
|
140
|
+
icon: getNodeIcon(node.type),
|
|
123
141
|
nodeType: node.type,
|
|
124
142
|
action: () => {
|
|
125
143
|
focusNode(node.id);
|
|
@@ -129,26 +147,50 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
// Action items
|
|
132
|
-
const actions: Array<{
|
|
150
|
+
const actions: Array<{
|
|
151
|
+
label: string;
|
|
152
|
+
shortcut?: string;
|
|
153
|
+
icon: PaletteItem['icon'];
|
|
154
|
+
iconTone?: PaletteItem['iconTone'];
|
|
155
|
+
action: () => void;
|
|
156
|
+
}> = [
|
|
133
157
|
{
|
|
134
|
-
label: 'New
|
|
135
|
-
|
|
158
|
+
label: 'New markdown note',
|
|
159
|
+
shortcut: 'M',
|
|
160
|
+
icon: IconNodeMarkdown,
|
|
161
|
+
iconTone: 'accent',
|
|
136
162
|
action: () => {
|
|
137
|
-
|
|
163
|
+
void createNodeInView({ type: 'markdown', title: 'New note', width: 520, height: 360 });
|
|
138
164
|
onClose();
|
|
139
165
|
},
|
|
140
166
|
},
|
|
167
|
+
...(sessionActive.value
|
|
168
|
+
? []
|
|
169
|
+
: [
|
|
170
|
+
{
|
|
171
|
+
label: 'Start agent session',
|
|
172
|
+
icon: IconSteer,
|
|
173
|
+
iconTone: 'purple' as const,
|
|
174
|
+
action: () => {
|
|
175
|
+
void startSession();
|
|
176
|
+
onClose();
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
]),
|
|
141
180
|
{
|
|
142
|
-
label: 'Fit
|
|
143
|
-
|
|
181
|
+
label: 'Fit view',
|
|
182
|
+
shortcut: 'F',
|
|
183
|
+
icon: IconFitAll,
|
|
184
|
+
iconTone: 'muted',
|
|
144
185
|
action: () => {
|
|
145
|
-
fitAll(
|
|
186
|
+
fitAll(canvasArea().width, canvasArea().height);
|
|
146
187
|
onClose();
|
|
147
188
|
},
|
|
148
189
|
},
|
|
149
190
|
{
|
|
150
191
|
label: 'Auto-arrange (grid)',
|
|
151
|
-
|
|
192
|
+
icon: IconArrange,
|
|
193
|
+
iconTone: 'muted',
|
|
152
194
|
action: () => {
|
|
153
195
|
autoArrange();
|
|
154
196
|
onClose();
|
|
@@ -156,7 +198,8 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
156
198
|
},
|
|
157
199
|
{
|
|
158
200
|
label: 'Auto-arrange (graph-aware)',
|
|
159
|
-
|
|
201
|
+
icon: IconArrange,
|
|
202
|
+
iconTone: 'muted',
|
|
160
203
|
action: () => {
|
|
161
204
|
forceDirectedArrange();
|
|
162
205
|
onClose();
|
|
@@ -164,7 +207,8 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
164
207
|
},
|
|
165
208
|
{
|
|
166
209
|
label: 'Toggle minimap',
|
|
167
|
-
|
|
210
|
+
icon: IconMinimap,
|
|
211
|
+
iconTone: 'muted',
|
|
168
212
|
action: () => {
|
|
169
213
|
onToggleMinimap();
|
|
170
214
|
onClose();
|
|
@@ -172,7 +216,8 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
172
216
|
},
|
|
173
217
|
{
|
|
174
218
|
label: 'Toggle theme (dark/light)',
|
|
175
|
-
|
|
219
|
+
icon: IconMoon,
|
|
220
|
+
iconTone: 'muted',
|
|
176
221
|
action: () => {
|
|
177
222
|
const next = canvasTheme.value === 'dark' ? 'light' : 'dark';
|
|
178
223
|
// An explicit pick ends any ?theme= session override, same as the
|
|
@@ -192,8 +237,9 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
192
237
|
id: `action:${a.label}`,
|
|
193
238
|
kind: 'action',
|
|
194
239
|
label: a.label,
|
|
195
|
-
|
|
196
|
-
|
|
240
|
+
...(a.shortcut ? { shortcut: a.shortcut } : {}),
|
|
241
|
+
icon: a.icon,
|
|
242
|
+
...(a.iconTone ? { iconTone: a.iconTone } : {}),
|
|
197
243
|
action: a.action,
|
|
198
244
|
});
|
|
199
245
|
}
|
|
@@ -229,6 +275,9 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
229
275
|
// Sort by score descending
|
|
230
276
|
filtered.sort((a, b) => b.score - a.score);
|
|
231
277
|
}
|
|
278
|
+
// Grouped render order (design item 7): Actions first, then Jump to.
|
|
279
|
+
filtered = [...filtered.filter((item) => item.kind === 'action'), ...filtered.filter((item) => item.kind === 'node')];
|
|
280
|
+
const firstNodeIndex = filtered.findIndex((item) => item.kind === 'node');
|
|
232
281
|
|
|
233
282
|
// ── Sync spatial search highlights to canvas ──────────────
|
|
234
283
|
useEffect(() => {
|
|
@@ -292,48 +341,74 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
292
341
|
|
|
293
342
|
return (
|
|
294
343
|
<div class="command-palette-backdrop" onMouseDown={onClose}>
|
|
295
|
-
<div
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
344
|
+
<div
|
|
345
|
+
ref={dialogRef}
|
|
346
|
+
class="command-palette"
|
|
347
|
+
role="dialog"
|
|
348
|
+
aria-modal="true"
|
|
349
|
+
aria-label="Search and commands"
|
|
350
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
351
|
+
>
|
|
352
|
+
<div class="command-palette-search">
|
|
353
|
+
<svg
|
|
354
|
+
width="15"
|
|
355
|
+
height="15"
|
|
356
|
+
viewBox="0 0 16 16"
|
|
357
|
+
fill="none"
|
|
358
|
+
stroke="currentColor"
|
|
359
|
+
stroke-width="1.5"
|
|
360
|
+
stroke-linecap="round"
|
|
361
|
+
aria-hidden="true"
|
|
362
|
+
>
|
|
363
|
+
<circle cx="7" cy="7" r="4.5" />
|
|
364
|
+
<line x1="10.5" y1="10.5" x2="14.5" y2="14.5" />
|
|
365
|
+
</svg>
|
|
366
|
+
<input
|
|
367
|
+
ref={inputRef}
|
|
368
|
+
type="text"
|
|
369
|
+
class="command-palette-input"
|
|
370
|
+
value={query}
|
|
371
|
+
onInput={(e) => setQuery((e.target as HTMLInputElement).value)}
|
|
372
|
+
onKeyDown={handleKeyDown}
|
|
373
|
+
placeholder="Search nodes, run actions, jump to…"
|
|
374
|
+
aria-label="Search nodes, run actions, jump to"
|
|
375
|
+
/>
|
|
376
|
+
<kbd class="command-palette-esc">esc</kbd>
|
|
319
377
|
</div>
|
|
320
378
|
<div class="command-palette-results" ref={listRef}>
|
|
321
379
|
{filtered.length === 0 && <div class="command-palette-empty">No matching nodes or actions</div>}
|
|
322
|
-
{filtered.map((item, i) =>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
class=
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
380
|
+
{filtered.map((item, i) => {
|
|
381
|
+
const Icon = item.icon;
|
|
382
|
+
const heading = i === 0 && item.kind === 'action' ? 'Actions' : i === firstNodeIndex ? 'Jump to' : null;
|
|
383
|
+
return (
|
|
384
|
+
<div key={item.id} class="command-palette-row">
|
|
385
|
+
{heading && <div class="command-palette-group">{heading}</div>}
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
class={`command-palette-item${i === clampedIndex ? ' selected' : ''}`}
|
|
389
|
+
onMouseEnter={() => setSelectedIndex(i)}
|
|
390
|
+
onClick={() => item.action()}
|
|
391
|
+
>
|
|
392
|
+
<span class={`command-palette-icon tone-${item.iconTone ?? 'kind'}`} aria-hidden="true">
|
|
393
|
+
<Icon size={14} />
|
|
394
|
+
</span>
|
|
395
|
+
<span class="command-palette-label">
|
|
396
|
+
{item.indices.length > 0 ? highlightMatch(item.label, item.indices) : item.label}
|
|
397
|
+
{item.description && <span class="command-palette-desc"> — {item.description}</span>}
|
|
398
|
+
</span>
|
|
399
|
+
{item.shortcut && <kbd class="command-palette-kbd">{item.shortcut}</kbd>}
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
);
|
|
403
|
+
})}
|
|
404
|
+
</div>
|
|
405
|
+
<div class="command-palette-hint">
|
|
406
|
+
<span>↑↓ navigate</span>
|
|
407
|
+
<span>↵ open</span>
|
|
408
|
+
<span>esc close</span>
|
|
409
|
+
<span>type: filter</span>
|
|
410
|
+
<span class="command-palette-hint-spacer" />
|
|
411
|
+
<span>{modChord('K')}</span>
|
|
337
412
|
</div>
|
|
338
413
|
</div>
|
|
339
414
|
</div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { computed } from '@preact/signals';
|
|
2
|
+
import {
|
|
3
|
+
connectionStatus,
|
|
4
|
+
hasInitialServerLayout,
|
|
5
|
+
reconnectAttempt,
|
|
6
|
+
reconnectDelay,
|
|
7
|
+
workbenchConnectionEpoch,
|
|
8
|
+
} from '../state/canvas-store';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Degraded connection states (rail-chrome-v2 phase 7, design item 14), mapped
|
|
12
|
+
* onto the transport the bridge already runs:
|
|
13
|
+
* - reconnecting: the stream (SSE or the polling fallback) is down and the
|
|
14
|
+
* bridge is backing off. Edits still go over HTTP; the board may be stale.
|
|
15
|
+
* - resyncing: a transport came back after a drop and the full `connected`
|
|
16
|
+
* snapshot is being applied — the seq cursor was dropped, the board reloads
|
|
17
|
+
* from the server's snapshot without touching local edits.
|
|
18
|
+
* Neither shows on first boot; the top-bar dot carries the same state.
|
|
19
|
+
*/
|
|
20
|
+
export type DegradedState = 'reconnecting' | 'resyncing' | null;
|
|
21
|
+
|
|
22
|
+
export const degradedState = computed<DegradedState>(() => {
|
|
23
|
+
if (connectionStatus.value === 'disconnected') return 'reconnecting';
|
|
24
|
+
if (connectionStatus.value === 'connected' && !hasInitialServerLayout.value && workbenchConnectionEpoch.value > 1) {
|
|
25
|
+
return 'resyncing';
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function ConnectionBanner() {
|
|
31
|
+
const state = degradedState.value;
|
|
32
|
+
if (!state) return null;
|
|
33
|
+
if (state === 'reconnecting') {
|
|
34
|
+
const attempt = reconnectAttempt.value;
|
|
35
|
+
const delay = reconnectDelay.value;
|
|
36
|
+
return (
|
|
37
|
+
<div class="connection-banner is-reconnecting" role="status" data-testid="connection-banner">
|
|
38
|
+
<span class="connection-banner-glyph" aria-hidden="true">
|
|
39
|
+
↻
|
|
40
|
+
</span>
|
|
41
|
+
<span class="connection-banner-text">
|
|
42
|
+
<b>Reconnecting…</b> live updates are paused; edits still save, the board may be stale until the stream
|
|
43
|
+
returns.
|
|
44
|
+
</span>
|
|
45
|
+
<span class="connection-banner-meta">
|
|
46
|
+
{attempt > 0 ? `retry ${attempt}` : 'retrying'}
|
|
47
|
+
{delay > 0 ? ` · ${Math.max(1, Math.round(delay / 1000))}s` : ''}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
<div class="connection-banner is-resyncing" role="status" data-testid="connection-banner">
|
|
54
|
+
<span class="connection-banner-glyph" aria-hidden="true">
|
|
55
|
+
⟳
|
|
56
|
+
</span>
|
|
57
|
+
<span class="connection-banner-text">
|
|
58
|
+
<b>Resyncing from snapshot</b> — the event cursor went stale; the board reloads without losing local edits.
|
|
59
|
+
</span>
|
|
60
|
+
<span class="connection-banner-progress" aria-hidden="true">
|
|
61
|
+
<span class="connection-banner-progress-fill" />
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -2,7 +2,6 @@ import type { ComponentChildren } from 'preact';
|
|
|
2
2
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
|
3
3
|
import {
|
|
4
4
|
contextPinnedNodeIds,
|
|
5
|
-
dockNode,
|
|
6
5
|
edges,
|
|
7
6
|
expandNode,
|
|
8
7
|
focusNode,
|
|
@@ -12,7 +11,6 @@ import {
|
|
|
12
11
|
removeNode,
|
|
13
12
|
toggleCollapsed,
|
|
14
13
|
toggleContextPin,
|
|
15
|
-
undockNode,
|
|
16
14
|
updateNode,
|
|
17
15
|
} from '../state/canvas-store';
|
|
18
16
|
import {
|
|
@@ -20,11 +18,15 @@ import {
|
|
|
20
18
|
createGroupFromClient,
|
|
21
19
|
createNodeFromClient,
|
|
22
20
|
refreshWebpageNodeFromClient,
|
|
21
|
+
removeEdgeFromClient,
|
|
23
22
|
removeNodeFromClient,
|
|
24
23
|
sendIntent,
|
|
24
|
+
setGroupChildrenFromClient,
|
|
25
25
|
ungroupFromClient,
|
|
26
|
+
updateEdgeFromClient,
|
|
26
27
|
updateNodeFromClient,
|
|
27
28
|
} from '../state/intent-bridge';
|
|
29
|
+
import { askText } from './TextPrompt';
|
|
28
30
|
import { EXPANDABLE_TYPES } from '../types';
|
|
29
31
|
import type { CanvasNodeState } from '../types';
|
|
30
32
|
|
|
@@ -41,6 +43,12 @@ export type MenuState =
|
|
|
41
43
|
y: number;
|
|
42
44
|
canvasX: number;
|
|
43
45
|
canvasY: number;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
kind: 'edge';
|
|
49
|
+
x: number;
|
|
50
|
+
y: number;
|
|
51
|
+
edgeId: string;
|
|
44
52
|
};
|
|
45
53
|
|
|
46
54
|
export function useContextMenu() {
|
|
@@ -58,6 +66,12 @@ export function useContextMenu() {
|
|
|
58
66
|
setMenu({ kind: 'canvas', x: e.clientX, y: e.clientY, canvasX, canvasY });
|
|
59
67
|
}, []);
|
|
60
68
|
|
|
69
|
+
const openEdgeMenu = useCallback((e: MouseEvent, edgeId: string) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
setMenu({ kind: 'edge', x: e.clientX, y: e.clientY, edgeId });
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
61
75
|
const closeMenu = useCallback(() => setMenu(null), []);
|
|
62
76
|
|
|
63
77
|
useEffect(() => {
|
|
@@ -74,7 +88,7 @@ export function useContextMenu() {
|
|
|
74
88
|
};
|
|
75
89
|
}, [menu]);
|
|
76
90
|
|
|
77
|
-
return { menu, openNodeMenu, openCanvasMenu, closeMenu };
|
|
91
|
+
return { menu, openNodeMenu, openCanvasMenu, openEdgeMenu, closeMenu };
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
interface ContextMenuProps {
|
|
@@ -99,6 +113,44 @@ export function ContextMenu({ menu, onClose }: ContextMenuProps) {
|
|
|
99
113
|
const node = nodes.value.get(menu.nodeId);
|
|
100
114
|
if (!node) return null;
|
|
101
115
|
items = buildNodeMenuItems(node);
|
|
116
|
+
} else if (menu.kind === 'edge') {
|
|
117
|
+
// The full edge lifecycle from one menu: relabel, retype, restyle, delete —
|
|
118
|
+
// a drawn edge used to be frozen the moment it existed.
|
|
119
|
+
const edgeId = menu.edgeId;
|
|
120
|
+
const edge = edges.value.get(edgeId);
|
|
121
|
+
items = [
|
|
122
|
+
{
|
|
123
|
+
label: edge?.label
|
|
124
|
+
? `Edit label “${edge.label.slice(0, 18)}${edge.label.length > 18 ? '…' : ''}”`
|
|
125
|
+
: 'Add label…',
|
|
126
|
+
action: () => {
|
|
127
|
+
void askText('Edge label (empty clears it)', 'label text', {
|
|
128
|
+
initial: edge?.label ?? '',
|
|
129
|
+
allowEmpty: true,
|
|
130
|
+
confirm: 'Save',
|
|
131
|
+
}).then((next) => {
|
|
132
|
+
if (next === null) return;
|
|
133
|
+
void updateEdgeFromClient(edgeId, { label: next });
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{ separator: true },
|
|
138
|
+
...(['flow', 'depends-on', 'relation', 'references'] as const).map((type) => ({
|
|
139
|
+
label: `${edge?.type === type ? '✓' : ' '} ${type}`,
|
|
140
|
+
action: () => void updateEdgeFromClient(edgeId, { type }),
|
|
141
|
+
})),
|
|
142
|
+
{ separator: true },
|
|
143
|
+
...(['solid', 'dashed', 'dotted'] as const).map((style) => ({
|
|
144
|
+
label: `${(edge?.style ?? 'solid') === style ? '✓' : ' '} ${style}`,
|
|
145
|
+
action: () => void updateEdgeFromClient(edgeId, { style }),
|
|
146
|
+
})),
|
|
147
|
+
{ separator: true },
|
|
148
|
+
{
|
|
149
|
+
label: 'Delete edge',
|
|
150
|
+
danger: true,
|
|
151
|
+
action: () => void removeEdgeFromClient(edgeId),
|
|
152
|
+
},
|
|
153
|
+
];
|
|
102
154
|
} else {
|
|
103
155
|
items = buildCanvasMenuItems(menu.canvasX, menu.canvasY);
|
|
104
156
|
}
|
|
@@ -147,7 +199,7 @@ export function ContextMenu({ menu, onClose }: ContextMenuProps) {
|
|
|
147
199
|
<button
|
|
148
200
|
key={itemKey}
|
|
149
201
|
type="button"
|
|
150
|
-
class=
|
|
202
|
+
class={`context-menu-item${item.danger ? ' is-danger' : ''}`}
|
|
151
203
|
onClick={() => {
|
|
152
204
|
item.action?.();
|
|
153
205
|
onClose();
|
|
@@ -167,6 +219,8 @@ interface MenuItem {
|
|
|
167
219
|
shortcut?: string;
|
|
168
220
|
action?: () => void;
|
|
169
221
|
separator?: boolean;
|
|
222
|
+
/** Destructive item — rendered in the danger color. */
|
|
223
|
+
danger?: boolean;
|
|
170
224
|
render?: (onClose: () => void) => ComponentChildren;
|
|
171
225
|
}
|
|
172
226
|
|
|
@@ -294,7 +348,9 @@ async function createPromptedNode(opts: {
|
|
|
294
348
|
titleFallback: string;
|
|
295
349
|
errorMessage: string;
|
|
296
350
|
}): Promise<void> {
|
|
297
|
-
|
|
351
|
+
// In-canvas prompt: `window.prompt` (and `window.alert`) are silent no-ops
|
|
352
|
+
// in embedded browser panes — these menu items "did nothing" there.
|
|
353
|
+
const value = await askText(opts.promptLabel, opts.placeholder ?? '');
|
|
298
354
|
if (!value) return;
|
|
299
355
|
const trimmed = value.trim();
|
|
300
356
|
if (!trimmed) return;
|
|
@@ -311,7 +367,7 @@ async function createPromptedNode(opts: {
|
|
|
311
367
|
});
|
|
312
368
|
|
|
313
369
|
if (!result.ok) {
|
|
314
|
-
|
|
370
|
+
console.error(`[canvas-menu] ${opts.errorMessage}`);
|
|
315
371
|
}
|
|
316
372
|
}
|
|
317
373
|
|
|
@@ -561,35 +617,40 @@ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
|
|
|
561
617
|
if (childIds.length > 0) {
|
|
562
618
|
items.push({
|
|
563
619
|
label: `Ungroup (${childIds.length} node${childIds.length !== 1 ? 's' : ''})`,
|
|
564
|
-
|
|
620
|
+
// Dissolve — the same op an agent's ungroup runs.
|
|
621
|
+
action: () => {
|
|
622
|
+
removeNode(node.id);
|
|
623
|
+
void ungroupFromClient(node.id);
|
|
624
|
+
},
|
|
565
625
|
});
|
|
566
626
|
}
|
|
567
627
|
}
|
|
568
628
|
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
});
|
|
586
|
-
}
|
|
629
|
+
// A member of a frame can leave it alone — the drag-out gesture ("release
|
|
630
|
+
// to remove") exists but is easy to miss; this is the discoverable path.
|
|
631
|
+
const parentGroupId = typeof node.data.parentGroup === 'string' ? node.data.parentGroup : null;
|
|
632
|
+
const parentGroup = parentGroupId ? nodes.value.get(parentGroupId) : undefined;
|
|
633
|
+
if (parentGroup && parentGroup.type === 'group') {
|
|
634
|
+
const parentTitle =
|
|
635
|
+
typeof parentGroup.data.title === 'string' && parentGroup.data.title ? parentGroup.data.title : 'frame';
|
|
636
|
+
const siblings = Array.isArray(parentGroup.data.children)
|
|
637
|
+
? (parentGroup.data.children as string[]).filter((childId) => childId !== node.id)
|
|
638
|
+
: [];
|
|
639
|
+
items.push({
|
|
640
|
+
label: `Remove from “${parentTitle}”`,
|
|
641
|
+
action: () => {
|
|
642
|
+
void setGroupChildrenFromClient(parentGroup.id, siblings);
|
|
643
|
+
},
|
|
644
|
+
});
|
|
587
645
|
}
|
|
588
646
|
|
|
589
|
-
// Report #64: status nodes are removable like any other node type.
|
|
647
|
+
// Report #64: status nodes are removable like any other node type. Named
|
|
648
|
+
// "Delete" — "Close" did not read as destructive, and a frame's delete
|
|
649
|
+
// deserves the honest caveat that its members stay.
|
|
590
650
|
items.push({ separator: true });
|
|
591
651
|
items.push({
|
|
592
|
-
label: '
|
|
652
|
+
label: node.type === 'group' ? 'Delete frame (children stay)' : 'Delete',
|
|
653
|
+
danger: true,
|
|
593
654
|
action: () => {
|
|
594
655
|
removeNode(node.id);
|
|
595
656
|
void removeNodeFromClient(node.id);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { clearContextPins, contextPinnedNodeIds
|
|
1
|
+
import { clearContextPins, contextPinnedNodeIds } from '../state/canvas-store';
|
|
2
2
|
import { attentionHistoryOpen } from '../state/attention-store';
|
|
3
3
|
|
|
4
4
|
export function ContextPinBar() {
|
|
5
5
|
const count = contextPinnedNodeIds.value.size;
|
|
6
|
-
if (count === 0 || attentionHistoryOpen.value
|
|
6
|
+
if (count === 0 || attentionHistoryOpen.value) return null;
|
|
7
7
|
|
|
8
8
|
return (
|
|
9
9
|
<div class="context-pin-bar">
|