pmx-canvas 0.6.4 → 0.7.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 +67 -3
- package/CHANGELOG.md +73 -0
- package/Readme.md +35 -5
- package/dist/canvas/global.css +498 -28
- package/dist/canvas/index.js +110 -95
- package/dist/canvas/mermaid-entry.js +258 -258
- package/dist/types/cli/agent.d.ts +4 -0
- package/dist/types/cli/commands/backup.d.ts +1 -0
- package/dist/types/cli/commands/boards.d.ts +1 -0
- package/dist/types/cli/commands/export.d.ts +1 -0
- package/dist/types/cli/commands/record.d.ts +12 -0
- package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
- package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
- package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -0
- package/dist/types/client/canvas/Presentation.d.ts +13 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
- package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
- package/dist/types/client/state/boards-store.d.ts +79 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/presentation.d.ts +3 -0
- package/dist/types/client/state/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +3 -3
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +8 -0
- package/dist/types/mcp/canvas-access.d.ts +10 -3
- package/dist/types/mcp/context-uri-template.d.ts +6 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/attribution.d.ts +11 -0
- package/dist/types/server/ax-context.d.ts +3 -1
- package/dist/types/server/ax-interaction.d.ts +2 -2
- package/dist/types/server/ax-state.d.ts +13 -3
- package/dist/types/server/backup.d.ts +36 -0
- package/dist/types/server/board-export.d.ts +45 -0
- package/dist/types/server/canvas-db.d.ts +65 -9
- package/dist/types/server/canvas-operations.d.ts +45 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +146 -3
- package/dist/types/server/canvas-validation.d.ts +3 -0
- package/dist/types/server/context-brief.d.ts +111 -0
- package/dist/types/server/context-reads.d.ts +55 -0
- package/dist/types/server/document-import.d.ts +66 -0
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/index.d.ts +107 -4
- package/dist/types/server/mutation-history.d.ts +1 -0
- package/dist/types/server/operations/invoker.d.ts +2 -0
- package/dist/types/server/operations/mcp.d.ts +7 -1
- package/dist/types/server/operations/ops/app.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
- package/dist/types/server/operations/ops/backup.d.ts +2 -0
- package/dist/types/server/operations/ops/boards.d.ts +10 -0
- package/dist/types/server/operations/ops/export.d.ts +2 -0
- package/dist/types/server/operations/ops/imports.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +4 -2
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/trace-manager.d.ts +7 -0
- package/dist/types/server/web-artifacts.d.ts +3 -0
- package/dist/types/server/webpage-node.d.ts +22 -1
- package/dist/types/server/workbench-auth.d.ts +3 -0
- package/dist/types/shared/boards.d.ts +6 -0
- package/dist/types/shared/tour.d.ts +72 -0
- package/docs/RELEASE.md +5 -0
- package/docs/cli.md +157 -0
- package/docs/http-api.md +277 -6
- package/docs/mcp.md +96 -7
- package/docs/node-types.md +106 -0
- package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
- package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
- package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
- package/docs/plans/plan-013-static-board-export.md +47 -0
- package/docs/plans/plan-014-connected-board-memory.md +140 -0
- package/docs/product-vision-2026-09.md +118 -18
- package/docs/sdk.md +50 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +62 -5
- package/skills/pmx-canvas/references/full-reference.md +44 -8
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +9 -0
- package/src/cli/commands/ax.ts +13 -0
- package/src/cli/commands/backup.ts +66 -0
- package/src/cli/commands/boards.ts +71 -0
- package/src/cli/commands/export.ts +30 -0
- package/src/cli/commands/nodes.ts +14 -1
- package/src/cli/commands/pins.ts +6 -2
- package/src/cli/commands/pump.ts +3 -0
- package/src/cli/commands/query.ts +33 -7
- package/src/cli/commands/record.ts +218 -0
- package/src/cli/commands/view.ts +7 -5
- package/src/cli/index.ts +21 -9
- package/src/client/App.tsx +28 -4
- package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
- package/src/client/canvas/BoardCopyDialog.tsx +153 -0
- package/src/client/canvas/BoardFolderDialog.tsx +120 -0
- package/src/client/canvas/BoardLinkDialog.tsx +93 -0
- package/src/client/canvas/BoardSwitcher.tsx +149 -0
- package/src/client/canvas/CanvasNode.tsx +8 -0
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +197 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/IntentLayer.tsx +9 -2
- package/src/client/canvas/Presentation.tsx +108 -0
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +49 -7
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/canvas/use-node-drag.ts +8 -3
- package/src/client/icons.tsx +2 -0
- package/src/client/mermaid-entry.ts +39 -0
- package/src/client/nodes/AttachmentNode.tsx +148 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/HtmlNode.tsx +12 -13
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- package/src/client/nodes/McpAppNode.tsx +105 -13
- package/src/client/nodes/MermaidNode.tsx +3 -2
- package/src/client/nodes/ax-node-actions.ts +2 -5
- package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
- package/src/client/state/boards-store.ts +167 -0
- package/src/client/state/canvas-store.ts +22 -0
- package/src/client/state/intent-bridge.ts +30 -2
- package/src/client/state/intent-store.ts +5 -1
- package/src/client/state/node-factory.ts +1 -0
- package/src/client/state/presentation.ts +7 -0
- package/src/client/state/session-store.ts +8 -0
- package/src/client/state/sse-bridge.ts +52 -5
- package/src/client/theme/global.css +498 -28
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +45 -9
- package/src/mcp/canvas-access.ts +59 -22
- package/src/mcp/context-uri-template.ts +28 -0
- package/src/mcp/server.ts +122 -15
- package/src/server/agent-context.ts +116 -29
- package/src/server/agent-presence.ts +15 -9
- package/src/server/attribution.ts +29 -0
- package/src/server/ax-context.ts +4 -2
- package/src/server/ax-interaction.ts +1 -0
- package/src/server/ax-state-manager.ts +72 -3
- package/src/server/ax-state.ts +41 -0
- package/src/server/backup.ts +178 -0
- package/src/server/board-export.ts +686 -0
- package/src/server/canvas-db.ts +612 -105
- package/src/server/canvas-operations.ts +183 -31
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +22 -0
- package/src/server/canvas-state.ts +812 -48
- package/src/server/canvas-validation.ts +9 -1
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +670 -9
- package/src/server/document-import.ts +223 -0
- package/src/server/html-primitives.ts +199 -91
- package/src/server/html-surface.ts +8 -3
- package/src/server/index.ts +243 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +29 -5
- package/src/server/operations/http.ts +9 -3
- package/src/server/operations/index.ts +8 -0
- package/src/server/operations/invoker.ts +14 -3
- package/src/server/operations/mcp.ts +50 -11
- package/src/server/operations/ops/app.ts +15 -0
- package/src/server/operations/ops/ax-read.ts +10 -6
- package/src/server/operations/ops/ax-timeline.ts +75 -0
- package/src/server/operations/ops/backup.ts +134 -0
- package/src/server/operations/ops/batch.ts +20 -4
- package/src/server/operations/ops/boards.ts +351 -0
- package/src/server/operations/ops/export.ts +74 -0
- package/src/server/operations/ops/ext-app.ts +4 -0
- package/src/server/operations/ops/groups.ts +14 -7
- package/src/server/operations/ops/imports.ts +220 -0
- package/src/server/operations/ops/json-render.ts +37 -5
- package/src/server/operations/ops/nodes.ts +99 -21
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/ops/viewport.ts +32 -0
- package/src/server/operations/registry.ts +129 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +155 -8
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +11 -3
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/server/workboard.ts +3 -3
- package/src/shared/boards.ts +18 -0
- package/src/shared/node-sizes.ts +1 -0
- package/src/shared/tour.ts +87 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { signal } from '@preact/signals';
|
|
2
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
3
|
+
import { requestJson } from '../state/intent-bridge';
|
|
4
|
+
import { activeBoardId } from '../state/boards-store';
|
|
5
|
+
|
|
6
|
+
interface ExportManifest {
|
|
7
|
+
boardId: string;
|
|
8
|
+
boardName: string;
|
|
9
|
+
cards: number;
|
|
10
|
+
connections: number;
|
|
11
|
+
frames: number;
|
|
12
|
+
placeholders: Array<{ nodeId: string; title: string; reason: string }>;
|
|
13
|
+
files: Array<{ nodeId: string; path: string; included: boolean }>;
|
|
14
|
+
attachments: Array<{ nodeId: string; name: string }>;
|
|
15
|
+
sourceDerivedCards: Array<{ nodeId: string; included: boolean }>;
|
|
16
|
+
embeddedImages: number;
|
|
17
|
+
remoteImages: string[];
|
|
18
|
+
links: string[];
|
|
19
|
+
frameNetworkDestinations: string[];
|
|
20
|
+
embeddedCodeCanAccessNetwork: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ExportResult {
|
|
24
|
+
path: string;
|
|
25
|
+
url: string;
|
|
26
|
+
bytes: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const exportDialogOpen = signal(false);
|
|
30
|
+
|
|
31
|
+
function sizeLabel(bytes: number): string {
|
|
32
|
+
return bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${Math.ceil(bytes / 1024)} KB`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Export the open board as one HTML file (plan 013). An export is a share, so
|
|
37
|
+
* the dialog lists what goes into the file before it is written; file contents
|
|
38
|
+
* stay out unless the owner ticks them in.
|
|
39
|
+
*/
|
|
40
|
+
export function ExportDialog() {
|
|
41
|
+
const [includeFiles, setIncludeFiles] = useState(false);
|
|
42
|
+
const [includeDerivedText, setIncludeDerivedText] = useState(false);
|
|
43
|
+
const [manifest, setManifest] = useState<ExportManifest | null>(null);
|
|
44
|
+
const [result, setResult] = useState<ExportResult | null>(null);
|
|
45
|
+
const [busy, setBusy] = useState(false);
|
|
46
|
+
const open = exportDialogOpen.value;
|
|
47
|
+
const boardId = activeBoardId.value;
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!open || !boardId) return;
|
|
51
|
+
let current = true;
|
|
52
|
+
setResult(null);
|
|
53
|
+
setManifest(null);
|
|
54
|
+
void requestJson<{ manifest?: ExportManifest } | null>(
|
|
55
|
+
'exportPreview',
|
|
56
|
+
`/api/canvas/export/preview?board=${encodeURIComponent(boardId)}&includeFiles=${includeFiles}&includeDerivedText=${includeDerivedText}`,
|
|
57
|
+
null,
|
|
58
|
+
).then((body) => {
|
|
59
|
+
if (current) setManifest(body?.manifest ?? null);
|
|
60
|
+
});
|
|
61
|
+
return () => {
|
|
62
|
+
current = false;
|
|
63
|
+
};
|
|
64
|
+
}, [open, includeFiles, includeDerivedText, boardId]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
setIncludeDerivedText(false);
|
|
68
|
+
}, [open, boardId]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!open) return;
|
|
72
|
+
const onKey = (e: KeyboardEvent) => {
|
|
73
|
+
if (e.key !== 'Escape') return;
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
exportDialogOpen.value = false;
|
|
76
|
+
};
|
|
77
|
+
window.addEventListener('keydown', onKey, { capture: true });
|
|
78
|
+
return () => window.removeEventListener('keydown', onKey, { capture: true });
|
|
79
|
+
}, [open]);
|
|
80
|
+
|
|
81
|
+
if (!open) return null;
|
|
82
|
+
|
|
83
|
+
const write = async () => {
|
|
84
|
+
if (!manifest || manifest.boardId !== activeBoardId.value) return;
|
|
85
|
+
setBusy(true);
|
|
86
|
+
const body = await requestJson<ExportResult | null>('exportBoard', '/api/canvas/export', null, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ board: manifest.boardId, includeFiles, includeDerivedText }),
|
|
90
|
+
});
|
|
91
|
+
setBusy(false);
|
|
92
|
+
if (body?.path && activeBoardId.value === manifest.boardId) setResult(body);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div class="text-prompt-backdrop" onPointerDown={() => (exportDialogOpen.value = false)}>
|
|
97
|
+
<div
|
|
98
|
+
class="text-prompt export-dialog"
|
|
99
|
+
role="dialog"
|
|
100
|
+
aria-label="Export board"
|
|
101
|
+
data-testid="export-dialog"
|
|
102
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
103
|
+
>
|
|
104
|
+
<div class="text-prompt-title">Export {manifest ? `“${manifest.boardName}”` : 'board'}</div>
|
|
105
|
+
{!manifest ? (
|
|
106
|
+
<div class="export-dialog-note">Reading the board…</div>
|
|
107
|
+
) : (
|
|
108
|
+
<>
|
|
109
|
+
<div class="export-dialog-note">
|
|
110
|
+
One HTML file anyone can open without installing anything. It will contain:
|
|
111
|
+
</div>
|
|
112
|
+
<ul class="export-dialog-list">
|
|
113
|
+
<li>
|
|
114
|
+
{manifest.cards} card{manifest.cards !== 1 ? 's' : ''} and {manifest.connections} connection
|
|
115
|
+
{manifest.connections !== 1 ? 's' : ''}
|
|
116
|
+
</li>
|
|
117
|
+
{manifest.frames > 0 && (
|
|
118
|
+
<li>{manifest.frames} embedded pages, charts or diagrams, with the code that draws them</li>
|
|
119
|
+
)}
|
|
120
|
+
{manifest.embeddedImages > 0 && <li>{manifest.embeddedImages} local images</li>}
|
|
121
|
+
{manifest.placeholders.length > 0 && (
|
|
122
|
+
<li>
|
|
123
|
+
Shown as placeholders:{' '}
|
|
124
|
+
{manifest.placeholders.map((entry) => `${entry.title} (${entry.reason})`).join(', ')}
|
|
125
|
+
</li>
|
|
126
|
+
)}
|
|
127
|
+
{manifest.remoteImages.length > 0 && (
|
|
128
|
+
<li>Web images loaded when opened: {manifest.remoteImages.join(', ')}</li>
|
|
129
|
+
)}
|
|
130
|
+
{manifest.links.length > 0 && <li>Links included in cards: {manifest.links.join(', ')}</li>}
|
|
131
|
+
{manifest.frameNetworkDestinations.length > 0 && (
|
|
132
|
+
<li>
|
|
133
|
+
Network destinations referenced by embedded pages: {manifest.frameNetworkDestinations.join(', ')}
|
|
134
|
+
</li>
|
|
135
|
+
)}
|
|
136
|
+
{manifest.embeddedCodeCanAccessNetwork && (
|
|
137
|
+
<li>
|
|
138
|
+
Warning: embedded code can access the network. Scripts may contact destinations that cannot be listed
|
|
139
|
+
in advance.
|
|
140
|
+
</li>
|
|
141
|
+
)}
|
|
142
|
+
</ul>
|
|
143
|
+
{(manifest.attachments?.length ?? 0) > 0 && (
|
|
144
|
+
<p>Original attachments are excluded. Their filenames remain visible.</p>
|
|
145
|
+
)}
|
|
146
|
+
{(manifest.sourceDerivedCards?.length ?? 0) > 0 && (
|
|
147
|
+
<label class="export-dialog-files">
|
|
148
|
+
<input
|
|
149
|
+
type="checkbox"
|
|
150
|
+
checked={includeDerivedText}
|
|
151
|
+
onChange={(event) => setIncludeDerivedText(event.currentTarget.checked)}
|
|
152
|
+
/>
|
|
153
|
+
<span>Include imported document text ({manifest.sourceDerivedCards.length} cards)</span>
|
|
154
|
+
</label>
|
|
155
|
+
)}
|
|
156
|
+
{manifest.files.length > 0 && (
|
|
157
|
+
<label class="export-dialog-files">
|
|
158
|
+
<input
|
|
159
|
+
type="checkbox"
|
|
160
|
+
checked={includeFiles}
|
|
161
|
+
onChange={(e) => setIncludeFiles((e.target as HTMLInputElement).checked)}
|
|
162
|
+
/>
|
|
163
|
+
<span>
|
|
164
|
+
Include the contents of {manifest.files.length} local file
|
|
165
|
+
{manifest.files.length !== 1 ? 's' : ''}: {manifest.files.map((file) => file.path).join(', ')}
|
|
166
|
+
</span>
|
|
167
|
+
</label>
|
|
168
|
+
)}
|
|
169
|
+
</>
|
|
170
|
+
)}
|
|
171
|
+
{result && (
|
|
172
|
+
<div class="export-dialog-result" data-testid="export-result">
|
|
173
|
+
<div>
|
|
174
|
+
Saved ({sizeLabel(result.bytes)}): <code>{result.path}</code>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="export-dialog-links">
|
|
177
|
+
<a href={result.url} target="_blank" rel="noopener noreferrer">
|
|
178
|
+
Open
|
|
179
|
+
</a>
|
|
180
|
+
<a href={`${result.url}?download=1`}>Download</a>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
<div class="text-prompt-actions">
|
|
185
|
+
<button type="button" class="text-prompt-cancel" onClick={() => (exportDialogOpen.value = false)}>
|
|
186
|
+
{result ? 'Done' : 'Cancel'}
|
|
187
|
+
</button>
|
|
188
|
+
{!result && (
|
|
189
|
+
<button type="button" class="text-prompt-confirm" disabled={!manifest || busy} onClick={() => void write()}>
|
|
190
|
+
{busy ? 'Exporting…' : 'Export'}
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import type { ComponentChildren } from 'preact';
|
|
3
|
+
import {
|
|
4
|
+
boardList,
|
|
5
|
+
activeBoardId,
|
|
6
|
+
createAndOpenBoard,
|
|
7
|
+
deleteBoard,
|
|
8
|
+
openBoard,
|
|
9
|
+
updateBoard,
|
|
10
|
+
type BoardSummary,
|
|
11
|
+
} from '../state/boards-store';
|
|
12
|
+
import { categoryAncestors, MIGRATED_SNAPSHOT_CATEGORY } from '../../shared/boards.js';
|
|
13
|
+
import { askText } from './TextPrompt';
|
|
14
|
+
import { BoardFolderDialog } from './BoardFolderDialog';
|
|
15
|
+
import { requestJson } from '../state/intent-bridge';
|
|
16
|
+
import { focusNode, nodes } from '../state/canvas-store';
|
|
17
|
+
import { BoardCopyDialog } from './BoardCopyDialog';
|
|
18
|
+
|
|
19
|
+
interface LibraryHit {
|
|
20
|
+
boardId: string;
|
|
21
|
+
boardTitle: string;
|
|
22
|
+
cardId: string;
|
|
23
|
+
title: string | null;
|
|
24
|
+
snippet: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function timeAgo(iso: string | null): string {
|
|
28
|
+
if (!iso) return 'never opened';
|
|
29
|
+
const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
|
|
30
|
+
if (mins < 1) return 'just now';
|
|
31
|
+
if (mins < 60) return `${mins}m ago`;
|
|
32
|
+
const hrs = Math.floor(mins / 60);
|
|
33
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
34
|
+
const days = Math.floor(hrs / 24);
|
|
35
|
+
if (days < 30) return `${days}d ago`;
|
|
36
|
+
return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function nodesLabel(count: number): string {
|
|
40
|
+
return `${count} node${count !== 1 ? 's' : ''}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function promptNewBoard(): Promise<void> {
|
|
44
|
+
const name = await askText('New board', 'Board name', { confirm: 'Create' });
|
|
45
|
+
if (name) await createAndOpenBoard(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const COLLAPSED_KEY = 'pmx-canvas-home-collapsed';
|
|
49
|
+
|
|
50
|
+
/** Which categories this viewer folded away; the migrated-snapshot shelf starts folded. */
|
|
51
|
+
function readCollapsed(): Set<string> {
|
|
52
|
+
try {
|
|
53
|
+
const raw = localStorage.getItem(COLLAPSED_KEY);
|
|
54
|
+
if (raw) return new Set(JSON.parse(raw) as string[]);
|
|
55
|
+
} catch {
|
|
56
|
+
// Storage can be unavailable (private windows, embedded panes): fall back to the default.
|
|
57
|
+
}
|
|
58
|
+
return new Set([MIGRATED_SNAPSHOT_CATEGORY]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeCollapsed(collapsed: Set<string>): void {
|
|
62
|
+
try {
|
|
63
|
+
localStorage.setItem(COLLAPSED_KEY, JSON.stringify([...collapsed]));
|
|
64
|
+
} catch {
|
|
65
|
+
// Folding still works for this visit without storage.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Home (plan 012): the view when no board is open — every board, filed under
|
|
71
|
+
* its category (most recently opened first within each). Categories keep the
|
|
72
|
+
* library findable as it grows into the working memory of many sessions.
|
|
73
|
+
* Deleting asks in place, naming the board and what it holds; browser confirm
|
|
74
|
+
* dialogs are no-ops in agent panes.
|
|
75
|
+
*/
|
|
76
|
+
export function HomeView() {
|
|
77
|
+
const boards = boardList.value;
|
|
78
|
+
const [confirmingDelete, setConfirmingDelete] = useState<string | null>(null);
|
|
79
|
+
const [moving, setMoving] = useState<string | null>(null);
|
|
80
|
+
const [copying, setCopying] = useState<string | null>(null);
|
|
81
|
+
const [search, setSearch] = useState('');
|
|
82
|
+
const [hits, setHits] = useState<LibraryHit[]>([]);
|
|
83
|
+
const [collapsed, setCollapsed] = useState<Set<string>>(readCollapsed);
|
|
84
|
+
const openHitSequence = useRef(0);
|
|
85
|
+
const folders = [
|
|
86
|
+
...new Set(boards.flatMap((board) => (board.category ? categoryAncestors(board.category) : []))),
|
|
87
|
+
].sort((a, b) => a.localeCompare(b));
|
|
88
|
+
const query = search.trim().toLocaleLowerCase();
|
|
89
|
+
const visible = boards.filter((board) => `${board.name} ${board.category ?? ''}`.toLocaleLowerCase().includes(query));
|
|
90
|
+
const visibleFolders = new Set(visible.flatMap((board) => (board.category ? categoryAncestors(board.category) : [])));
|
|
91
|
+
const movingBoard = boards.find((board) => board.id === moving);
|
|
92
|
+
const copyingBoard = boards.find((board) => board.id === copying);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const q = search.trim();
|
|
96
|
+
if (!q) {
|
|
97
|
+
setHits([]);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let cancelled = false;
|
|
101
|
+
const timer = window.setTimeout(() => {
|
|
102
|
+
void requestJson<{ results?: LibraryHit[] } | null>(
|
|
103
|
+
'searchLibrary',
|
|
104
|
+
`/api/canvas/search?q=${encodeURIComponent(q)}&scope=library&limit=30`,
|
|
105
|
+
null,
|
|
106
|
+
).then((result) => {
|
|
107
|
+
if (!cancelled) setHits(result?.results ?? []);
|
|
108
|
+
});
|
|
109
|
+
}, 150);
|
|
110
|
+
return () => {
|
|
111
|
+
cancelled = true;
|
|
112
|
+
window.clearTimeout(timer);
|
|
113
|
+
};
|
|
114
|
+
}, [search]);
|
|
115
|
+
|
|
116
|
+
const openHit = async (hit: LibraryHit) => {
|
|
117
|
+
const sequence = ++openHitSequence.current;
|
|
118
|
+
await openBoard(hit.boardId);
|
|
119
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
120
|
+
if (sequence !== openHitSequence.current || activeBoardId.value !== hit.boardId) return;
|
|
121
|
+
if (nodes.value.has(hit.cardId)) {
|
|
122
|
+
focusNode(hit.cardId);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
await new Promise((resolve) => window.setTimeout(resolve, 25));
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const toggle = (name: string) => {
|
|
130
|
+
const next = new Set(collapsed);
|
|
131
|
+
if (next.has(name)) next.delete(name);
|
|
132
|
+
else next.add(name);
|
|
133
|
+
setCollapsed(next);
|
|
134
|
+
writeCollapsed(next);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const rename = async (board: BoardSummary) => {
|
|
138
|
+
const name = await askText('Rename board', 'Board name', { initial: board.name, confirm: 'Rename' });
|
|
139
|
+
if (name && name !== board.name) await updateBoard(board.id, { name });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const row = (board: BoardSummary) =>
|
|
143
|
+
confirmingDelete === board.id ? (
|
|
144
|
+
<li key={board.id} class="home-board is-confirming" data-testid="home-board">
|
|
145
|
+
<div class="home-board-confirm-text">
|
|
146
|
+
Delete <strong>{board.name}</strong> and its {nodesLabel(board.nodeCount)} and snapshots? This can't be
|
|
147
|
+
undone.
|
|
148
|
+
</div>
|
|
149
|
+
<div class="home-board-actions">
|
|
150
|
+
<button type="button" class="home-board-action" onClick={() => setConfirmingDelete(null)}>
|
|
151
|
+
Cancel
|
|
152
|
+
</button>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
class="home-board-action is-danger"
|
|
156
|
+
onClick={() => {
|
|
157
|
+
setConfirmingDelete(null);
|
|
158
|
+
void deleteBoard(board.id);
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
Delete board
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
</li>
|
|
165
|
+
) : (
|
|
166
|
+
<li key={board.id} class="home-board" data-testid="home-board">
|
|
167
|
+
<button type="button" class="home-board-open" onClick={() => void openBoard(board.id)}>
|
|
168
|
+
<span class="home-board-name">{board.name}</span>
|
|
169
|
+
<span class="home-board-meta">
|
|
170
|
+
{nodesLabel(board.nodeCount)} · {timeAgo(board.lastOpenedAt)}
|
|
171
|
+
</span>
|
|
172
|
+
</button>
|
|
173
|
+
<div class="home-board-actions">
|
|
174
|
+
<button type="button" class="home-board-action" aria-haspopup="dialog" onClick={() => setMoving(board.id)}>
|
|
175
|
+
Move
|
|
176
|
+
</button>
|
|
177
|
+
<button type="button" class="home-board-action" aria-haspopup="dialog" onClick={() => setCopying(board.id)}>
|
|
178
|
+
Create from…
|
|
179
|
+
</button>
|
|
180
|
+
<button type="button" class="home-board-action" onClick={() => void rename(board)}>
|
|
181
|
+
Rename
|
|
182
|
+
</button>
|
|
183
|
+
<button type="button" class="home-board-action" onClick={() => setConfirmingDelete(board.id)}>
|
|
184
|
+
Delete
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</li>
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const folder = (path: string, depth: number): ComponentChildren => {
|
|
191
|
+
const children = folders.filter(
|
|
192
|
+
(candidate) => candidate.slice(0, candidate.lastIndexOf('/')) === path && candidate.includes('/'),
|
|
193
|
+
);
|
|
194
|
+
const direct = visible.filter((board) => board.category === path);
|
|
195
|
+
const count = visible.filter((board) => board.category === path || board.category?.startsWith(`${path}/`)).length;
|
|
196
|
+
const expanded = !!query || !collapsed.has(path);
|
|
197
|
+
return (
|
|
198
|
+
<section key={path} class="home-section" data-testid="home-section" data-folder={path} aria-label={path}>
|
|
199
|
+
<button type="button" class="home-section-head" aria-expanded={expanded} onClick={() => toggle(path)}>
|
|
200
|
+
<svg class="home-section-chevron" viewBox="0 0 20 20" width="18" height="18" fill="none" aria-hidden="true">
|
|
201
|
+
<path d="m7.5 4.75 5.25 5.25-5.25 5.25" />
|
|
202
|
+
</svg>
|
|
203
|
+
<svg class="home-section-folder" viewBox="0 0 20 20" width="18" height="18" fill="none" aria-hidden="true">
|
|
204
|
+
<path d="M2.75 5.75A1.75 1.75 0 0 1 4.5 4h3l1.75 2h6.25a1.75 1.75 0 0 1 1.75 1.75v6.5A1.75 1.75 0 0 1 15.5 16h-11a1.75 1.75 0 0 1-1.75-1.75v-8.5Z" />
|
|
205
|
+
</svg>
|
|
206
|
+
<span class="home-section-name">{path.split('/').at(-1)}</span>
|
|
207
|
+
<span class="home-section-count">{count}</span>
|
|
208
|
+
</button>
|
|
209
|
+
{expanded && (
|
|
210
|
+
<div class="home-folder-children" style={{ marginLeft: depth < 5 ? '14px' : '0' }}>
|
|
211
|
+
{children.filter((child) => visibleFolders.has(child)).map((child) => folder(child, depth + 1))}
|
|
212
|
+
{direct.length > 0 && (
|
|
213
|
+
<ul class="home-view-list" aria-label={`Boards in ${path}`}>
|
|
214
|
+
{direct.map(row)}
|
|
215
|
+
</ul>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</section>
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div class="home-view" data-testid="home-view">
|
|
225
|
+
<div class="home-view-inner">
|
|
226
|
+
<div class="home-view-head">
|
|
227
|
+
<h1 class="home-view-title">Boards</h1>
|
|
228
|
+
<button type="button" class="home-view-new" onClick={() => void promptNewBoard()}>
|
|
229
|
+
New board
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
<input
|
|
233
|
+
class="text-prompt-input home-search"
|
|
234
|
+
type="search"
|
|
235
|
+
aria-label="Find boards and folders"
|
|
236
|
+
placeholder="Find boards and folders…"
|
|
237
|
+
value={search}
|
|
238
|
+
onInput={(event) => setSearch(event.currentTarget.value)}
|
|
239
|
+
/>
|
|
240
|
+
{boards.length === 0 ? (
|
|
241
|
+
<div class="home-view-empty">No boards yet. Create one, or let an agent start writing — it opens one.</div>
|
|
242
|
+
) : query && hits.length > 0 ? (
|
|
243
|
+
<ul class="home-view-list" aria-label="Library search results">
|
|
244
|
+
{hits.map((hit) => (
|
|
245
|
+
<li class="home-board" key={`${hit.boardId}:${hit.cardId}`}>
|
|
246
|
+
<button type="button" class="home-board-open" onClick={() => void openHit(hit)}>
|
|
247
|
+
<span class="home-board-name">{hit.title || hit.boardTitle}</span>
|
|
248
|
+
<span class="home-board-meta">
|
|
249
|
+
{hit.boardTitle} · {hit.snippet}
|
|
250
|
+
</span>
|
|
251
|
+
</button>
|
|
252
|
+
</li>
|
|
253
|
+
))}
|
|
254
|
+
</ul>
|
|
255
|
+
) : visible.length === 0 ? (
|
|
256
|
+
<div class="home-view-empty" role="status">
|
|
257
|
+
No boards match “{search}”.
|
|
258
|
+
</div>
|
|
259
|
+
) : (
|
|
260
|
+
<>
|
|
261
|
+
{folders.filter((path) => !path.includes('/') && visibleFolders.has(path)).map((path) => folder(path, 0))}
|
|
262
|
+
{visible.some((board) => !board.category) && (
|
|
263
|
+
<section aria-label="Unfiled boards">
|
|
264
|
+
{folders.length > 0 && <div class="home-folder-hint">Unfiled boards</div>}
|
|
265
|
+
<ul class="home-view-list">{visible.filter((board) => !board.category).map(row)}</ul>
|
|
266
|
+
</section>
|
|
267
|
+
)}
|
|
268
|
+
</>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
{movingBoard && (
|
|
272
|
+
<BoardFolderDialog key={movingBoard.id} board={movingBoard} folders={folders} onClose={() => setMoving(null)} />
|
|
273
|
+
)}
|
|
274
|
+
{copyingBoard && <BoardCopyDialog key={copyingBoard.id} board={copyingBoard} onClose={() => setCopying(null)} />}
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect } from 'preact/hooks';
|
|
2
2
|
import { nodes } from '../state/canvas-store';
|
|
3
|
-
import { hoveredIntentId, intents, type ClientIntent } from '../state/intent-store';
|
|
3
|
+
import { hoveredIntentId, intents, removeIntent, type ClientIntent } from '../state/intent-store';
|
|
4
4
|
import { vetoGhostIntent } from '../state/intent-bridge';
|
|
5
5
|
import { getNodeIcon } from '../icons';
|
|
6
6
|
import { TYPE_LABELS } from '../types';
|
|
@@ -241,7 +241,14 @@ export function IntentLayer() {
|
|
|
241
241
|
if (list.length === 0) return null;
|
|
242
242
|
|
|
243
243
|
return (
|
|
244
|
-
<div
|
|
244
|
+
<div
|
|
245
|
+
class="intent-layer"
|
|
246
|
+
onAnimationEnd={(event) => {
|
|
247
|
+
if (event.animationName !== 'intent-settle' && event.animationName !== 'intent-dissolve') return;
|
|
248
|
+
const id = (event.target as HTMLElement).dataset.intentId;
|
|
249
|
+
if (id && intents.value.get(id)?.phase !== 'forming') removeIntent(id);
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
245
252
|
<svg
|
|
246
253
|
class="intent-line-layer"
|
|
247
254
|
style={{
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
|
+
import { cameraSchema, interpolateCamera, resolveStop, type Camera, type Tour } from '../../shared/tour.js';
|
|
3
|
+
import { canvasArea } from './canvas-area';
|
|
4
|
+
import { cancelViewportAnimation, hasInitialServerLayout, nodes, viewport } from '../state/canvas-store';
|
|
5
|
+
import { presenting, recordingCamera } from '../state/presentation';
|
|
6
|
+
import { requestJson } from '../state/intent-bridge';
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
pmxCapture?: {
|
|
11
|
+
ready: () => boolean;
|
|
12
|
+
area: typeof canvasArea;
|
|
13
|
+
camera: () => Camera;
|
|
14
|
+
frame: (camera: Camera) => Promise<Camera>;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Presentation() {
|
|
20
|
+
const [tour, setTour] = useState<Tour>({ stops: [] });
|
|
21
|
+
const [index, setIndex] = useState(0);
|
|
22
|
+
const [error, setError] = useState('');
|
|
23
|
+
const active = presenting.value;
|
|
24
|
+
const ready = hasInitialServerLayout.value;
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const params = new URLSearchParams(location.search);
|
|
27
|
+
presenting.value = params.get('present') === '1';
|
|
28
|
+
recordingCamera.value = params.get('capture') === '1';
|
|
29
|
+
window.pmxCapture = {
|
|
30
|
+
ready: () => hasInitialServerLayout.value,
|
|
31
|
+
area: canvasArea,
|
|
32
|
+
camera: () => ({ ...viewport.value }),
|
|
33
|
+
frame: async (camera) => {
|
|
34
|
+
recordingCamera.value = true;
|
|
35
|
+
cancelViewportAnimation();
|
|
36
|
+
viewport.value = cameraSchema.parse(camera);
|
|
37
|
+
// Two animation frames: Preact has committed and the browser has had
|
|
38
|
+
// a paint opportunity before the screenshot command is acknowledged.
|
|
39
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
40
|
+
return { ...viewport.value };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return () => {
|
|
44
|
+
delete window.pmxCapture;
|
|
45
|
+
presenting.value = false;
|
|
46
|
+
recordingCamera.value = false;
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!active || !ready) return;
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
void requestJson('read tour', '/api/canvas/tour', { tour: { stops: [] } as Tour }).then((result) => {
|
|
53
|
+
if (!cancelled) {
|
|
54
|
+
setTour(result.tour);
|
|
55
|
+
setIndex(0);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return () => {
|
|
59
|
+
cancelled = true;
|
|
60
|
+
};
|
|
61
|
+
}, [active, ready]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!active || recordingCamera.value) return;
|
|
64
|
+
const stop = tour.stops[index];
|
|
65
|
+
if (!stop) return;
|
|
66
|
+
const area = canvasArea();
|
|
67
|
+
let target: Camera;
|
|
68
|
+
try {
|
|
69
|
+
target = resolveStop(stop, [...nodes.value.values()], area.width, area.height);
|
|
70
|
+
setError('');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
cancelViewportAnimation();
|
|
76
|
+
const from = { ...viewport.value };
|
|
77
|
+
const start = performance.now();
|
|
78
|
+
const duration = (stop.duration ?? 1) * 1000;
|
|
79
|
+
let frame = 0;
|
|
80
|
+
const tick = (now: number) => {
|
|
81
|
+
if (recordingCamera.value) return;
|
|
82
|
+
const t = duration === 0 ? 1 : Math.min(1, (now - start) / duration);
|
|
83
|
+
viewport.value = interpolateCamera(from, target, t, area.width, area.height, stop.easing, stop.pullback);
|
|
84
|
+
if (t < 1) frame = requestAnimationFrame(tick);
|
|
85
|
+
};
|
|
86
|
+
frame = requestAnimationFrame(tick);
|
|
87
|
+
return () => cancelAnimationFrame(frame);
|
|
88
|
+
}, [active, index, tour]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!active) return;
|
|
91
|
+
const key = (event: KeyboardEvent) => {
|
|
92
|
+
event.stopImmediatePropagation();
|
|
93
|
+
if (['Escape', 'ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp', ' '].includes(event.key))
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
if (event.key === 'Escape') presenting.value = false;
|
|
96
|
+
if (['ArrowRight', 'ArrowDown', ' '].includes(event.key))
|
|
97
|
+
setIndex((i) => Math.max(0, Math.min(tour.stops.length - 1, i + 1)));
|
|
98
|
+
if (['ArrowLeft', 'ArrowUp'].includes(event.key)) setIndex((i) => Math.max(0, i - 1));
|
|
99
|
+
};
|
|
100
|
+
document.addEventListener('keydown', key, true);
|
|
101
|
+
return () => document.removeEventListener('keydown', key, true);
|
|
102
|
+
}, [active, tour]);
|
|
103
|
+
return active && error ? (
|
|
104
|
+
<div class="presentation-error" role="alert">
|
|
105
|
+
{error}
|
|
106
|
+
</div>
|
|
107
|
+
) : null;
|
|
108
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ApprovalGateView,
|
|
9
9
|
endSession,
|
|
10
10
|
heldGates,
|
|
11
|
+
resolvedGates,
|
|
11
12
|
pendingGates,
|
|
12
13
|
refreshTimeline,
|
|
13
14
|
reopenGate,
|
|
@@ -195,6 +196,28 @@ function HeldGateRow({ gate }: { gate: ApprovalGateView }) {
|
|
|
195
196
|
);
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
function answerLabel(gate: ApprovalGateView): string {
|
|
200
|
+
const by = gate.resolvedBy;
|
|
201
|
+
if (!by) return gate.status;
|
|
202
|
+
const decision = gate.status === 'approved' ? 'Approved' : 'Rejected';
|
|
203
|
+
if (by.actor === 'human') return `${decision} by you`;
|
|
204
|
+
if (by.actor === 'system') return `${decision} by system`;
|
|
205
|
+
const name = by.agentId || by.source || 'agent';
|
|
206
|
+
return `${gate.selfAnswer ? 'Self-' : ''}${decision.toLowerCase()} by ${name}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function ResolvedGateRow({ gate }: { gate: ApprovalGateView }) {
|
|
210
|
+
return (
|
|
211
|
+
<li class={`session-item status-${gate.status}`} data-gate-id={gate.id}>
|
|
212
|
+
<span class="session-glyph" aria-hidden="true" />
|
|
213
|
+
<div class="session-item-main">
|
|
214
|
+
<span class="session-item-title">{gate.title}</span>
|
|
215
|
+
<div class="session-item-detail">{answerLabel(gate)}</div>
|
|
216
|
+
</div>
|
|
217
|
+
</li>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
198
221
|
/**
|
|
199
222
|
* Scope row under the header: the fence the human granted, or the affordance
|
|
200
223
|
* to grant one from the current selection. The server refuses agent writes
|
|
@@ -296,6 +319,7 @@ export function SessionPanel() {
|
|
|
296
319
|
const items = sessionWorkItems.value;
|
|
297
320
|
const gates = pendingGates.value;
|
|
298
321
|
const held = heldGates.value;
|
|
322
|
+
const resolved = resolvedGates.value;
|
|
299
323
|
const entries = timelineEntries.value;
|
|
300
324
|
const [workOpen, setWorkOpen] = useState(false);
|
|
301
325
|
// Which filter chip the pointer/focus is on — drives the caption under the
|
|
@@ -435,7 +459,7 @@ export function SessionPanel() {
|
|
|
435
459
|
</span>
|
|
436
460
|
</button>
|
|
437
461
|
{showList &&
|
|
438
|
-
(gates.length === 0 && held.length === 0 && items.length === 0 ? (
|
|
462
|
+
(gates.length === 0 && held.length === 0 && resolved.length === 0 && items.length === 0 ? (
|
|
439
463
|
<div class="session-empty">No work items yet — the agent's tasks and gates appear here.</div>
|
|
440
464
|
) : (
|
|
441
465
|
<ul class="session-list" aria-live="polite" aria-label="Work items and gates">
|
|
@@ -445,6 +469,9 @@ export function SessionPanel() {
|
|
|
445
469
|
{held.map((gate) => (
|
|
446
470
|
<HeldGateRow key={gate.id} gate={gate} />
|
|
447
471
|
))}
|
|
472
|
+
{resolved.map((gate) => (
|
|
473
|
+
<ResolvedGateRow key={gate.id} gate={gate} />
|
|
474
|
+
))}
|
|
448
475
|
{items.map((item) => (
|
|
449
476
|
<WorkItemRow key={item.id} item={item} />
|
|
450
477
|
))}
|