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
package/src/client/App.tsx
CHANGED
|
@@ -1,549 +1,66 @@
|
|
|
1
|
-
import type { ComponentChildren } from 'preact';
|
|
2
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
3
2
|
import { AttentionHistory } from './canvas/AttentionHistory';
|
|
4
3
|
import { AttentionToast } from './canvas/AttentionToast';
|
|
4
|
+
import { registerCanvasArea, canvasArea } from './canvas/canvas-area';
|
|
5
5
|
import { CanvasViewport } from './canvas/CanvasViewport';
|
|
6
|
+
import { CommandBar } from './canvas/CommandBar';
|
|
7
|
+
import { ConnectionBanner } from './canvas/ConnectionBanner';
|
|
6
8
|
import { CommandPalette } from './canvas/CommandPalette';
|
|
7
9
|
import { ContextMenu, useContextMenu } from './canvas/ContextMenu';
|
|
8
10
|
import { ContextPinBar } from './canvas/ContextPinBar';
|
|
9
|
-
import {
|
|
11
|
+
import { EmptyState } from './canvas/EmptyState';
|
|
12
|
+
import { createNodeInView } from './canvas/create-in-view';
|
|
13
|
+
import { undoFromKeyboard } from './state/session-store';
|
|
14
|
+
import { ActivityFeed, WritersSheet } from './canvas/ExternalWriters';
|
|
10
15
|
import { ExpandedNodeOverlay } from './canvas/ExpandedNodeOverlay';
|
|
11
16
|
import { Minimap } from './canvas/Minimap';
|
|
12
17
|
import { SelectionBar } from './canvas/SelectionBar';
|
|
18
|
+
import { SessionPanel } from './canvas/SessionPanel';
|
|
19
|
+
import { SessionReceipt } from './canvas/SessionReceipt';
|
|
13
20
|
import { ShortcutOverlay } from './canvas/ShortcutOverlay';
|
|
14
21
|
import { SnapshotPanel } from './canvas/SnapshotPanel';
|
|
22
|
+
import { promptedCreate, ToolRail } from './canvas/ToolRail';
|
|
23
|
+
import { TextPrompt } from './canvas/TextPrompt';
|
|
24
|
+
import { TopBar } from './canvas/TopBar';
|
|
15
25
|
import {
|
|
16
26
|
activeNodeId,
|
|
17
27
|
animateViewport,
|
|
18
|
-
|
|
19
|
-
canvasTheme,
|
|
28
|
+
canvasTool,
|
|
20
29
|
clearSelection,
|
|
21
30
|
collapseExpandedNode,
|
|
22
|
-
connectionStatus,
|
|
23
31
|
contextPinnedNodeIds,
|
|
24
32
|
cycleActiveNode,
|
|
25
33
|
edges,
|
|
26
34
|
expandedNodeId,
|
|
27
35
|
fitAll,
|
|
28
|
-
forceDirectedArrange,
|
|
29
36
|
hasInitialServerLayout,
|
|
30
37
|
nodes,
|
|
31
38
|
pendingExpandedNodeCloseId,
|
|
32
|
-
|
|
39
|
+
removeNode,
|
|
40
|
+
selectedEdgeId,
|
|
33
41
|
selectedNodeIds,
|
|
34
|
-
|
|
35
|
-
setViewport,
|
|
36
|
-
traceEnabled,
|
|
42
|
+
spacePanHeld,
|
|
37
43
|
viewport,
|
|
38
44
|
walkGraph,
|
|
39
45
|
zoomByFactor,
|
|
46
|
+
groupsOfSelection,
|
|
40
47
|
} from './state/canvas-store';
|
|
41
48
|
import { connectSSE } from './state/sse-bridge';
|
|
42
49
|
import { intents } from './state/intent-store';
|
|
43
|
-
import {
|
|
50
|
+
import { sessionActive } from './state/presence-store';
|
|
44
51
|
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
IconPen,
|
|
53
|
-
IconResetView,
|
|
54
|
-
IconSearch,
|
|
55
|
-
IconShortcuts,
|
|
56
|
-
IconSnapshot,
|
|
57
|
-
IconSun,
|
|
58
|
-
IconTextAnnotation,
|
|
59
|
-
IconTrace,
|
|
60
|
-
IconZoomIn,
|
|
61
|
-
IconZoomOut,
|
|
62
|
-
} from './icons';
|
|
63
|
-
import { clearThemeOverride } from './state/theme-override';
|
|
64
|
-
import { invalidateTokenCache } from './theme/tokens';
|
|
65
|
-
import { MOD_KEY } from './utils/platform';
|
|
66
|
-
import {
|
|
67
|
-
CANVAS_THEMES,
|
|
68
|
-
CANVAS_THEME_META,
|
|
69
|
-
type CanvasThemeName,
|
|
70
|
-
canvasThemeScheme,
|
|
71
|
-
normalizeCanvasThemeName,
|
|
72
|
-
} from '../shared/themes.js';
|
|
52
|
+
createGroupFromClient,
|
|
53
|
+
removeEdgeFromClient,
|
|
54
|
+
removeNodeFromClient,
|
|
55
|
+
reportClientViewportSize,
|
|
56
|
+
ungroupFromClient,
|
|
57
|
+
} from './state/intent-bridge';
|
|
58
|
+
import type { AnnotationTool } from './types';
|
|
73
59
|
|
|
74
60
|
function logAppError(action: string, error: unknown): void {
|
|
75
61
|
console.error(`[app] ${action} failed`, error);
|
|
76
62
|
}
|
|
77
63
|
|
|
78
|
-
function sendIntent(type: string, payload: Record<string, unknown> = {}): void {
|
|
79
|
-
fetch(`/api/workbench/intent?_ts=${Date.now()}`, {
|
|
80
|
-
method: 'POST',
|
|
81
|
-
headers: { 'Content-Type': 'application/json' },
|
|
82
|
-
body: JSON.stringify({ type, payload }),
|
|
83
|
-
}).catch((error) => {
|
|
84
|
-
logAppError('sendIntent', error);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
type AnnotationTool = 'pen' | 'eraser' | 'text' | null;
|
|
89
|
-
|
|
90
|
-
function ToolbarHint({
|
|
91
|
-
label,
|
|
92
|
-
detail,
|
|
93
|
-
shortcut,
|
|
94
|
-
align = 'center',
|
|
95
|
-
children,
|
|
96
|
-
}: {
|
|
97
|
-
label: string;
|
|
98
|
-
detail?: string;
|
|
99
|
-
shortcut?: string;
|
|
100
|
-
align?: 'start' | 'center' | 'end';
|
|
101
|
-
children: ComponentChildren;
|
|
102
|
-
}) {
|
|
103
|
-
return (
|
|
104
|
-
<span class={`toolbar-tooltip-anchor toolbar-tooltip-anchor-${align}`}>
|
|
105
|
-
{children}
|
|
106
|
-
<span class="toolbar-tooltip" role="tooltip">
|
|
107
|
-
<span class="toolbar-tooltip-label">{label}</span>
|
|
108
|
-
{(detail || shortcut) && (
|
|
109
|
-
<span class="toolbar-tooltip-meta">
|
|
110
|
-
{detail && <span>{detail}</span>}
|
|
111
|
-
{shortcut && <kbd class="toolbar-tooltip-shortcut">{shortcut}</kbd>}
|
|
112
|
-
</span>
|
|
113
|
-
)}
|
|
114
|
-
</span>
|
|
115
|
-
</span>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function Toolbar({
|
|
120
|
-
minimapVisible,
|
|
121
|
-
onToggleMinimap,
|
|
122
|
-
snapshotOpen,
|
|
123
|
-
onToggleSnapshot,
|
|
124
|
-
snapshotBtnRef,
|
|
125
|
-
onOpenPalette,
|
|
126
|
-
onOpenShortcuts,
|
|
127
|
-
annotationTool,
|
|
128
|
-
onToggleAnnotationMode,
|
|
129
|
-
onToggleAnnotationEraser,
|
|
130
|
-
onToggleTextAnnotation,
|
|
131
|
-
}: {
|
|
132
|
-
minimapVisible: boolean;
|
|
133
|
-
onToggleMinimap: () => void;
|
|
134
|
-
snapshotOpen: boolean;
|
|
135
|
-
onToggleSnapshot: () => void;
|
|
136
|
-
snapshotBtnRef: { current: HTMLButtonElement | null };
|
|
137
|
-
onOpenPalette: () => void;
|
|
138
|
-
onOpenShortcuts: () => void;
|
|
139
|
-
annotationTool: AnnotationTool;
|
|
140
|
-
onToggleAnnotationMode: () => void;
|
|
141
|
-
onToggleAnnotationEraser: () => void;
|
|
142
|
-
onToggleTextAnnotation: () => void;
|
|
143
|
-
}) {
|
|
144
|
-
const status = connectionStatus.value;
|
|
145
|
-
const hasSynced = hasInitialServerLayout.value;
|
|
146
|
-
const v = viewport.value;
|
|
147
|
-
const nodeCount = nodes.value.size;
|
|
148
|
-
const edgeCount = edges.value.size;
|
|
149
|
-
const isTraceOn = traceEnabled.value;
|
|
150
|
-
const traceNodeCount = Array.from(nodes.value.values()).filter((n) => n.type === 'trace').length;
|
|
151
|
-
const statusTitle = status === 'connected' && !hasSynced ? 'syncing' : status;
|
|
152
|
-
const statusLabel = statusTitle.charAt(0).toUpperCase() + statusTitle.slice(1);
|
|
153
|
-
const countsLabel = hasSynced
|
|
154
|
-
? [
|
|
155
|
-
`${nodeCount} node${nodeCount !== 1 ? 's' : ''}`,
|
|
156
|
-
...(edgeCount > 0 ? [`${edgeCount} edge${edgeCount !== 1 ? 's' : ''}`] : []),
|
|
157
|
-
...(traceNodeCount > 0
|
|
158
|
-
? [`${traceNodeCount} trace${traceNodeCount !== 1 ? 's' : ''}`]
|
|
159
|
-
: isTraceOn
|
|
160
|
-
? ['trace armed']
|
|
161
|
-
: []),
|
|
162
|
-
].join(' · ')
|
|
163
|
-
: 'Syncing canvas…';
|
|
164
|
-
|
|
165
|
-
// Popover state: the theme picker (desktop toolbar) and the mobile "More"
|
|
166
|
-
// menu share one open-slot so at most one popover shows at a time.
|
|
167
|
-
const [openMenu, setOpenMenu] = useState<null | 'theme' | 'more'>(null);
|
|
168
|
-
const groupRef = useRef<HTMLDivElement>(null);
|
|
169
|
-
|
|
170
|
-
useEffect(() => {
|
|
171
|
-
if (!openMenu) return;
|
|
172
|
-
const onPointerDown = (e: PointerEvent) => {
|
|
173
|
-
if (groupRef.current && e.target instanceof Node && !groupRef.current.contains(e.target)) {
|
|
174
|
-
setOpenMenu(null);
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
const onKeyDown = (e: KeyboardEvent) => {
|
|
178
|
-
if (e.key === 'Escape') setOpenMenu(null);
|
|
179
|
-
};
|
|
180
|
-
document.addEventListener('pointerdown', onPointerDown);
|
|
181
|
-
document.addEventListener('keydown', onKeyDown);
|
|
182
|
-
return () => {
|
|
183
|
-
document.removeEventListener('pointerdown', onPointerDown);
|
|
184
|
-
document.removeEventListener('keydown', onKeyDown);
|
|
185
|
-
};
|
|
186
|
-
}, [openMenu]);
|
|
187
|
-
|
|
188
|
-
const applyTheme = (next: CanvasThemeName) => {
|
|
189
|
-
// An explicit pick ends any ?theme= session override (host default) and
|
|
190
|
-
// returns this client to the shared server-global theme it just saved.
|
|
191
|
-
clearThemeOverride();
|
|
192
|
-
document.documentElement.setAttribute('data-theme', next);
|
|
193
|
-
invalidateTokenCache();
|
|
194
|
-
canvasTheme.value = next;
|
|
195
|
-
void saveCanvasTheme(next);
|
|
196
|
-
setOpenMenu(null);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const menuAction = (fn: () => void) => () => {
|
|
200
|
-
fn();
|
|
201
|
-
setOpenMenu(null);
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const activeTheme = normalizeCanvasThemeName(canvasTheme.value);
|
|
205
|
-
const themeMenuItems = CANVAS_THEMES.map((name) => (
|
|
206
|
-
<button
|
|
207
|
-
key={name}
|
|
208
|
-
type="button"
|
|
209
|
-
role="menuitemradio"
|
|
210
|
-
aria-checked={activeTheme === name}
|
|
211
|
-
class={`toolbar-menu-item${activeTheme === name ? ' active' : ''}`}
|
|
212
|
-
onClick={() => applyTheme(name)}
|
|
213
|
-
>
|
|
214
|
-
<span class="theme-swatch" style={{ background: CANVAS_THEME_META[name].swatchBg }}>
|
|
215
|
-
<span class="theme-swatch-dot" style={{ background: CANVAS_THEME_META[name].swatchAccent }} />
|
|
216
|
-
</span>
|
|
217
|
-
<span>{CANVAS_THEME_META[name].label}</span>
|
|
218
|
-
{activeTheme === name && (
|
|
219
|
-
<span class="toolbar-menu-check" aria-hidden="true">
|
|
220
|
-
✓
|
|
221
|
-
</span>
|
|
222
|
-
)}
|
|
223
|
-
</button>
|
|
224
|
-
));
|
|
225
|
-
|
|
226
|
-
return (
|
|
227
|
-
<div class="toolbar-group" ref={groupRef}>
|
|
228
|
-
{/* ── Navigation Bar ──────────────────────────────────── */}
|
|
229
|
-
<div class="canvas-toolbar">
|
|
230
|
-
<ToolbarHint label="PMX Canvas" detail="Focus Field · spatial workbench for coding agents" align="start">
|
|
231
|
-
<span class="canvas-brand" aria-label="PMX Canvas">
|
|
232
|
-
<IconLogo size={22} />
|
|
233
|
-
</span>
|
|
234
|
-
</ToolbarHint>
|
|
235
|
-
|
|
236
|
-
<div class="separator" />
|
|
237
|
-
|
|
238
|
-
<ToolbarHint
|
|
239
|
-
label="Canvas status"
|
|
240
|
-
detail={hasSynced ? statusLabel : 'Syncing canvas from server'}
|
|
241
|
-
align="start"
|
|
242
|
-
>
|
|
243
|
-
<span class={`connection-dot ${status}`} aria-label={`Canvas status: ${statusTitle}`} />
|
|
244
|
-
</ToolbarHint>
|
|
245
|
-
<span class="hud-collapsible-text" style={{ fontSize: '11px', color: 'var(--c-muted)' }}>
|
|
246
|
-
{sessionId.value ? sessionId.value.slice(0, 12) : '…'}
|
|
247
|
-
</span>
|
|
248
|
-
|
|
249
|
-
<div class="separator" />
|
|
250
|
-
|
|
251
|
-
<ToolbarHint label="Fit canvas" detail="Frame every node on screen">
|
|
252
|
-
<button type="button" onClick={() => fitAll(window.innerWidth, window.innerHeight)} aria-label="Fit canvas">
|
|
253
|
-
<IconFitAll />
|
|
254
|
-
</button>
|
|
255
|
-
</ToolbarHint>
|
|
256
|
-
<ToolbarHint label="Reset view" shortcut={`${MOD_KEY}+0`}>
|
|
257
|
-
<button type="button" onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)} aria-label="Reset view">
|
|
258
|
-
<IconResetView />
|
|
259
|
-
</button>
|
|
260
|
-
</ToolbarHint>
|
|
261
|
-
<ToolbarHint label="Zoom in" shortcut={`${MOD_KEY}++`}>
|
|
262
|
-
<button type="button" onClick={() => zoomByFactor(1.25)} aria-label="Zoom in">
|
|
263
|
-
<IconZoomIn />
|
|
264
|
-
</button>
|
|
265
|
-
</ToolbarHint>
|
|
266
|
-
<ToolbarHint label="Zoom out" shortcut={`${MOD_KEY}+-`}>
|
|
267
|
-
<button type="button" onClick={() => zoomByFactor(1 / 1.25)} aria-label="Zoom out">
|
|
268
|
-
<IconZoomOut />
|
|
269
|
-
</button>
|
|
270
|
-
</ToolbarHint>
|
|
271
|
-
<span
|
|
272
|
-
class="hud-collapsible-text"
|
|
273
|
-
style={{ fontSize: '10px', color: 'var(--c-dim)', minWidth: '36px', textAlign: 'center' }}
|
|
274
|
-
>
|
|
275
|
-
{Math.round(v.scale * 100)}%
|
|
276
|
-
</span>
|
|
277
|
-
|
|
278
|
-
{/* Secondary controls collapse into the More menu on narrow screens. */}
|
|
279
|
-
<span class="toolbar-collapsible">
|
|
280
|
-
<div class="separator" />
|
|
281
|
-
|
|
282
|
-
<ToolbarHint
|
|
283
|
-
label="Arrange layout"
|
|
284
|
-
detail={edgeCount > 0 ? 'Graph-aware layout for connected nodes' : 'Grid layout for loose nodes'}
|
|
285
|
-
>
|
|
286
|
-
<button
|
|
287
|
-
type="button"
|
|
288
|
-
onClick={() => (edgeCount > 0 ? forceDirectedArrange() : autoArrange())}
|
|
289
|
-
aria-label="Arrange layout"
|
|
290
|
-
>
|
|
291
|
-
<IconArrange />
|
|
292
|
-
</button>
|
|
293
|
-
</ToolbarHint>
|
|
294
|
-
<ToolbarHint
|
|
295
|
-
label={minimapVisible ? 'Hide minimap' : 'Show minimap'}
|
|
296
|
-
detail="Quickly navigate large canvases"
|
|
297
|
-
>
|
|
298
|
-
<button
|
|
299
|
-
type="button"
|
|
300
|
-
onClick={onToggleMinimap}
|
|
301
|
-
aria-label={minimapVisible ? 'Hide minimap' : 'Show minimap'}
|
|
302
|
-
style={{ color: minimapVisible ? 'var(--c-accent)' : undefined }}
|
|
303
|
-
>
|
|
304
|
-
<IconMinimap />
|
|
305
|
-
</button>
|
|
306
|
-
</ToolbarHint>
|
|
307
|
-
{/* Anchor wrapper: the theme menu must open under THIS control, not at
|
|
308
|
-
the toolbar-group's right edge (0.4.2 report Finding T). */}
|
|
309
|
-
<span class="toolbar-menu-anchor">
|
|
310
|
-
<ToolbarHint label="Theme" detail={`Current theme: ${CANVAS_THEME_META[activeTheme].label}`}>
|
|
311
|
-
<button
|
|
312
|
-
type="button"
|
|
313
|
-
onClick={() => setOpenMenu(openMenu === 'theme' ? null : 'theme')}
|
|
314
|
-
aria-label="Choose theme"
|
|
315
|
-
aria-haspopup="menu"
|
|
316
|
-
aria-expanded={openMenu === 'theme'}
|
|
317
|
-
style={{ color: openMenu === 'theme' ? 'var(--c-accent)' : undefined }}
|
|
318
|
-
>
|
|
319
|
-
{canvasThemeScheme(activeTheme) === 'dark' ? <IconSun /> : <IconMoon />}
|
|
320
|
-
</button>
|
|
321
|
-
</ToolbarHint>
|
|
322
|
-
{openMenu === 'theme' && (
|
|
323
|
-
<div class="toolbar-menu" role="menu" aria-label="Theme">
|
|
324
|
-
{themeMenuItems}
|
|
325
|
-
</div>
|
|
326
|
-
)}
|
|
327
|
-
</span>
|
|
328
|
-
<ToolbarHint label="Snapshots" detail="Capture and restore canvas states" align="end">
|
|
329
|
-
<button
|
|
330
|
-
ref={snapshotBtnRef}
|
|
331
|
-
type="button"
|
|
332
|
-
onClick={onToggleSnapshot}
|
|
333
|
-
aria-label="Snapshots"
|
|
334
|
-
style={{ color: snapshotOpen ? 'var(--c-accent)' : undefined }}
|
|
335
|
-
>
|
|
336
|
-
<IconSnapshot />
|
|
337
|
-
</button>
|
|
338
|
-
</ToolbarHint>
|
|
339
|
-
</span>
|
|
340
|
-
|
|
341
|
-
{/* Narrow screens: everything collapsed lives in this single menu. */}
|
|
342
|
-
<span class="toolbar-more toolbar-menu-anchor">
|
|
343
|
-
<ToolbarHint label="More" detail="All canvas actions" align="end">
|
|
344
|
-
<button
|
|
345
|
-
type="button"
|
|
346
|
-
onClick={() => setOpenMenu(openMenu === 'more' ? null : 'more')}
|
|
347
|
-
aria-label="More actions"
|
|
348
|
-
aria-haspopup="menu"
|
|
349
|
-
aria-expanded={openMenu === 'more'}
|
|
350
|
-
style={{ color: openMenu === 'more' ? 'var(--c-accent)' : undefined }}
|
|
351
|
-
>
|
|
352
|
-
<span class="toolbar-more-glyph" aria-hidden="true">
|
|
353
|
-
⋯
|
|
354
|
-
</span>
|
|
355
|
-
</button>
|
|
356
|
-
</ToolbarHint>
|
|
357
|
-
{openMenu === 'more' && (
|
|
358
|
-
<div class="toolbar-menu" role="menu" aria-label="Canvas actions">
|
|
359
|
-
<button
|
|
360
|
-
type="button"
|
|
361
|
-
class="toolbar-menu-item"
|
|
362
|
-
onClick={menuAction(() => (edgeCount > 0 ? forceDirectedArrange() : autoArrange()))}
|
|
363
|
-
>
|
|
364
|
-
<IconArrange />
|
|
365
|
-
<span>Arrange layout</span>
|
|
366
|
-
</button>
|
|
367
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleMinimap)}>
|
|
368
|
-
<IconMinimap />
|
|
369
|
-
<span>{minimapVisible ? 'Hide minimap' : 'Show minimap'}</span>
|
|
370
|
-
</button>
|
|
371
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleSnapshot)}>
|
|
372
|
-
<IconSnapshot />
|
|
373
|
-
<span>Snapshots</span>
|
|
374
|
-
</button>
|
|
375
|
-
<div class="toolbar-menu-separator" />
|
|
376
|
-
<button
|
|
377
|
-
type="button"
|
|
378
|
-
class="toolbar-menu-item"
|
|
379
|
-
onClick={menuAction(() => sendIntent('trace-toggle', { enabled: !isTraceOn }))}
|
|
380
|
-
>
|
|
381
|
-
<IconTrace />
|
|
382
|
-
<span>{isTraceOn ? 'Disable trace' : 'Enable trace'}</span>
|
|
383
|
-
</button>
|
|
384
|
-
{(isTraceOn || traceNodeCount > 0) && (
|
|
385
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(() => sendIntent('trace-clear'))}>
|
|
386
|
-
<IconClearTrace />
|
|
387
|
-
<span>Clear trace</span>
|
|
388
|
-
</button>
|
|
389
|
-
)}
|
|
390
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleAnnotationMode)}>
|
|
391
|
-
<IconPen />
|
|
392
|
-
<span>{annotationTool === 'pen' ? 'Stop annotating' : 'Annotate canvas'}</span>
|
|
393
|
-
</button>
|
|
394
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleAnnotationEraser)}>
|
|
395
|
-
<IconEraser />
|
|
396
|
-
<span>{annotationTool === 'eraser' ? 'Stop erasing' : 'Erase annotations'}</span>
|
|
397
|
-
</button>
|
|
398
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleTextAnnotation)}>
|
|
399
|
-
<IconTextAnnotation />
|
|
400
|
-
<span>{annotationTool === 'text' ? 'Stop text annotations' : 'Text annotations'}</span>
|
|
401
|
-
</button>
|
|
402
|
-
<div class="toolbar-menu-separator" />
|
|
403
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onOpenPalette)}>
|
|
404
|
-
<IconSearch />
|
|
405
|
-
<span>Search nodes and actions</span>
|
|
406
|
-
</button>
|
|
407
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onOpenShortcuts)}>
|
|
408
|
-
<IconShortcuts />
|
|
409
|
-
<span>Keyboard shortcuts</span>
|
|
410
|
-
</button>
|
|
411
|
-
<div class="toolbar-menu-separator" />
|
|
412
|
-
<div class="toolbar-menu-heading">Theme</div>
|
|
413
|
-
{themeMenuItems}
|
|
414
|
-
</div>
|
|
415
|
-
)}
|
|
416
|
-
</span>
|
|
417
|
-
</div>
|
|
418
|
-
|
|
419
|
-
{/* ── Action Bar (hidden on narrow screens — lives in the More menu) ── */}
|
|
420
|
-
<div class="canvas-toolbar toolbar-actions-bar">
|
|
421
|
-
<ToolbarHint
|
|
422
|
-
label={isTraceOn ? 'Disable trace' : 'Enable trace'}
|
|
423
|
-
detail={isTraceOn ? 'Stop collecting new trace nodes' : 'Capture agent execution on the canvas'}
|
|
424
|
-
>
|
|
425
|
-
<button
|
|
426
|
-
type="button"
|
|
427
|
-
onClick={() => sendIntent('trace-toggle', { enabled: !isTraceOn })}
|
|
428
|
-
aria-label={isTraceOn ? 'Disable trace' : 'Enable trace'}
|
|
429
|
-
style={{ color: isTraceOn ? 'var(--c-purple)' : undefined }}
|
|
430
|
-
>
|
|
431
|
-
<IconTrace />
|
|
432
|
-
</button>
|
|
433
|
-
</ToolbarHint>
|
|
434
|
-
{(isTraceOn || traceNodeCount > 0) && (
|
|
435
|
-
<ToolbarHint
|
|
436
|
-
label="Clear trace"
|
|
437
|
-
detail={
|
|
438
|
-
traceNodeCount > 0
|
|
439
|
-
? `Remove ${traceNodeCount} trace node${traceNodeCount === 1 ? '' : 's'}`
|
|
440
|
-
: 'Trace is enabled but still empty'
|
|
441
|
-
}
|
|
442
|
-
>
|
|
443
|
-
<button type="button" onClick={() => sendIntent('trace-clear')} aria-label="Clear trace">
|
|
444
|
-
<IconClearTrace />
|
|
445
|
-
</button>
|
|
446
|
-
</ToolbarHint>
|
|
447
|
-
)}
|
|
448
|
-
|
|
449
|
-
<div class="separator" />
|
|
450
|
-
|
|
451
|
-
<ToolbarHint
|
|
452
|
-
label={annotationTool === 'pen' ? 'Stop annotating' : 'Annotate canvas'}
|
|
453
|
-
detail="Draw directly on the canvas for human-visible markup"
|
|
454
|
-
>
|
|
455
|
-
<button
|
|
456
|
-
type="button"
|
|
457
|
-
onClick={onToggleAnnotationMode}
|
|
458
|
-
aria-label={annotationTool === 'pen' ? 'Stop annotating' : 'Annotate canvas'}
|
|
459
|
-
aria-pressed={annotationTool === 'pen'}
|
|
460
|
-
style={{ color: annotationTool === 'pen' ? 'var(--c-accent)' : undefined }}
|
|
461
|
-
>
|
|
462
|
-
<IconPen />
|
|
463
|
-
</button>
|
|
464
|
-
</ToolbarHint>
|
|
465
|
-
<ToolbarHint
|
|
466
|
-
label={annotationTool === 'eraser' ? 'Stop erasing' : 'Erase annotations'}
|
|
467
|
-
detail="Click a drawn annotation to remove it"
|
|
468
|
-
>
|
|
469
|
-
<button
|
|
470
|
-
type="button"
|
|
471
|
-
onClick={onToggleAnnotationEraser}
|
|
472
|
-
aria-label={annotationTool === 'eraser' ? 'Stop erasing' : 'Erase annotations'}
|
|
473
|
-
aria-pressed={annotationTool === 'eraser'}
|
|
474
|
-
style={{ color: annotationTool === 'eraser' ? 'var(--c-accent)' : undefined }}
|
|
475
|
-
>
|
|
476
|
-
<IconEraser />
|
|
477
|
-
</button>
|
|
478
|
-
</ToolbarHint>
|
|
479
|
-
<ToolbarHint
|
|
480
|
-
label={annotationTool === 'text' ? 'Stop text annotations' : 'Text annotations'}
|
|
481
|
-
detail="Click anywhere to type an intent note"
|
|
482
|
-
>
|
|
483
|
-
<button
|
|
484
|
-
type="button"
|
|
485
|
-
onClick={onToggleTextAnnotation}
|
|
486
|
-
aria-label={annotationTool === 'text' ? 'Stop text annotations' : 'Text annotations'}
|
|
487
|
-
aria-pressed={annotationTool === 'text'}
|
|
488
|
-
style={{ color: annotationTool === 'text' ? 'var(--c-accent)' : undefined }}
|
|
489
|
-
>
|
|
490
|
-
<IconTextAnnotation />
|
|
491
|
-
</button>
|
|
492
|
-
</ToolbarHint>
|
|
493
|
-
|
|
494
|
-
<div class="separator" />
|
|
495
|
-
|
|
496
|
-
<ToolbarHint label="Search nodes and actions" shortcut={`${MOD_KEY}+K`}>
|
|
497
|
-
<button type="button" onClick={onOpenPalette} aria-label="Search nodes and actions">
|
|
498
|
-
<IconSearch />
|
|
499
|
-
</button>
|
|
500
|
-
</ToolbarHint>
|
|
501
|
-
<ToolbarHint label="Keyboard shortcuts" shortcut="?" align="end">
|
|
502
|
-
<button type="button" onClick={onOpenShortcuts} aria-label="Keyboard shortcuts">
|
|
503
|
-
<IconShortcuts />
|
|
504
|
-
</button>
|
|
505
|
-
</ToolbarHint>
|
|
506
|
-
|
|
507
|
-
<span class="hud-collapsible-text" style={{ fontSize: '10px', color: 'var(--c-dim)' }}>
|
|
508
|
-
{countsLabel}
|
|
509
|
-
</span>
|
|
510
|
-
</div>
|
|
511
|
-
</div>
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function WelcomeCard({ onOpenPalette }: { onOpenPalette: () => void }) {
|
|
516
|
-
return (
|
|
517
|
-
<div class="welcome-card">
|
|
518
|
-
<div class="welcome-icon">◇</div>
|
|
519
|
-
<div class="welcome-title">Shape What The Agent Sees</div>
|
|
520
|
-
<div class="welcome-subtitle">
|
|
521
|
-
Lay out notes, files, and evidence. Bring related nodes together. Pin what matters. The board will reflect the
|
|
522
|
-
active focus.
|
|
523
|
-
</div>
|
|
524
|
-
<div class="welcome-hints">
|
|
525
|
-
<button type="button" class="welcome-hint" onClick={onOpenPalette}>
|
|
526
|
-
<kbd>{MOD_KEY}+K</kbd>
|
|
527
|
-
<span>Create a note</span>
|
|
528
|
-
</button>
|
|
529
|
-
<div class="welcome-hint">
|
|
530
|
-
<kbd>Drop files</kbd>
|
|
531
|
-
<span>Add evidence to the board</span>
|
|
532
|
-
</div>
|
|
533
|
-
<div class="welcome-hint">
|
|
534
|
-
<kbd>{'\u2726'}</kbd>
|
|
535
|
-
<span>Pin important nodes</span>
|
|
536
|
-
</div>
|
|
537
|
-
<div class="welcome-hint">
|
|
538
|
-
<kbd>Move nearby</kbd>
|
|
539
|
-
<span>Shape the focus field</span>
|
|
540
|
-
</div>
|
|
541
|
-
</div>
|
|
542
|
-
<div class="welcome-footer">The canvas is a shared attention surface, not just an editor.</div>
|
|
543
|
-
</div>
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
64
|
export function App() {
|
|
548
65
|
const [minimapVisible, setMinimapVisible] = useState(true);
|
|
549
66
|
const [snapshotOpen, setSnapshotOpen] = useState(false);
|
|
@@ -551,24 +68,17 @@ export function App() {
|
|
|
551
68
|
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
552
69
|
const [annotationTool, setAnnotationTool] = useState<AnnotationTool>(null);
|
|
553
70
|
const snapshotBtnRef = useRef<HTMLButtonElement>(null);
|
|
554
|
-
const { menu, openNodeMenu, openCanvasMenu, closeMenu } = useContextMenu();
|
|
71
|
+
const { menu, openNodeMenu, openCanvasMenu, openEdgeMenu, closeMenu } = useContextMenu();
|
|
555
72
|
const hasInitialLayout = hasInitialServerLayout.value;
|
|
73
|
+
// rail-chrome-v2: the one selector every agent surface mounts on. Exposed
|
|
74
|
+
// as a data attribute so styling and tests can key on it; the quiet board
|
|
75
|
+
// (no attached session) must stay byte-clean of agent chrome.
|
|
76
|
+
const sessionIsActive = sessionActive.value;
|
|
556
77
|
|
|
557
78
|
const handleToggleMinimap = useCallback(() => setMinimapVisible((v) => !v), []);
|
|
558
79
|
const handleToggleSnapshot = useCallback(() => setSnapshotOpen((v) => !v), []);
|
|
559
80
|
const handleCloseSnapshot = useCallback(() => setSnapshotOpen(false), []);
|
|
560
|
-
const
|
|
561
|
-
() => setAnnotationTool((tool) => (tool === 'pen' ? null : 'pen')),
|
|
562
|
-
[],
|
|
563
|
-
);
|
|
564
|
-
const handleToggleAnnotationEraser = useCallback(
|
|
565
|
-
() => setAnnotationTool((tool) => (tool === 'eraser' ? null : 'eraser')),
|
|
566
|
-
[],
|
|
567
|
-
);
|
|
568
|
-
const handleToggleTextAnnotation = useCallback(
|
|
569
|
-
() => setAnnotationTool((tool) => (tool === 'text' ? null : 'text')),
|
|
570
|
-
[],
|
|
571
|
-
);
|
|
81
|
+
const handleSetAnnotationTool = useCallback((tool: AnnotationTool) => setAnnotationTool(tool), []);
|
|
572
82
|
|
|
573
83
|
const handleMinimapNavigate = useCallback((x: number, y: number) => {
|
|
574
84
|
animateViewport({ x, y, scale: viewport.value.scale }, 200);
|
|
@@ -580,7 +90,8 @@ export function App() {
|
|
|
580
90
|
|
|
581
91
|
// Keep the server's idea of this window's size current (0.4.6 orb feedback
|
|
582
92
|
// #2) so an agent `fit` computes a scale that actually fits the human's
|
|
583
|
-
//
|
|
93
|
+
// canvas region. Connect-time reporting lives in the SSE bridge; this covers
|
|
94
|
+
// resizes. The report itself reads canvasArea(), so it reflects the region.
|
|
584
95
|
useEffect(() => {
|
|
585
96
|
let timer: number | null = null;
|
|
586
97
|
const schedule = () => {
|
|
@@ -599,7 +110,9 @@ export function App() {
|
|
|
599
110
|
|
|
600
111
|
useEffect(() => {
|
|
601
112
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
602
|
-
|
|
113
|
+
// AltGr on European Windows layouts reports Ctrl+Alt — never treat it
|
|
114
|
+
// as the shortcut modifier, or AltGr+Z (ż, ź…) fires undo.
|
|
115
|
+
const mod = (e.metaKey || e.ctrlKey) && !e.altKey;
|
|
603
116
|
|
|
604
117
|
// Cmd/Ctrl+K toggles command palette (works from anywhere, including inputs)
|
|
605
118
|
if (mod && e.key === 'k') {
|
|
@@ -636,9 +149,20 @@ export function App() {
|
|
|
636
149
|
return;
|
|
637
150
|
}
|
|
638
151
|
|
|
639
|
-
// Ignore other shortcuts when inside inputs
|
|
640
|
-
|
|
641
|
-
|
|
152
|
+
// Ignore other shortcuts when inside inputs or editable content — the
|
|
153
|
+
// single-key rail shortcuts below would otherwise eat characters typed
|
|
154
|
+
// into the inline markdown editor (contenteditable), which INPUT/TEXTAREA
|
|
155
|
+
// checks alone do not cover.
|
|
156
|
+
const target = e.target as HTMLElement | null;
|
|
157
|
+
const tag = target?.tagName;
|
|
158
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || target?.isContentEditable) return;
|
|
159
|
+
|
|
160
|
+
// Held Space = temporary pan tool (released on keyup below).
|
|
161
|
+
if (e.key === ' ' && !e.repeat && !mod) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
spacePanHeld.value = true;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
642
166
|
|
|
643
167
|
// ? toggles shortcut overlay
|
|
644
168
|
if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
|
|
@@ -647,12 +171,21 @@ export function App() {
|
|
|
647
171
|
return;
|
|
648
172
|
}
|
|
649
173
|
|
|
650
|
-
if (mod && e.key === '
|
|
174
|
+
if (mod && (e.key === 'z' || e.key === 'Z')) {
|
|
175
|
+
// One shared undo stack (item 10): Ctrl+Z undoes whichever op is on
|
|
176
|
+
// top, agent or human; Shift redoes.
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
void undoFromKeyboard(e.shiftKey);
|
|
179
|
+
} else if (mod && (e.key === 'y' || e.key === 'Y')) {
|
|
180
|
+
// Ctrl+Y — the Windows redo convention.
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
void undoFromKeyboard(true);
|
|
183
|
+
} else if (mod && e.key === '0') {
|
|
651
184
|
e.preventDefault();
|
|
652
185
|
animateViewport({ x: 0, y: 0, scale: 1 }, 250);
|
|
653
186
|
} else if (mod && (e.key === '=' || e.key === '+')) {
|
|
654
187
|
e.preventDefault();
|
|
655
|
-
// Same centre-anchored zoom as the
|
|
188
|
+
// Same centre-anchored zoom as the top-bar +/- buttons these shortcuts
|
|
656
189
|
// are advertised on (the tooltip names this key) — a raw scale change
|
|
657
190
|
// is origin-anchored and drifts the board away under the cursor.
|
|
658
191
|
zoomByFactor(1.25);
|
|
@@ -660,7 +193,7 @@ export function App() {
|
|
|
660
193
|
e.preventDefault();
|
|
661
194
|
zoomByFactor(1 / 1.25);
|
|
662
195
|
} else if (e.key === 'Escape') {
|
|
663
|
-
if (selectedNodeIds.value.size > 0) {
|
|
196
|
+
if (selectedNodeIds.value.size > 0 || selectedEdgeId.value !== null) {
|
|
664
197
|
clearSelection();
|
|
665
198
|
return;
|
|
666
199
|
}
|
|
@@ -669,19 +202,117 @@ export function App() {
|
|
|
669
202
|
} else if (e.key === 'Tab') {
|
|
670
203
|
e.preventDefault();
|
|
671
204
|
cycleActiveNode(e.shiftKey ? -1 : 1);
|
|
205
|
+
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
206
|
+
// A click-selected edge deletes first — keyboard parity with the edge
|
|
207
|
+
// context menu (draw with C, click it, press Delete).
|
|
208
|
+
if (selectedEdgeId.value !== null) {
|
|
209
|
+
const edgeId = selectedEdgeId.value;
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
selectedEdgeId.value = null;
|
|
212
|
+
void removeEdgeFromClient(edgeId).catch((error) => logAppError('delete-edge', error));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Delete the selection, else the focused node — what the selection
|
|
216
|
+
// bar's delete and the card's × do, from the keyboard.
|
|
217
|
+
const ids =
|
|
218
|
+
selectedNodeIds.value.size > 0 ? [...selectedNodeIds.value] : activeNodeId.value ? [activeNodeId.value] : [];
|
|
219
|
+
if (ids.length === 0) return;
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
for (const id of ids) {
|
|
222
|
+
removeNode(id);
|
|
223
|
+
void removeNodeFromClient(id).catch((error) => logAppError('delete', error));
|
|
224
|
+
}
|
|
225
|
+
clearSelection();
|
|
226
|
+
activeNodeId.value = null;
|
|
672
227
|
} else if (activeNodeId.value && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
|
673
228
|
e.preventDefault();
|
|
674
229
|
const dir = e.key.replace('Arrow', '').toLowerCase() as 'up' | 'down' | 'left' | 'right';
|
|
675
230
|
walkGraph(dir);
|
|
231
|
+
} else if (!mod && !e.altKey) {
|
|
232
|
+
// Rail shortcuts — the rail buttons advertise these in their titles.
|
|
233
|
+
const key = e.key.toLowerCase();
|
|
234
|
+
if (key === 'v' && !e.shiftKey) {
|
|
235
|
+
canvasTool.value = 'select';
|
|
236
|
+
} else if (key === 'c' && !e.shiftKey && !mod) {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
canvasTool.value = canvasTool.value === 'connect' ? 'select' : 'connect';
|
|
239
|
+
} else if (key === 'm' && !e.shiftKey) {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
void createNodeInView({ type: 'markdown', title: 'New note', width: 520, height: 360 }).catch((error) =>
|
|
242
|
+
logAppError('create markdown', error),
|
|
243
|
+
);
|
|
244
|
+
} else if (key === 'a' && !e.shiftKey) {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
setAnnotationTool((tool) => (tool === 'pen' ? null : 'pen'));
|
|
247
|
+
} else if (key === 'g' && !e.shiftKey) {
|
|
248
|
+
// Groups v2: G groups the selection (≥2 nodes); otherwise a new empty frame.
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
const ids = Array.from(selectedNodeIds.value);
|
|
251
|
+
if (ids.length >= 2) {
|
|
252
|
+
void createGroupFromClient({ title: 'Group', childIds: ids }).catch((error) =>
|
|
253
|
+
logAppError('group selection', error),
|
|
254
|
+
);
|
|
255
|
+
clearSelection();
|
|
256
|
+
} else {
|
|
257
|
+
void createNodeInView({ type: 'group', title: 'Group' }).catch((error) =>
|
|
258
|
+
logAppError('create group', error),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
} else if (key === 'g' && e.shiftKey) {
|
|
262
|
+
// Shift+G dissolves every group the selection touches — the same
|
|
263
|
+
// `group.remove` op an agent runs (children released, frame gone).
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
for (const groupId of groupsOfSelection()) {
|
|
266
|
+
removeNode(groupId);
|
|
267
|
+
void ungroupFromClient(groupId).catch((error) => logAppError('ungroup', error));
|
|
268
|
+
}
|
|
269
|
+
clearSelection();
|
|
270
|
+
} else if (key === 'i' && !e.shiftKey) {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
promptedCreate('image');
|
|
273
|
+
} else if (key === 'w' && !e.shiftKey) {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
promptedCreate('webpage');
|
|
276
|
+
} else if (key === 'h' && !e.shiftKey) {
|
|
277
|
+
e.preventDefault();
|
|
278
|
+
void createNodeInView({ type: 'html', title: 'HTML surface' }).catch((error) =>
|
|
279
|
+
logAppError('create html', error),
|
|
280
|
+
);
|
|
281
|
+
} else if (key === 'f' && e.shiftKey) {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
promptedCreate('file');
|
|
284
|
+
} else if (key === 'f' && !e.shiftKey) {
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
const area = canvasArea();
|
|
287
|
+
fitAll(area.width, area.height);
|
|
288
|
+
}
|
|
676
289
|
}
|
|
677
290
|
};
|
|
678
291
|
|
|
292
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
293
|
+
if (e.key === ' ') spacePanHeld.value = false;
|
|
294
|
+
};
|
|
295
|
+
const handleBlur = () => {
|
|
296
|
+
spacePanHeld.value = false;
|
|
297
|
+
};
|
|
298
|
+
|
|
679
299
|
document.addEventListener('keydown', handleKeyDown);
|
|
300
|
+
document.addEventListener('keyup', handleKeyUp);
|
|
301
|
+
window.addEventListener('blur', handleBlur);
|
|
680
302
|
return () => {
|
|
681
303
|
document.removeEventListener('keydown', handleKeyDown);
|
|
304
|
+
document.removeEventListener('keyup', handleKeyUp);
|
|
305
|
+
window.removeEventListener('blur', handleBlur);
|
|
682
306
|
};
|
|
683
307
|
}, [annotationTool, closeMenu, paletteOpen, shortcutsOpen]);
|
|
684
308
|
|
|
309
|
+
// The session panel takes 320px from the canvas region: re-report the
|
|
310
|
+
// region size so an agent `fit` keeps fitting the visible canvas.
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
const timer = window.setTimeout(() => void reportClientViewportSize(), 250);
|
|
313
|
+
return () => window.clearTimeout(timer);
|
|
314
|
+
}, [sessionIsActive]);
|
|
315
|
+
|
|
685
316
|
useEffect(() => {
|
|
686
317
|
if (!hasInitialLayout) return;
|
|
687
318
|
const ready = (window as Window & { __pmxCanvasBootstrapReady?: () => void }).__pmxCanvasBootstrapReady;
|
|
@@ -689,66 +320,59 @@ export function App() {
|
|
|
689
320
|
}, [hasInitialLayout]);
|
|
690
321
|
|
|
691
322
|
const allNodes = Array.from(nodes.value.values());
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
.filter((n) => n.dockPosition === 'right')
|
|
695
|
-
.sort((a, b) => {
|
|
696
|
-
const order: Record<string, number> = { context: 0, ledger: 1 };
|
|
697
|
-
return (order[a.type] ?? 2) - (order[b.type] ?? 2);
|
|
698
|
-
});
|
|
323
|
+
|
|
324
|
+
const area = canvasArea();
|
|
699
325
|
|
|
700
326
|
return (
|
|
701
|
-
<div
|
|
702
|
-
<
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
onToggleMinimap={handleToggleMinimap}
|
|
711
|
-
snapshotOpen={snapshotOpen}
|
|
712
|
-
onToggleSnapshot={handleToggleSnapshot}
|
|
713
|
-
snapshotBtnRef={snapshotBtnRef}
|
|
714
|
-
onOpenPalette={() => setPaletteOpen(true)}
|
|
715
|
-
onOpenShortcuts={() => setShortcutsOpen((v) => !v)}
|
|
716
|
-
annotationTool={annotationTool}
|
|
717
|
-
onToggleAnnotationMode={handleToggleAnnotationMode}
|
|
718
|
-
onToggleAnnotationEraser={handleToggleAnnotationEraser}
|
|
719
|
-
onToggleTextAnnotation={handleToggleTextAnnotation}
|
|
720
|
-
/>
|
|
721
|
-
<div class="hud-right">
|
|
722
|
-
{dockedRight.map((n) => (
|
|
723
|
-
<DockedNode key={n.id} node={n} />
|
|
724
|
-
))}
|
|
725
|
-
</div>
|
|
726
|
-
</div>
|
|
727
|
-
<AttentionToast />
|
|
728
|
-
<AttentionHistory />
|
|
729
|
-
<CanvasViewport
|
|
730
|
-
onNodeContextMenu={openNodeMenu}
|
|
731
|
-
onCanvasContextMenu={openCanvasMenu}
|
|
732
|
-
annotationMode={annotationTool !== null}
|
|
327
|
+
<div class="app-shell" data-session-active={sessionIsActive ? 'true' : 'false'}>
|
|
328
|
+
<ToolRail
|
|
329
|
+
minimapVisible={minimapVisible}
|
|
330
|
+
onToggleMinimap={handleToggleMinimap}
|
|
331
|
+
snapshotOpen={snapshotOpen}
|
|
332
|
+
onToggleSnapshot={handleToggleSnapshot}
|
|
333
|
+
snapshotBtnRef={snapshotBtnRef}
|
|
334
|
+
onOpenPalette={() => setPaletteOpen(true)}
|
|
335
|
+
onOpenShortcuts={() => setShortcutsOpen((v) => !v)}
|
|
733
336
|
annotationTool={annotationTool}
|
|
337
|
+
onSetAnnotationTool={handleSetAnnotationTool}
|
|
734
338
|
/>
|
|
735
|
-
|
|
736
|
-
<
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
339
|
+
<div class="app-main">
|
|
340
|
+
<TopBar />
|
|
341
|
+
<ConnectionBanner />
|
|
342
|
+
<div class="canvas-region" ref={(el) => registerCanvasArea(el)}>
|
|
343
|
+
<CanvasViewport
|
|
344
|
+
onNodeContextMenu={openNodeMenu}
|
|
345
|
+
onCanvasContextMenu={openCanvasMenu}
|
|
346
|
+
onEdgeContextMenu={openEdgeMenu}
|
|
347
|
+
annotationMode={annotationTool !== null}
|
|
348
|
+
annotationTool={annotationTool}
|
|
349
|
+
/>
|
|
350
|
+
<AttentionToast />
|
|
351
|
+
<AttentionHistory />
|
|
352
|
+
<ActivityFeed />
|
|
353
|
+
<WritersSheet />
|
|
354
|
+
{hasInitialLayout && allNodes.length === 0 && intents.value.size === 0 && (
|
|
355
|
+
<EmptyState onOpenPalette={() => setPaletteOpen(true)} />
|
|
356
|
+
)}
|
|
357
|
+
{selectedNodeIds.value.size > 0 && <SelectionBar />}
|
|
358
|
+
{sessionIsActive ? <CommandBar /> : contextPinnedNodeIds.value.size > 0 && <ContextPinBar />}
|
|
359
|
+
<SessionReceipt onOpenSnapshots={() => setSnapshotOpen(true)} />
|
|
360
|
+
<TextPrompt />
|
|
361
|
+
{expandedNodeId.value && <ExpandedNodeOverlay />}
|
|
362
|
+
<SnapshotPanel open={snapshotOpen} onClose={handleCloseSnapshot} anchorRef={snapshotBtnRef} />
|
|
363
|
+
{minimapVisible && (
|
|
364
|
+
<Minimap
|
|
365
|
+
viewport={viewport}
|
|
366
|
+
nodes={nodes}
|
|
367
|
+
edges={edges}
|
|
368
|
+
onNavigate={handleMinimapNavigate}
|
|
369
|
+
containerWidth={area.width}
|
|
370
|
+
containerHeight={area.height}
|
|
371
|
+
/>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
{sessionIsActive && <SessionPanel />}
|
|
752
376
|
{menu && <ContextMenu menu={menu} onClose={closeMenu} />}
|
|
753
377
|
{paletteOpen && <CommandPalette onClose={() => setPaletteOpen(false)} onToggleMinimap={handleToggleMinimap} />}
|
|
754
378
|
{shortcutsOpen && <ShortcutOverlay onClose={() => setShortcutsOpen(false)} />}
|