pmx-canvas 0.4.7 → 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 +156 -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/ExtAppFrame.d.ts +32 -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 +23 -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 +54 -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 +109 -18
- 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 +57 -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/ExtAppFrame.tsx +79 -2
- 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 +237 -61
- 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,11 +1,31 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import {
|
|
3
|
+
type CanvasSnapshotInfo,
|
|
4
|
+
deleteSnapshot,
|
|
3
5
|
listSnapshots,
|
|
4
|
-
|
|
6
|
+
requestJson,
|
|
5
7
|
restoreSnapshot,
|
|
6
|
-
|
|
7
|
-
type CanvasSnapshotInfo,
|
|
8
|
+
saveSnapshot,
|
|
8
9
|
} from '../state/intent-bridge';
|
|
10
|
+
import { type DiffSummary, summarizeDiff } from './SessionReceipt';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* History drawer (rail-chrome-v2 phase 7, design item 8): snapshots and
|
|
14
|
+
* session receipts in one reverse-chronological list. A session's entry IS
|
|
15
|
+
* the snapshot the server took when it attached ("Before session · <label> ·
|
|
16
|
+
* HH:MM"), so the two histories are one timeline: session entries carry View
|
|
17
|
+
* diff (the session's own changes) and Restore pre-state; manual snapshots
|
|
18
|
+
* keep Restore / Delete. Opened from the rail's Snapshots button or the
|
|
19
|
+
* receipt's Full log. The root keeps the `.snapshot-panel` hook.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const SESSION_PREFIX = 'Before session · ';
|
|
23
|
+
|
|
24
|
+
function timeLabel(iso: string): string {
|
|
25
|
+
const date = new Date(iso);
|
|
26
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
27
|
+
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
28
|
+
}
|
|
9
29
|
|
|
10
30
|
function timeAgo(iso: string): string {
|
|
11
31
|
const diff = Date.now() - new Date(iso).getTime();
|
|
@@ -18,6 +38,14 @@ function timeAgo(iso: string): string {
|
|
|
18
38
|
return `${days}d ago`;
|
|
19
39
|
}
|
|
20
40
|
|
|
41
|
+
/** "Before session · Copilot · 14:00" → "Copilot". */
|
|
42
|
+
function sessionLabel(name: string): string | null {
|
|
43
|
+
if (!name.startsWith(SESSION_PREFIX)) return null;
|
|
44
|
+
const rest = name.slice(SESSION_PREFIX.length);
|
|
45
|
+
const cut = rest.lastIndexOf(' · ');
|
|
46
|
+
return cut > 0 ? rest.slice(0, cut) : rest;
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
export function SnapshotPanel({
|
|
22
50
|
open,
|
|
23
51
|
onClose,
|
|
@@ -33,10 +61,10 @@ export function SnapshotPanel({
|
|
|
33
61
|
const [restoringId, setRestoringId] = useState<string | null>(null);
|
|
34
62
|
const [nameInput, setNameInput] = useState('');
|
|
35
63
|
const [confirming, setConfirming] = useState<{ id: string; action: 'restore' | 'delete' } | null>(null);
|
|
64
|
+
const [diffs, setDiffs] = useState<Record<string, DiffSummary | 'loading'>>({});
|
|
36
65
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
37
66
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
38
67
|
|
|
39
|
-
// Load snapshots when panel opens
|
|
40
68
|
useEffect(() => {
|
|
41
69
|
if (!open) return;
|
|
42
70
|
setLoading(true);
|
|
@@ -46,7 +74,6 @@ export function SnapshotPanel({
|
|
|
46
74
|
});
|
|
47
75
|
}, [open]);
|
|
48
76
|
|
|
49
|
-
// Focus input on open
|
|
50
77
|
useEffect(() => {
|
|
51
78
|
if (open) setTimeout(() => inputRef.current?.focus(), 50);
|
|
52
79
|
}, [open]);
|
|
@@ -65,7 +92,6 @@ export function SnapshotPanel({
|
|
|
65
92
|
return () => document.removeEventListener('mousedown', handler);
|
|
66
93
|
}, [open, onClose]);
|
|
67
94
|
|
|
68
|
-
// Escape to close
|
|
69
95
|
useEffect(() => {
|
|
70
96
|
if (!open) return;
|
|
71
97
|
const handler = (e: KeyboardEvent) => {
|
|
@@ -100,30 +126,79 @@ export function SnapshotPanel({
|
|
|
100
126
|
|
|
101
127
|
const handleDelete = useCallback(async (id: string) => {
|
|
102
128
|
const result = await deleteSnapshot(id);
|
|
103
|
-
if (result.ok)
|
|
104
|
-
setSnapshots((prev) => prev.filter((s) => s.id !== id));
|
|
105
|
-
}
|
|
129
|
+
if (result.ok) setSnapshots((prev) => prev.filter((s) => s.id !== id));
|
|
106
130
|
setConfirming(null);
|
|
107
131
|
}, []);
|
|
108
132
|
|
|
133
|
+
const viewDiff = useCallback(async (id: string) => {
|
|
134
|
+
setDiffs((prev) => ({ ...prev, [id]: 'loading' }));
|
|
135
|
+
const body = await requestJson<{ diff?: unknown }>(
|
|
136
|
+
'snapshotDiff',
|
|
137
|
+
`/api/canvas/snapshots/${encodeURIComponent(id)}/diff`,
|
|
138
|
+
{},
|
|
139
|
+
);
|
|
140
|
+
setDiffs((prev) => ({ ...prev, [id]: summarizeDiff(body.diff) ?? { added: 0, removed: 0, modified: 0 } }));
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
109
143
|
if (!open) return null;
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
const actions = (snap: CanvasSnapshotInfo, restoreLabel: string) =>
|
|
146
|
+
confirming?.id === snap.id ? (
|
|
147
|
+
<>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
class={`snapshot-action-btn ${confirming.action === 'delete' ? 'snapshot-action-confirm' : 'snapshot-action-restore'}`}
|
|
151
|
+
onClick={() => (confirming.action === 'delete' ? handleDelete(snap.id) : handleRestore(snap.id))}
|
|
152
|
+
title={confirming.action === 'delete' ? 'Confirm delete' : 'Confirm restore'}
|
|
153
|
+
disabled={restoringId !== null}
|
|
154
|
+
>
|
|
155
|
+
{confirming.action === 'delete' ? 'Delete' : restoringId === snap.id ? 'Restoring…' : 'Confirm'}
|
|
156
|
+
</button>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
class="snapshot-action-btn"
|
|
160
|
+
onClick={() => setConfirming(null)}
|
|
161
|
+
title="Cancel"
|
|
162
|
+
disabled={restoringId !== null}
|
|
163
|
+
>
|
|
164
|
+
Cancel
|
|
165
|
+
</button>
|
|
166
|
+
</>
|
|
167
|
+
) : (
|
|
168
|
+
<>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
class="snapshot-action-btn snapshot-action-restore"
|
|
172
|
+
onClick={() => setConfirming({ id: snap.id, action: 'restore' })}
|
|
173
|
+
title="Restore this snapshot — replaces the current canvas (undoable)"
|
|
174
|
+
disabled={restoringId !== null}
|
|
175
|
+
>
|
|
176
|
+
{restoringId === snap.id ? 'Restoring…' : restoreLabel}
|
|
177
|
+
</button>
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
class="snapshot-action-btn snapshot-action-delete"
|
|
181
|
+
onClick={() => setConfirming({ id: snap.id, action: 'delete' })}
|
|
182
|
+
title="Delete this snapshot"
|
|
183
|
+
aria-label="Delete snapshot"
|
|
184
|
+
disabled={restoringId !== null}
|
|
185
|
+
>
|
|
186
|
+
✕
|
|
187
|
+
</button>
|
|
188
|
+
</>
|
|
189
|
+
);
|
|
115
190
|
|
|
116
191
|
return (
|
|
117
|
-
<div ref={panelRef} class="snapshot-panel"
|
|
118
|
-
{/* Header */}
|
|
192
|
+
<div ref={panelRef} class="snapshot-panel" data-testid="history-drawer" role="dialog" aria-label="History">
|
|
119
193
|
<div class="snapshot-panel-header">
|
|
120
|
-
<span class="snapshot-panel-title">
|
|
121
|
-
<
|
|
122
|
-
|
|
194
|
+
<span class="snapshot-panel-title">History</span>
|
|
195
|
+
<span class="snapshot-panel-sub">snapshots + sessions</span>
|
|
196
|
+
<span class="snapshot-panel-spacer" />
|
|
197
|
+
<button type="button" class="snapshot-panel-close" onClick={onClose} title="Close" aria-label="Close history">
|
|
198
|
+
×
|
|
123
199
|
</button>
|
|
124
200
|
</div>
|
|
125
201
|
|
|
126
|
-
{/* Save form */}
|
|
127
202
|
<div class="snapshot-save-form">
|
|
128
203
|
<input
|
|
129
204
|
ref={inputRef}
|
|
@@ -134,85 +209,96 @@ export function SnapshotPanel({
|
|
|
134
209
|
onKeyDown={(e) => {
|
|
135
210
|
if (e.key === 'Enter') handleSave();
|
|
136
211
|
}}
|
|
137
|
-
placeholder="
|
|
212
|
+
placeholder="Save a snapshot as…"
|
|
213
|
+
aria-label="Snapshot name"
|
|
138
214
|
maxLength={80}
|
|
139
215
|
disabled={saving}
|
|
140
216
|
/>
|
|
141
217
|
<button type="button" class="snapshot-save-btn" onClick={handleSave} disabled={!nameInput.trim() || saving}>
|
|
142
|
-
{saving ? '
|
|
218
|
+
{saving ? '…' : 'Save'}
|
|
143
219
|
</button>
|
|
144
220
|
</div>
|
|
145
|
-
|
|
146
221
|
<div class="snapshot-restore-note">Restoring replaces the current canvas. You can undo it if needed.</div>
|
|
147
222
|
|
|
148
|
-
{/* Snapshot list */}
|
|
149
223
|
<div class="snapshot-list">
|
|
150
|
-
{loading && <div class="snapshot-empty">Loading
|
|
151
|
-
|
|
224
|
+
{loading && <div class="snapshot-empty">Loading…</div>}
|
|
152
225
|
{!loading && snapshots.length === 0 && (
|
|
153
|
-
<div class="snapshot-empty">
|
|
226
|
+
<div class="snapshot-empty">
|
|
227
|
+
No history yet. Save a snapshot, or start an agent session — it snapshots for you.
|
|
228
|
+
</div>
|
|
154
229
|
)}
|
|
155
|
-
|
|
230
|
+
{/* Boards the human saved on purpose come first; the automatic
|
|
231
|
+
pre-session snapshots live under their own header — mixing them
|
|
232
|
+
buried the deliberate saves under session churn. */}
|
|
156
233
|
{!loading &&
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
234
|
+
[
|
|
235
|
+
...snapshots.filter((snap) => sessionLabel(snap.name) === null),
|
|
236
|
+
...(snapshots.some((snap) => sessionLabel(snap.name) !== null)
|
|
237
|
+
? [{ id: '__sessions-header__' } as (typeof snapshots)[number]]
|
|
238
|
+
: []),
|
|
239
|
+
...snapshots.filter((snap) => sessionLabel(snap.name) !== null),
|
|
240
|
+
].map((snap) => {
|
|
241
|
+
if (snap.id === '__sessions-header__') {
|
|
242
|
+
return (
|
|
243
|
+
<div key={snap.id} class="snapshot-section-header" data-testid="history-sessions-header">
|
|
244
|
+
Sessions
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const label = sessionLabel(snap.name);
|
|
249
|
+
const diff = diffs[snap.id];
|
|
250
|
+
if (label !== null) {
|
|
251
|
+
return (
|
|
252
|
+
<div key={snap.id} class="snapshot-item is-session" data-testid="history-session">
|
|
253
|
+
<div class="snapshot-item-head">
|
|
254
|
+
<span class="snapshot-session-dot" aria-hidden="true" />
|
|
255
|
+
<span class="snapshot-item-name">Agent session — {label}</span>
|
|
256
|
+
<span class="snapshot-item-time">{timeLabel(snap.createdAt)}</span>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="snapshot-item-meta">
|
|
259
|
+
snapshot at session start · {snap.nodeCount} node{snap.nodeCount !== 1 ? 's' : ''}
|
|
260
|
+
{diff && diff !== 'loading' && (
|
|
261
|
+
<span class="snapshot-item-diff">
|
|
262
|
+
{' '}
|
|
263
|
+
· since: {diff.added} added · {diff.removed} removed · {diff.modified} modified
|
|
264
|
+
</span>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
<div class="snapshot-item-actions is-session">
|
|
171
268
|
<button
|
|
172
269
|
type="button"
|
|
173
|
-
class=
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
disabled={restoringId !== null}
|
|
270
|
+
class="snapshot-action-btn snapshot-action-diff"
|
|
271
|
+
disabled={diff === 'loading'}
|
|
272
|
+
onClick={() => void viewDiff(snap.id)}
|
|
177
273
|
>
|
|
178
|
-
{
|
|
274
|
+
{diff === 'loading' ? 'Comparing…' : 'View diff'}
|
|
179
275
|
</button>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<button
|
|
202
|
-
type="button"
|
|
203
|
-
class="snapshot-action-btn snapshot-action-delete"
|
|
204
|
-
onClick={() => setConfirming({ id: snap.id, action: 'delete' })}
|
|
205
|
-
title="Delete this snapshot"
|
|
206
|
-
disabled={restoringId !== null}
|
|
207
|
-
>
|
|
208
|
-
{'\u2715'}
|
|
209
|
-
</button>
|
|
210
|
-
</>
|
|
211
|
-
)}
|
|
276
|
+
{actions(snap, 'Restore pre-state')}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return (
|
|
282
|
+
<div key={snap.id} class="snapshot-item" data-testid="history-snapshot">
|
|
283
|
+
<div class="snapshot-item-head">
|
|
284
|
+
<span class="snapshot-item-glyph" aria-hidden="true">
|
|
285
|
+
◫
|
|
286
|
+
</span>
|
|
287
|
+
<span class="snapshot-item-name">{snap.name}</span>
|
|
288
|
+
<span class="snapshot-item-time">{timeLabel(snap.createdAt)}</span>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="snapshot-item-meta">
|
|
291
|
+
{snap.nodeCount} node{snap.nodeCount !== 1 ? 's' : ''}
|
|
292
|
+
{snap.edgeCount > 0 ? ` · ${snap.edgeCount} edge${snap.edgeCount !== 1 ? 's' : ''}` : ''}
|
|
293
|
+
{' · '}
|
|
294
|
+
{timeAgo(snap.createdAt)}
|
|
295
|
+
</div>
|
|
296
|
+
<div class="snapshot-item-actions">{actions(snap, 'Restore')}</div>
|
|
212
297
|
</div>
|
|
213
|
-
|
|
214
|
-
)
|
|
298
|
+
);
|
|
299
|
+
})}
|
|
215
300
|
</div>
|
|
301
|
+
<div class="snapshot-panel-foot">Every agent session start writes a snapshot automatically.</div>
|
|
216
302
|
</div>
|
|
217
303
|
);
|
|
218
304
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { signal } from '@preact/signals';
|
|
2
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* In-canvas replacement for `window.prompt`, which embedded browser panes
|
|
6
|
+
* silently no-op (a Shift+F that "does nothing") and no test can drive. One
|
|
7
|
+
* request at a time; a new ask cancels the previous one.
|
|
8
|
+
*/
|
|
9
|
+
interface TextPromptRequest {
|
|
10
|
+
title: string;
|
|
11
|
+
placeholder: string;
|
|
12
|
+
/** Prefilled (and pre-selected) value — edit flows start from the current text. */
|
|
13
|
+
initial: string;
|
|
14
|
+
/** Submitting an empty field resolves '' instead of null — "clear the value" flows. */
|
|
15
|
+
allowEmpty: boolean;
|
|
16
|
+
confirm: string;
|
|
17
|
+
/** Bumped per ask so consecutive requests get a FRESH input element. */
|
|
18
|
+
seq: number;
|
|
19
|
+
resolve: (value: string | null) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const textPromptRequest = signal<TextPromptRequest | null>(null);
|
|
23
|
+
let promptSeq = 0;
|
|
24
|
+
|
|
25
|
+
export function askText(
|
|
26
|
+
title: string,
|
|
27
|
+
placeholder: string,
|
|
28
|
+
opts: { initial?: string; allowEmpty?: boolean; confirm?: string } = {},
|
|
29
|
+
): Promise<string | null> {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
textPromptRequest.value?.resolve(null);
|
|
32
|
+
promptSeq += 1;
|
|
33
|
+
textPromptRequest.value = {
|
|
34
|
+
seq: promptSeq,
|
|
35
|
+
title,
|
|
36
|
+
placeholder,
|
|
37
|
+
initial: opts.initial ?? '',
|
|
38
|
+
allowEmpty: opts.allowEmpty ?? false,
|
|
39
|
+
confirm: opts.confirm ?? 'Add',
|
|
40
|
+
resolve,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function settle(value: string | null): void {
|
|
46
|
+
const request = textPromptRequest.value;
|
|
47
|
+
textPromptRequest.value = null;
|
|
48
|
+
request?.resolve(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function TextPrompt() {
|
|
52
|
+
const request = textPromptRequest.value;
|
|
53
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!request) return;
|
|
57
|
+
// Esc closes the dialog wherever focus sits (a click can move focus off the
|
|
58
|
+
// input, and the global shortcut layer must never see this Esc). Focus and
|
|
59
|
+
// the prefill are NOT here: an effect runs a frame after the dialog paints,
|
|
60
|
+
// and a key (or a test's fill) inside that gap lands on the wrong target.
|
|
61
|
+
const onKey = (e: KeyboardEvent) => {
|
|
62
|
+
if (e.key !== 'Escape') return;
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
settle(null);
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener('keydown', onKey, { capture: true });
|
|
68
|
+
return () => window.removeEventListener('keydown', onKey, { capture: true });
|
|
69
|
+
}, [request]);
|
|
70
|
+
|
|
71
|
+
if (!request) return null;
|
|
72
|
+
|
|
73
|
+
const submit = () => {
|
|
74
|
+
const value = inputRef.current?.value.trim() ?? '';
|
|
75
|
+
settle(value ? value : request.allowEmpty ? '' : null);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div class="text-prompt-backdrop" onPointerDown={() => settle(null)}>
|
|
80
|
+
<div
|
|
81
|
+
class="text-prompt"
|
|
82
|
+
role="dialog"
|
|
83
|
+
aria-label={request.title}
|
|
84
|
+
data-testid="text-prompt"
|
|
85
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
86
|
+
>
|
|
87
|
+
<div class="text-prompt-title">{request.title}</div>
|
|
88
|
+
<input
|
|
89
|
+
// A fresh element per ask (so defaultValue applies) focused in the
|
|
90
|
+
// ref callback, which Preact runs synchronously during commit — the
|
|
91
|
+
// dialog is never on screen unfocused.
|
|
92
|
+
key={request.seq}
|
|
93
|
+
ref={(el) => {
|
|
94
|
+
inputRef.current = el;
|
|
95
|
+
if (!el) return;
|
|
96
|
+
el.focus();
|
|
97
|
+
// Edit flows prefill — selecting it lets a retype replace in one stroke.
|
|
98
|
+
if (request.initial) el.select();
|
|
99
|
+
}}
|
|
100
|
+
class="text-prompt-input"
|
|
101
|
+
type="text"
|
|
102
|
+
defaultValue={request.initial}
|
|
103
|
+
placeholder={request.placeholder}
|
|
104
|
+
onKeyDown={(e) => {
|
|
105
|
+
// Contained: the global shortcut layer must not see these.
|
|
106
|
+
if (e.key === 'Enter') {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
submit();
|
|
110
|
+
} else if (e.key === 'Escape') {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
e.stopPropagation();
|
|
113
|
+
settle(null);
|
|
114
|
+
}
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
<div class="text-prompt-actions">
|
|
118
|
+
<button type="button" class="text-prompt-cancel" onClick={() => settle(null)}>
|
|
119
|
+
Cancel
|
|
120
|
+
</button>
|
|
121
|
+
<button type="button" class="text-prompt-confirm" onClick={submit}>
|
|
122
|
+
{request.confirm}
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|