pmx-canvas 0.6.5 → 0.7.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 +139 -14
- package/CHANGELOG.md +70 -0
- package/Readme.md +32 -6
- package/dist/canvas/global.css +510 -31
- package/dist/canvas/index.js +92 -77
- package/dist/types/cli/agent.d.ts +3 -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/daemon.d.ts +5 -5
- package/dist/types/cli/shared.d.ts +3 -1
- 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/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- 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/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +1 -1
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +6 -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 +63 -9
- package/dist/types/server/canvas-operations.d.ts +23 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +140 -3
- 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/index.d.ts +103 -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/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/nodes.d.ts +1 -1
- 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/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/docs/RELEASE.md +5 -0
- package/docs/cli.md +73 -0
- package/docs/http-api.md +198 -2
- package/docs/mcp.md +71 -6
- package/docs/node-types.md +26 -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 +68 -11
- package/skills/pmx-canvas/evals/evals.json +130 -0
- package/skills/pmx-canvas/references/full-reference.md +43 -7
- 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/references/tours-and-recording.md +69 -0
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +11 -2
- package/src/cli/agent.ts +4 -0
- package/src/cli/commands/apps.ts +1 -1
- 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 +37 -0
- package/src/cli/commands/nodes.ts +4 -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/view.ts +7 -5
- package/src/cli/daemon.ts +37 -7
- package/src/cli/index.ts +19 -9
- package/src/cli/shared.ts +11 -2
- package/src/client/App.tsx +21 -4
- package/src/client/canvas/AttentionToast.tsx +5 -3
- 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 +0 -1
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +199 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/Presentation.tsx +39 -9
- package/src/client/canvas/SelectionBar.tsx +0 -2
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +34 -18
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/icons.tsx +2 -0
- package/src/client/nodes/AttachmentNode.tsx +145 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- 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/attention-bridge.ts +3 -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/node-factory.ts +1 -0
- package/src/client/state/session-store.ts +5 -0
- package/src/client/state/sse-bridge.ts +52 -6
- package/src/client/theme/global.css +510 -31
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +16 -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/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 +599 -107
- package/src/server/canvas-operations.ts +68 -9
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +12 -0
- package/src/server/canvas-state.ts +774 -45
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +659 -2
- package/src/server/document-import.ts +223 -0
- package/src/server/index.ts +231 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +26 -4
- 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 +5 -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 +8 -1
- 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/imports.ts +236 -0
- package/src/server/operations/ops/nodes.ts +57 -5
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/registry.ts +123 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/server.ts +146 -2
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +2 -1
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/boards.ts +18 -0
- package/src/shared/node-sizes.ts +1 -0
|
@@ -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,9 +1,10 @@
|
|
|
1
|
-
import { useEffect, useState } from 'preact/hooks';
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { cameraSchema, interpolateCamera, resolveStop, type Camera, type Tour } from '../../shared/tour.js';
|
|
3
3
|
import { canvasArea } from './canvas-area';
|
|
4
4
|
import { cancelViewportAnimation, hasInitialServerLayout, nodes, viewport } from '../state/canvas-store';
|
|
5
5
|
import { presenting, recordingCamera } from '../state/presentation';
|
|
6
6
|
import { requestJson } from '../state/intent-bridge';
|
|
7
|
+
import { usePanZoom } from './use-pan-zoom';
|
|
7
8
|
|
|
8
9
|
declare global {
|
|
9
10
|
interface Window {
|
|
@@ -22,6 +23,17 @@ export function Presentation() {
|
|
|
22
23
|
const [error, setError] = useState('');
|
|
23
24
|
const active = presenting.value;
|
|
24
25
|
const ready = hasInitialServerLayout.value;
|
|
26
|
+
const frame = useRef(0);
|
|
27
|
+
const moveCamera = (camera: Camera) => {
|
|
28
|
+
cancelAnimationFrame(frame.current);
|
|
29
|
+
viewport.value = camera;
|
|
30
|
+
};
|
|
31
|
+
// A separate gesture surface keeps cards read-only and camera changes local.
|
|
32
|
+
const navigationRef = usePanZoom({
|
|
33
|
+
viewport,
|
|
34
|
+
onViewportChange: moveCamera,
|
|
35
|
+
onViewportCommit: moveCamera,
|
|
36
|
+
});
|
|
25
37
|
useEffect(() => {
|
|
26
38
|
const params = new URLSearchParams(location.search);
|
|
27
39
|
presenting.value = params.get('present') === '1';
|
|
@@ -76,20 +88,20 @@ export function Presentation() {
|
|
|
76
88
|
const from = { ...viewport.value };
|
|
77
89
|
const start = performance.now();
|
|
78
90
|
const duration = (stop.duration ?? 1) * 1000;
|
|
79
|
-
let frame = 0;
|
|
80
91
|
const tick = (now: number) => {
|
|
81
92
|
if (recordingCamera.value) return;
|
|
82
93
|
const t = duration === 0 ? 1 : Math.min(1, (now - start) / duration);
|
|
83
94
|
viewport.value = interpolateCamera(from, target, t, area.width, area.height, stop.easing, stop.pullback);
|
|
84
|
-
if (t < 1) frame = requestAnimationFrame(tick);
|
|
95
|
+
if (t < 1) frame.current = requestAnimationFrame(tick);
|
|
85
96
|
};
|
|
86
|
-
frame = requestAnimationFrame(tick);
|
|
87
|
-
return () => cancelAnimationFrame(frame);
|
|
97
|
+
frame.current = requestAnimationFrame(tick);
|
|
98
|
+
return () => cancelAnimationFrame(frame.current);
|
|
88
99
|
}, [active, index, tour]);
|
|
89
100
|
useEffect(() => {
|
|
90
101
|
if (!active) return;
|
|
91
102
|
const key = (event: KeyboardEvent) => {
|
|
92
103
|
event.stopImmediatePropagation();
|
|
104
|
+
if (event.target instanceof HTMLButtonElement && ['Enter', ' '].includes(event.key)) return;
|
|
93
105
|
if (['Escape', 'ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp', ' '].includes(event.key))
|
|
94
106
|
event.preventDefault();
|
|
95
107
|
if (event.key === 'Escape') presenting.value = false;
|
|
@@ -100,9 +112,27 @@ export function Presentation() {
|
|
|
100
112
|
document.addEventListener('keydown', key, true);
|
|
101
113
|
return () => document.removeEventListener('keydown', key, true);
|
|
102
114
|
}, [active, tour]);
|
|
103
|
-
return active
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
115
|
+
return active ? (
|
|
116
|
+
<>
|
|
117
|
+
{!recordingCamera.value && (
|
|
118
|
+
<div
|
|
119
|
+
class="presentation-navigation"
|
|
120
|
+
ref={navigationRef}
|
|
121
|
+
onPointerDown={(event) => {
|
|
122
|
+
if (event.button === 1 || event.button === 2) cancelAnimationFrame(frame.current);
|
|
123
|
+
}}
|
|
124
|
+
onContextMenu={(event) => event.preventDefault()}
|
|
125
|
+
>
|
|
126
|
+
<button type="button" class="presentation-exit" onClick={() => (presenting.value = false)}>
|
|
127
|
+
Exit presentation
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
{error && (
|
|
132
|
+
<div class="presentation-error" role="alert">
|
|
133
|
+
{error}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</>
|
|
107
137
|
) : null;
|
|
108
138
|
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
clearSelection,
|
|
9
9
|
contextPinnedNodeIds,
|
|
10
10
|
distributeSelection,
|
|
11
|
-
removeNode,
|
|
12
11
|
selectedNodeIds,
|
|
13
12
|
} from '../state/canvas-store';
|
|
14
13
|
import { createEdgeFromClient, createGroupFromClient, removeNodeFromClient } from '../state/intent-bridge';
|
|
@@ -51,7 +50,6 @@ export function SelectionBar() {
|
|
|
51
50
|
const handleDelete = useCallback(() => {
|
|
52
51
|
const ids = Array.from(selectedNodeIds.value);
|
|
53
52
|
for (const id of ids) {
|
|
54
|
-
removeNode(id);
|
|
55
53
|
void removeNodeFromClient(id);
|
|
56
54
|
}
|
|
57
55
|
clearSelection();
|
|
@@ -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
|
))}
|
|
@@ -30,8 +30,12 @@ import {
|
|
|
30
30
|
forceDirectedArrange,
|
|
31
31
|
nodes,
|
|
32
32
|
traceEnabled,
|
|
33
|
+
viewport,
|
|
33
34
|
} from '../state/canvas-store';
|
|
35
|
+
import { importFiles } from './import-files';
|
|
36
|
+
import { canvasArea } from './canvas-area';
|
|
34
37
|
import { saveCanvasTheme } from '../state/intent-bridge';
|
|
38
|
+
import { openBoard } from '../state/boards-store';
|
|
35
39
|
import { createNodeInView } from './create-in-view';
|
|
36
40
|
import { askText } from './TextPrompt';
|
|
37
41
|
import { FeedbackDialog } from './FeedbackDialog';
|
|
@@ -175,6 +179,7 @@ export function ToolRail({
|
|
|
175
179
|
const [openMenu, setOpenMenu] = useState<null | 'theme' | 'annotate'>(null);
|
|
176
180
|
const [menuAnchor, setMenuAnchor] = useState<{ top: number; bottom: number; right: number } | null>(null);
|
|
177
181
|
const railRef = useRef<HTMLDivElement>(null);
|
|
182
|
+
const documentInput = useRef<HTMLInputElement>(null);
|
|
178
183
|
|
|
179
184
|
const toggleMenu = (menu: 'theme' | 'annotate') => (e: MouseEvent) => {
|
|
180
185
|
if (openMenu === menu) {
|
|
@@ -240,9 +245,18 @@ export function ToolRail({
|
|
|
240
245
|
return (
|
|
241
246
|
<>
|
|
242
247
|
<div class="tool-rail" ref={railRef} role="toolbar" aria-label="Canvas tools" aria-orientation="vertical">
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
<RailButton
|
|
249
|
+
label="Home"
|
|
250
|
+
ariaLabel="PMX Canvas — Home"
|
|
251
|
+
onClick={() => {
|
|
252
|
+
setOpenMenu(null);
|
|
253
|
+
void openBoard(null).catch((error) => logRailError('openHome', error));
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<span class="rail-brand">
|
|
257
|
+
<IconLogo size={22} />
|
|
258
|
+
</span>
|
|
259
|
+
</RailButton>
|
|
246
260
|
|
|
247
261
|
<RailButton
|
|
248
262
|
label="Bug and feedback"
|
|
@@ -322,6 +336,27 @@ export function ToolRail({
|
|
|
322
336
|
<RailButton label="File" shortcut="Shift+F" onClick={() => promptedCreate('file')}>
|
|
323
337
|
<IconNodeFile size={15} />
|
|
324
338
|
</RailButton>
|
|
339
|
+
<RailButton label="Attach document" onClick={() => documentInput.current?.click()}>
|
|
340
|
+
<span aria-hidden="true">↥</span>
|
|
341
|
+
</RailButton>
|
|
342
|
+
<input
|
|
343
|
+
ref={documentInput}
|
|
344
|
+
type="file"
|
|
345
|
+
hidden
|
|
346
|
+
multiple
|
|
347
|
+
accept=".pdf,.ppt,.pptx,.xls,.xlsx,.doc,.docx,.odt,.ods,.odp"
|
|
348
|
+
aria-label="Attach documents"
|
|
349
|
+
onChange={(event) => {
|
|
350
|
+
const area = canvasArea();
|
|
351
|
+
const view = viewport.value;
|
|
352
|
+
void importFiles(
|
|
353
|
+
Array.from(event.currentTarget.files ?? []),
|
|
354
|
+
(area.width / 2 - view.x) / view.scale,
|
|
355
|
+
(area.height / 2 - view.y) / view.scale,
|
|
356
|
+
);
|
|
357
|
+
event.currentTarget.value = '';
|
|
358
|
+
}}
|
|
359
|
+
/>
|
|
325
360
|
<RailButton label="Webpage" shortcut="W" onClick={() => promptedCreate('webpage')}>
|
|
326
361
|
<IconNodeWebpage size={15} />
|
|
327
362
|
</RailButton>
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { exportDialogOpen } from './ExportDialog';
|
|
2
|
+
import { activeBoardId } from '../state/boards-store';
|
|
3
|
+
import { BoardSwitcher } from './BoardSwitcher';
|
|
1
4
|
import type { ComponentChildren } from 'preact';
|
|
2
5
|
import { presenting } from '../state/presentation';
|
|
3
6
|
import { useEffect, useState } from 'preact/hooks';
|
|
@@ -264,8 +267,8 @@ export function TopBar() {
|
|
|
264
267
|
const isTraceOn = traceEnabled.value;
|
|
265
268
|
const traceNodeCount = Array.from(nodes.value.values()).filter((n) => n.type === 'trace').length;
|
|
266
269
|
|
|
267
|
-
//
|
|
268
|
-
//
|
|
270
|
+
// Until the board list arrives, the switcher shows the workspace basename
|
|
271
|
+
// (fetched once from /health).
|
|
269
272
|
const [workspaceName, setWorkspaceName] = useState<string>('');
|
|
270
273
|
useEffect(() => {
|
|
271
274
|
let cancelled = false;
|
|
@@ -314,9 +317,7 @@ export function TopBar() {
|
|
|
314
317
|
>
|
|
315
318
|
<span class={`connection-dot ${degraded ?? status}`} aria-label={`Canvas status: ${statusTitle}`} />
|
|
316
319
|
</BarHint>
|
|
317
|
-
<
|
|
318
|
-
{workspaceName || 'PMX Canvas'}
|
|
319
|
-
</span>
|
|
320
|
+
<BoardSwitcher fallbackName={workspaceName || 'PMX Canvas'} />
|
|
320
321
|
<span class="top-bar-meta hud-collapsible-text">{sessionId.value ? sessionId.value.slice(0, 12) : '…'}</span>
|
|
321
322
|
<span class="top-bar-meta hud-collapsible-text">{countsLabel}</span>
|
|
322
323
|
|
|
@@ -330,20 +331,35 @@ export function TopBar() {
|
|
|
330
331
|
|
|
331
332
|
<div class="top-bar-sep" />
|
|
332
333
|
|
|
333
|
-
|
|
334
|
-
label="
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
334
|
+
{activeBoardId.value && (
|
|
335
|
+
<BarHint label="Export board" body="One HTML file of this board that anyone can open, no install needed.">
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
class="present-button"
|
|
339
|
+
onClick={() => {
|
|
340
|
+
exportDialogOpen.value = true;
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
Export
|
|
344
|
+
</button>
|
|
345
|
+
</BarHint>
|
|
346
|
+
)}
|
|
347
|
+
{activeBoardId.value && (
|
|
348
|
+
<BarHint
|
|
349
|
+
label="Present board"
|
|
350
|
+
body="Arrow keys or Space step through stops; Esc exits. Without a saved tour, groups are read top-to-bottom, left-to-right."
|
|
343
351
|
>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
class="present-button"
|
|
355
|
+
onClick={() => {
|
|
356
|
+
presenting.value = true;
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
Present
|
|
360
|
+
</button>
|
|
361
|
+
</BarHint>
|
|
362
|
+
)}
|
|
347
363
|
<BarHint label="Zoom out" shortcut={modChord('\u2212')}>
|
|
348
364
|
<button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1 / 1.25)} aria-label="Zoom out">
|
|
349
365
|
<IconZoomOut />
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { createNodeFromClient } from '../state/intent-bridge';
|
|
1
|
+
import { createNodeFromClient, requestJson } from '../state/intent-bridge';
|
|
2
|
+
import { activeBoardId } from '../state/boards-store';
|
|
3
|
+
import { showToast } from '../state/attention-bridge';
|
|
2
4
|
|
|
3
5
|
const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico', 'avif']);
|
|
4
6
|
const MD_EXTS = new Set(['md', 'mdx', 'markdown']);
|
|
@@ -17,6 +19,7 @@ export function nodeTypeFromFilename(name: string): 'image' | 'markdown' | 'file
|
|
|
17
19
|
* markdown, everything else a file node with the text inlined.
|
|
18
20
|
*/
|
|
19
21
|
export async function importFiles(files: File[], baseWx: number, baseWy: number): Promise<void> {
|
|
22
|
+
const boardId = activeBoardId.value;
|
|
20
23
|
const nodeW = 400;
|
|
21
24
|
const nodeH = 300;
|
|
22
25
|
const spacing = 20;
|
|
@@ -32,12 +35,24 @@ export async function importFiles(files: File[], baseWx: number, baseWy: number)
|
|
|
32
35
|
const type = nodeTypeFromFilename(file.name);
|
|
33
36
|
const fileName = file.name;
|
|
34
37
|
|
|
38
|
+
if (/\.(pdf|pptx?|xlsx?|docx?|od[pts])$/i.test(fileName)) {
|
|
39
|
+
if (!boardId || file.size === 0 || file.size > 20 * 1024 * 1024) {
|
|
40
|
+
showToast('remove', 'File not attached', 'Open a board and choose a nonempty file under 20 MiB.');
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const query = new URLSearchParams({ boardId, name: fileName, mime: file.type, x: String(wx), y: String(wy) });
|
|
44
|
+
showToast('context', 'Attaching document', fileName);
|
|
45
|
+
await requestJson('attachDocument', `/api/canvas/attachments?${query}`, null, { method: 'POST', body: file });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (activeBoardId.value !== boardId) return;
|
|
35
49
|
if (type === 'image') {
|
|
36
50
|
const reader = new FileReader();
|
|
37
51
|
const dataUri: string = await new Promise((resolve) => {
|
|
38
52
|
reader.onload = () => resolve(reader.result as string);
|
|
39
53
|
reader.readAsDataURL(file);
|
|
40
54
|
});
|
|
55
|
+
if (activeBoardId.value !== boardId) return;
|
|
41
56
|
await createNodeFromClient({
|
|
42
57
|
type: 'image',
|
|
43
58
|
title: fileName,
|
|
@@ -46,9 +61,11 @@ export async function importFiles(files: File[], baseWx: number, baseWy: number)
|
|
|
46
61
|
y: wy,
|
|
47
62
|
width: nodeW,
|
|
48
63
|
height: nodeH,
|
|
64
|
+
...(boardId ? { boardId } : {}),
|
|
49
65
|
});
|
|
50
66
|
} else {
|
|
51
67
|
const text = await file.text();
|
|
68
|
+
if (activeBoardId.value !== boardId) return;
|
|
52
69
|
const isWide = type === 'markdown' || type === 'file';
|
|
53
70
|
await createNodeFromClient({
|
|
54
71
|
type,
|
|
@@ -58,6 +75,7 @@ export async function importFiles(files: File[], baseWx: number, baseWy: number)
|
|
|
58
75
|
y: wy,
|
|
59
76
|
width: isWide ? 720 : nodeW,
|
|
60
77
|
height: isWide ? 500 : nodeH,
|
|
78
|
+
...(boardId ? { boardId } : {}),
|
|
61
79
|
});
|
|
62
80
|
}
|
|
63
81
|
}
|
|
@@ -7,6 +7,7 @@ export const KIND_COLOR: Record<CanvasNodeState['type'], string> = {
|
|
|
7
7
|
webpage: 'var(--c-warn)',
|
|
8
8
|
'json-render': 'var(--c-ok)',
|
|
9
9
|
graph: 'var(--c-purple)',
|
|
10
|
+
board: 'var(--c-accent)',
|
|
10
11
|
prompt: 'var(--c-accent)',
|
|
11
12
|
response: 'var(--c-ok)',
|
|
12
13
|
status: 'var(--c-warn)',
|
package/src/client/icons.tsx
CHANGED