pmx-canvas 0.1.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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/Readme.md +865 -0
- package/dist/canvas/global.css +3173 -0
- package/dist/canvas/index.js +183 -0
- package/dist/json-render/index.css +2 -0
- package/dist/json-render/index.js +389 -0
- package/dist/types/cli/agent.d.ts +13 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/watch.d.ts +5 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
- package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
- package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
- package/dist/types/client/canvas/DockedNode.d.ts +4 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
- package/dist/types/client/canvas/Minimap.d.ts +23 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
- package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
- package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
- package/dist/types/client/canvas/snap-guides.d.ts +23 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
- package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
- package/dist/types/client/ext-app/bridge.d.ts +161 -0
- package/dist/types/client/icons.d.ts +70 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/nodes/ContextNode.d.ts +34 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
- package/dist/types/client/nodes/FileNode.d.ts +5 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -0
- package/dist/types/client/nodes/ImageNode.d.ts +10 -0
- package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
- package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
- package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
- package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
- package/dist/types/client/nodes/PromptNode.d.ts +5 -0
- package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
- package/dist/types/client/nodes/StatusNode.d.ts +4 -0
- package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
- package/dist/types/client/nodes/TraceNode.d.ts +4 -0
- package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
- package/dist/types/client/nodes/image-warnings.d.ts +6 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
- package/dist/types/client/nodes/md-format.d.ts +25 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/attention-store.d.ts +25 -0
- package/dist/types/client/state/canvas-store.d.ts +74 -0
- package/dist/types/client/state/intent-bridge.d.ts +158 -0
- package/dist/types/client/state/sse-bridge.d.ts +5 -0
- package/dist/types/client/theme/tokens.d.ts +27 -0
- package/dist/types/client/types.d.ts +40 -0
- package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
- package/dist/types/client/utils/placement.d.ts +1 -0
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/json-render/catalog.d.ts +815 -0
- package/dist/types/json-render/charts/components.d.ts +54 -0
- package/dist/types/json-render/charts/definitions.d.ts +103 -0
- package/dist/types/json-render/charts/extra-components.d.ts +58 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
- package/dist/types/json-render/renderer/index.d.ts +16 -0
- package/dist/types/json-render/schema.d.ts +46 -0
- package/dist/types/json-render/server.d.ts +55 -0
- package/dist/types/mcp/server.d.ts +22 -0
- package/dist/types/server/agent-context.d.ts +21 -0
- package/dist/types/server/artifact-paths.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +154 -0
- package/dist/types/server/canvas-provenance.d.ts +13 -0
- package/dist/types/server/canvas-schema.d.ts +49 -0
- package/dist/types/server/canvas-serialization.d.ts +25 -0
- package/dist/types/server/canvas-state.d.ts +174 -0
- package/dist/types/server/canvas-validation.d.ts +33 -0
- package/dist/types/server/chart-template.d.ts +29 -0
- package/dist/types/server/code-graph.d.ts +67 -0
- package/dist/types/server/context-cards.d.ts +24 -0
- package/dist/types/server/diagram-presets.d.ts +28 -0
- package/dist/types/server/ext-app-call-registry.d.ts +16 -0
- package/dist/types/server/ext-app-tool-result.d.ts +1 -0
- package/dist/types/server/file-watcher.d.ts +16 -0
- package/dist/types/server/index.d.ts +243 -0
- package/dist/types/server/mcp-app-candidate.d.ts +25 -0
- package/dist/types/server/mcp-app-host.d.ts +65 -0
- package/dist/types/server/mcp-app-runtime.d.ts +47 -0
- package/dist/types/server/mutation-history.d.ts +105 -0
- package/dist/types/server/placement.d.ts +37 -0
- package/dist/types/server/server.d.ts +103 -0
- package/dist/types/server/spatial-analysis.d.ts +87 -0
- package/dist/types/server/trace-manager.d.ts +48 -0
- package/dist/types/server/web-artifacts.d.ts +50 -0
- package/dist/types/server/webpage-node.d.ts +25 -0
- package/dist/types/shared/auto-arrange.d.ts +29 -0
- package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
- package/dist/types/shared/placement.d.ts +26 -0
- package/dist/types/shared/semantic-attention.d.ts +97 -0
- package/package.json +109 -0
- package/skills/data-analysis/SKILL.md +324 -0
- package/skills/doc-coauthoring/SKILL.md +375 -0
- package/skills/frontend-design/SKILL.md +45 -0
- package/skills/json-render-codegen/SKILL.md +112 -0
- package/skills/json-render-core/SKILL.md +265 -0
- package/skills/json-render-ink/SKILL.md +273 -0
- package/skills/json-render-mcp/SKILL.md +132 -0
- package/skills/json-render-react/SKILL.md +264 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/playwright-cli/SKILL.md +67 -0
- package/skills/pmx-canvas/SKILL.md +668 -0
- package/skills/pmx-canvas/evals/evals.json +186 -0
- package/skills/pmx-canvas-testing/SKILL.md +78 -0
- package/skills/published-consumer-e2e/SKILL.md +43 -0
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
- package/skills/web-artifacts-builder/SKILL.md +80 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- package/src/cli/agent.ts +2144 -0
- package/src/cli/index.ts +622 -0
- package/src/cli/watch.ts +88 -0
- package/src/client/App.tsx +507 -0
- package/src/client/canvas/AttentionHistory.tsx +81 -0
- package/src/client/canvas/AttentionToast.tsx +19 -0
- package/src/client/canvas/CanvasNode.tsx +363 -0
- package/src/client/canvas/CanvasViewport.tsx +590 -0
- package/src/client/canvas/CommandPalette.tsx +302 -0
- package/src/client/canvas/ContextMenu.tsx +601 -0
- package/src/client/canvas/ContextPinBar.tsx +25 -0
- package/src/client/canvas/ContextPinHud.tsx +22 -0
- package/src/client/canvas/DockedNode.tsx +66 -0
- package/src/client/canvas/EdgeLayer.tsx +280 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
- package/src/client/canvas/FocusFieldLayer.tsx +107 -0
- package/src/client/canvas/Minimap.tsx +301 -0
- package/src/client/canvas/SelectionBar.tsx +69 -0
- package/src/client/canvas/ShortcutOverlay.tsx +69 -0
- package/src/client/canvas/SnapshotPanel.tsx +236 -0
- package/src/client/canvas/snap-guides.ts +170 -0
- package/src/client/canvas/use-node-drag.ts +51 -0
- package/src/client/canvas/use-node-resize.ts +59 -0
- package/src/client/canvas/use-pan-zoom.ts +191 -0
- package/src/client/ext-app/bridge.ts +542 -0
- package/src/client/icons.tsx +424 -0
- package/src/client/index.tsx +7 -0
- package/src/client/nodes/ContextNode.tsx +412 -0
- package/src/client/nodes/ExtAppFrame.tsx +509 -0
- package/src/client/nodes/FileNode.tsx +256 -0
- package/src/client/nodes/GroupNode.tsx +39 -0
- package/src/client/nodes/ImageNode.tsx +160 -0
- package/src/client/nodes/InlineFormatBar.tsx +169 -0
- package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
- package/src/client/nodes/LedgerNode.tsx +37 -0
- package/src/client/nodes/MarkdownNode.tsx +359 -0
- package/src/client/nodes/McpAppNode.tsx +85 -0
- package/src/client/nodes/MdFormatBar.tsx +109 -0
- package/src/client/nodes/PromptNode.tsx +597 -0
- package/src/client/nodes/ResponseNode.tsx +153 -0
- package/src/client/nodes/StatusNode.tsx +84 -0
- package/src/client/nodes/StatusSummary.tsx +38 -0
- package/src/client/nodes/TraceNode.tsx +120 -0
- package/src/client/nodes/WebpageNode.tsx +288 -0
- package/src/client/nodes/image-warnings.ts +95 -0
- package/src/client/nodes/inline-editor-commands.ts +37 -0
- package/src/client/nodes/md-format.ts +206 -0
- package/src/client/state/attention-bridge.ts +328 -0
- package/src/client/state/attention-store.ts +73 -0
- package/src/client/state/canvas-store.ts +631 -0
- package/src/client/state/intent-bridge.ts +315 -0
- package/src/client/state/sse-bridge.ts +965 -0
- package/src/client/theme/global.css +3173 -0
- package/src/client/theme/tokens.ts +72 -0
- package/src/client/types-shims.d.ts +5 -0
- package/src/client/types.ts +81 -0
- package/src/client/utils/ext-app-tool-result.ts +4 -0
- package/src/client/utils/placement.ts +4 -0
- package/src/client/utils/platform.ts +2 -0
- package/src/json-render/catalog.ts +256 -0
- package/src/json-render/charts/components.tsx +198 -0
- package/src/json-render/charts/definitions.ts +81 -0
- package/src/json-render/charts/extra-components.tsx +267 -0
- package/src/json-render/charts/extra-definitions.ts +145 -0
- package/src/json-render/renderer/index.css +174 -0
- package/src/json-render/renderer/index.tsx +86 -0
- package/src/json-render/schema.ts +62 -0
- package/src/json-render/server.ts +597 -0
- package/src/mcp/server.ts +1377 -0
- package/src/server/agent-context.ts +242 -0
- package/src/server/artifact-paths.ts +17 -0
- package/src/server/canvas-operations.ts +1279 -0
- package/src/server/canvas-provenance.ts +243 -0
- package/src/server/canvas-schema.ts +432 -0
- package/src/server/canvas-serialization.ts +95 -0
- package/src/server/canvas-state.ts +1134 -0
- package/src/server/canvas-validation.ts +114 -0
- package/src/server/chart-template.ts +449 -0
- package/src/server/code-graph.ts +370 -0
- package/src/server/context-cards.ts +31 -0
- package/src/server/diagram-presets.ts +71 -0
- package/src/server/ext-app-call-registry.ts +77 -0
- package/src/server/ext-app-tool-result.ts +4 -0
- package/src/server/file-watcher.ts +121 -0
- package/src/server/index.ts +647 -0
- package/src/server/mcp-app-candidate.ts +174 -0
- package/src/server/mcp-app-host.ts +814 -0
- package/src/server/mcp-app-runtime.ts +459 -0
- package/src/server/mutation-history.ts +350 -0
- package/src/server/placement.ts +125 -0
- package/src/server/server.ts +3846 -0
- package/src/server/spatial-analysis.ts +356 -0
- package/src/server/trace-manager.ts +333 -0
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
- package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
- package/src/server/web-artifacts.ts +442 -0
- package/src/server/webpage-node.ts +328 -0
- package/src/shared/auto-arrange.ts +439 -0
- package/src/shared/ext-app-tool-result.ts +76 -0
- package/src/shared/placement.ts +81 -0
- package/src/shared/semantic-attention.ts +598 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import type { CanvasEdge, CanvasLayout, CanvasNodeState } from '../client/types.js';
|
|
2
|
+
import { buildSpatialContext, type SpatialContext } from '../server/spatial-analysis.js';
|
|
3
|
+
|
|
4
|
+
export type SemanticWatchEventType =
|
|
5
|
+
| 'context-pin'
|
|
6
|
+
| 'connect'
|
|
7
|
+
| 'remove'
|
|
8
|
+
| 'group'
|
|
9
|
+
| 'move-end';
|
|
10
|
+
|
|
11
|
+
export const ALL_SEMANTIC_WATCH_EVENT_TYPES: SemanticWatchEventType[] = [
|
|
12
|
+
'context-pin',
|
|
13
|
+
'connect',
|
|
14
|
+
'remove',
|
|
15
|
+
'group',
|
|
16
|
+
'move-end',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export interface SemanticWatchNodeSummary {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string | null;
|
|
22
|
+
nodeType: CanvasNodeState['type'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SemanticWatchEdgeSummary {
|
|
26
|
+
id: string;
|
|
27
|
+
edgeType: CanvasEdge['type'];
|
|
28
|
+
fromId: string;
|
|
29
|
+
toId: string;
|
|
30
|
+
fromTitle: string | null;
|
|
31
|
+
toTitle: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ContextPinWatchEvent {
|
|
35
|
+
type: 'context-pin';
|
|
36
|
+
timestamp?: string;
|
|
37
|
+
sessionId?: string;
|
|
38
|
+
added: SemanticWatchNodeSummary[];
|
|
39
|
+
removed: SemanticWatchNodeSummary[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ConnectWatchEvent {
|
|
43
|
+
type: 'connect';
|
|
44
|
+
timestamp?: string;
|
|
45
|
+
sessionId?: string;
|
|
46
|
+
edges: SemanticWatchEdgeSummary[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RemoveWatchEvent {
|
|
50
|
+
type: 'remove';
|
|
51
|
+
timestamp?: string;
|
|
52
|
+
sessionId?: string;
|
|
53
|
+
nodes: SemanticWatchNodeSummary[];
|
|
54
|
+
edges: SemanticWatchEdgeSummary[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface GroupCreatedSummary {
|
|
58
|
+
id: string;
|
|
59
|
+
title: string | null;
|
|
60
|
+
childCount: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface GroupUpdatedSummary {
|
|
64
|
+
id: string;
|
|
65
|
+
title: string | null;
|
|
66
|
+
addedChildIds: string[];
|
|
67
|
+
removedChildIds: string[];
|
|
68
|
+
childCount: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface GroupWatchEvent {
|
|
72
|
+
type: 'group';
|
|
73
|
+
timestamp?: string;
|
|
74
|
+
sessionId?: string;
|
|
75
|
+
created: GroupCreatedSummary[];
|
|
76
|
+
updated: GroupUpdatedSummary[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface MoveEndNodeSummary extends SemanticWatchNodeSummary {
|
|
80
|
+
reasons: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface MoveEndWatchEvent {
|
|
84
|
+
type: 'move-end';
|
|
85
|
+
timestamp?: string;
|
|
86
|
+
sessionId?: string;
|
|
87
|
+
nodes: MoveEndNodeSummary[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type SemanticWatchEvent =
|
|
91
|
+
| ContextPinWatchEvent
|
|
92
|
+
| ConnectWatchEvent
|
|
93
|
+
| RemoveWatchEvent
|
|
94
|
+
| GroupWatchEvent
|
|
95
|
+
| MoveEndWatchEvent;
|
|
96
|
+
|
|
97
|
+
export interface SseMessage {
|
|
98
|
+
event: string;
|
|
99
|
+
data: unknown;
|
|
100
|
+
id?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface EventMeta {
|
|
104
|
+
timestamp?: string;
|
|
105
|
+
sessionId?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SemanticAttentionRegion {
|
|
109
|
+
id: string;
|
|
110
|
+
primaryNodeId: string;
|
|
111
|
+
nodeIds: string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface SemanticAttentionSnapshot {
|
|
115
|
+
layout: CanvasLayout | null;
|
|
116
|
+
pinnedNodeIds: string[];
|
|
117
|
+
primaryFocusNodeIds: string[];
|
|
118
|
+
secondaryFocusNodeIds: string[];
|
|
119
|
+
regions: SemanticAttentionRegion[];
|
|
120
|
+
spatial: SpatialContext | null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getNodeTitle(node: CanvasNodeState | undefined): string | null {
|
|
124
|
+
if (!node) return null;
|
|
125
|
+
const raw = node.data.title;
|
|
126
|
+
return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function summarizeNode(node: CanvasNodeState | undefined, fallbackId: string): SemanticWatchNodeSummary {
|
|
130
|
+
return {
|
|
131
|
+
id: node?.id ?? fallbackId,
|
|
132
|
+
title: getNodeTitle(node),
|
|
133
|
+
nodeType: node?.type ?? 'markdown',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function summarizeEdge(edge: CanvasEdge, nodeMap: Map<string, CanvasNodeState>): SemanticWatchEdgeSummary {
|
|
138
|
+
return {
|
|
139
|
+
id: edge.id,
|
|
140
|
+
edgeType: edge.type,
|
|
141
|
+
fromId: edge.from,
|
|
142
|
+
toId: edge.to,
|
|
143
|
+
fromTitle: getNodeTitle(nodeMap.get(edge.from)),
|
|
144
|
+
toTitle: getNodeTitle(nodeMap.get(edge.to)),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toNodeMap(nodes: CanvasNodeState[]): Map<string, CanvasNodeState> {
|
|
149
|
+
return new Map(nodes.map((node) => [node.id, node]));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function toEdgeMap(edges: CanvasEdge[]): Map<string, CanvasEdge> {
|
|
153
|
+
return new Map(edges.map((edge) => [edge.id, edge]));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sortIds(values: Iterable<string>): string[] {
|
|
157
|
+
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function arraysEqual(a: string[], b: string[]): boolean {
|
|
161
|
+
if (a.length !== b.length) return false;
|
|
162
|
+
for (let i = 0; i < a.length; i++) {
|
|
163
|
+
if (a[i] !== b[i]) return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function summarizeTitleList(nodes: SemanticWatchNodeSummary[]): string {
|
|
169
|
+
return nodes
|
|
170
|
+
.map((node) => node.title ?? node.id)
|
|
171
|
+
.map((value) => `"${value}"`)
|
|
172
|
+
.join(', ');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getGroupChildren(node: CanvasNodeState | undefined): string[] {
|
|
176
|
+
if (!node || node.type !== 'group') return [];
|
|
177
|
+
const raw = node.data.children;
|
|
178
|
+
if (!Array.isArray(raw)) return [];
|
|
179
|
+
return raw.filter((value): value is string => typeof value === 'string').sort((a, b) => a.localeCompare(b));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildClusterPeerMap(spatial: SpatialContext): Map<string, string[]> {
|
|
183
|
+
const map = new Map<string, string[]>();
|
|
184
|
+
for (const cluster of spatial.clusters) {
|
|
185
|
+
const members = [...cluster.nodeIds].sort((a, b) => a.localeCompare(b));
|
|
186
|
+
for (const nodeId of members) {
|
|
187
|
+
map.set(nodeId, members.filter((id) => id !== nodeId));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return map;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildNeighborhoodMap(spatial: SpatialContext): Map<string, string[]> {
|
|
194
|
+
const map = new Map<string, string[]>();
|
|
195
|
+
for (const neighborhood of spatial.pinnedNeighborhoods) {
|
|
196
|
+
map.set(
|
|
197
|
+
neighborhood.pinnedNodeId,
|
|
198
|
+
neighborhood.neighbors.map((neighbor) => neighbor.id).sort((a, b) => a.localeCompare(b)),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return map;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function diffSet(prev: Set<string>, next: Set<string>): { added: string[]; removed: string[] } {
|
|
205
|
+
const added = sortIds(Array.from(next).filter((id) => !prev.has(id)));
|
|
206
|
+
const removed = sortIds(Array.from(prev).filter((id) => !next.has(id)));
|
|
207
|
+
return { added, removed };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeEventMeta(payload: unknown): EventMeta {
|
|
211
|
+
const record = payload && typeof payload === 'object' ? payload as Record<string, unknown> : {};
|
|
212
|
+
return {
|
|
213
|
+
timestamp: typeof record.timestamp === 'string' ? record.timestamp : undefined,
|
|
214
|
+
sessionId: typeof record.sessionId === 'string' ? record.sessionId : undefined,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function compactCount(count: number, singular: string, plural = `${singular}s`): string {
|
|
219
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function buildAttentionSnapshot(
|
|
223
|
+
currentLayout: CanvasLayout | null,
|
|
224
|
+
currentPins: Set<string>,
|
|
225
|
+
spatial: SpatialContext | null,
|
|
226
|
+
): SemanticAttentionSnapshot {
|
|
227
|
+
if (!currentLayout || !spatial) {
|
|
228
|
+
return {
|
|
229
|
+
layout: currentLayout,
|
|
230
|
+
pinnedNodeIds: [],
|
|
231
|
+
primaryFocusNodeIds: [],
|
|
232
|
+
secondaryFocusNodeIds: [],
|
|
233
|
+
regions: [],
|
|
234
|
+
spatial,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const nodeMap = toNodeMap(currentLayout.nodes);
|
|
239
|
+
const pinnedNodeIds = sortIds(currentPins).filter((nodeId) => nodeMap.has(nodeId));
|
|
240
|
+
const primaryIds = new Set(pinnedNodeIds);
|
|
241
|
+
const secondaryIds = new Set<string>();
|
|
242
|
+
const regions: SemanticAttentionRegion[] = [];
|
|
243
|
+
|
|
244
|
+
for (const neighborhood of spatial.pinnedNeighborhoods) {
|
|
245
|
+
if (!primaryIds.has(neighborhood.pinnedNodeId)) continue;
|
|
246
|
+
const nodeIds = sortIds([
|
|
247
|
+
neighborhood.pinnedNodeId,
|
|
248
|
+
...neighborhood.neighbors.map((neighbor) => neighbor.id),
|
|
249
|
+
]).filter((nodeId, index, values) => values.indexOf(nodeId) === index && nodeMap.has(nodeId));
|
|
250
|
+
for (const nodeId of nodeIds) {
|
|
251
|
+
if (!primaryIds.has(nodeId)) secondaryIds.add(nodeId);
|
|
252
|
+
}
|
|
253
|
+
regions.push({
|
|
254
|
+
id: `region-${neighborhood.pinnedNodeId}`,
|
|
255
|
+
primaryNodeId: neighborhood.pinnedNodeId,
|
|
256
|
+
nodeIds,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (regions.length === 0 && pinnedNodeIds.length > 0) {
|
|
261
|
+
for (const pinnedNodeId of pinnedNodeIds) {
|
|
262
|
+
regions.push({
|
|
263
|
+
id: `region-${pinnedNodeId}`,
|
|
264
|
+
primaryNodeId: pinnedNodeId,
|
|
265
|
+
nodeIds: [pinnedNodeId],
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
layout: currentLayout,
|
|
272
|
+
pinnedNodeIds,
|
|
273
|
+
primaryFocusNodeIds: pinnedNodeIds,
|
|
274
|
+
secondaryFocusNodeIds: sortIds(secondaryIds),
|
|
275
|
+
regions,
|
|
276
|
+
spatial,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function formatCompactWatchEvent(event: SemanticWatchEvent): string {
|
|
281
|
+
switch (event.type) {
|
|
282
|
+
case 'context-pin': {
|
|
283
|
+
const parts: string[] = [];
|
|
284
|
+
if (event.added.length > 0) parts.push(`+${event.added.length}: ${summarizeTitleList(event.added)}`);
|
|
285
|
+
if (event.removed.length > 0) parts.push(`removed: ${summarizeTitleList(event.removed)}`);
|
|
286
|
+
return `context-pin ${parts.join(' | ')}`.trim();
|
|
287
|
+
}
|
|
288
|
+
case 'connect':
|
|
289
|
+
return `connect ${compactCount(event.edges.length, 'edge')}: ${event.edges
|
|
290
|
+
.map((edge) => `"${edge.fromTitle ?? edge.fromId}" -> "${edge.toTitle ?? edge.toId}" (${edge.edgeType})`)
|
|
291
|
+
.join(', ')}`;
|
|
292
|
+
case 'remove': {
|
|
293
|
+
const parts: string[] = [];
|
|
294
|
+
if (event.nodes.length > 0) {
|
|
295
|
+
parts.push(`${compactCount(event.nodes.length, 'node')}: ${summarizeTitleList(event.nodes)}`);
|
|
296
|
+
}
|
|
297
|
+
if (event.edges.length > 0) {
|
|
298
|
+
parts.push(`${compactCount(event.edges.length, 'edge')}`);
|
|
299
|
+
}
|
|
300
|
+
return `remove ${parts.join(' | ')}`.trim();
|
|
301
|
+
}
|
|
302
|
+
case 'group': {
|
|
303
|
+
const parts: string[] = [];
|
|
304
|
+
if (event.created.length > 0) {
|
|
305
|
+
parts.push(
|
|
306
|
+
`created: ${event.created
|
|
307
|
+
.map((group) => `"${group.title ?? group.id}" (${compactCount(group.childCount, 'child')})`)
|
|
308
|
+
.join(', ')}`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (event.updated.length > 0) {
|
|
312
|
+
parts.push(
|
|
313
|
+
`updated: ${event.updated
|
|
314
|
+
.map((group) =>
|
|
315
|
+
`"${group.title ?? group.id}" +${group.addedChildIds.length} -${group.removedChildIds.length} children`,
|
|
316
|
+
)
|
|
317
|
+
.join(', ')}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return `group ${parts.join(' | ')}`.trim();
|
|
321
|
+
}
|
|
322
|
+
case 'move-end':
|
|
323
|
+
return `move-end: ${event.nodes
|
|
324
|
+
.map((node) => `"${node.title ?? node.id}" ${node.reasons.join('; ')}`)
|
|
325
|
+
.join(', ')}`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export class SemanticWatchReducer {
|
|
330
|
+
private currentLayout: CanvasLayout | null = null;
|
|
331
|
+
private currentPins = new Set<string>();
|
|
332
|
+
private previousSpatial: SpatialContext | null = null;
|
|
333
|
+
|
|
334
|
+
setInitialPins(nodeIds: string[]): void {
|
|
335
|
+
this.currentPins = new Set(nodeIds);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
getAttentionSnapshot(): SemanticAttentionSnapshot {
|
|
339
|
+
return buildAttentionSnapshot(this.currentLayout, this.currentPins, this.previousSpatial);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
handleMessage(message: SseMessage): SemanticWatchEvent[] {
|
|
343
|
+
if (message.event === 'context-pins-changed') {
|
|
344
|
+
return this.handleContextPinsChanged(message.data);
|
|
345
|
+
}
|
|
346
|
+
if (message.event === 'canvas-layout-update') {
|
|
347
|
+
return this.handleLayoutUpdate(message.data);
|
|
348
|
+
}
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private handleContextPinsChanged(payload: unknown): SemanticWatchEvent[] {
|
|
353
|
+
const record = payload && typeof payload === 'object' ? payload as Record<string, unknown> : {};
|
|
354
|
+
const nodeIds = Array.isArray(record.nodeIds)
|
|
355
|
+
? record.nodeIds.filter((id): id is string => typeof id === 'string')
|
|
356
|
+
: [];
|
|
357
|
+
const meta = normalizeEventMeta(payload);
|
|
358
|
+
const nextPins = new Set(nodeIds);
|
|
359
|
+
const { added, removed } = diffSet(this.currentPins, nextPins);
|
|
360
|
+
this.currentPins = nextPins;
|
|
361
|
+
|
|
362
|
+
if (!this.currentLayout) return [];
|
|
363
|
+
|
|
364
|
+
const nodeMap = toNodeMap(this.currentLayout.nodes);
|
|
365
|
+
const previousEventPins = {
|
|
366
|
+
added: added.map((id) => summarizeNode(nodeMap.get(id), id)),
|
|
367
|
+
removed: removed.map((id) => summarizeNode(nodeMap.get(id), id)),
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
this.previousSpatial = buildSpatialContext(
|
|
371
|
+
this.currentLayout.nodes,
|
|
372
|
+
this.currentLayout.edges,
|
|
373
|
+
this.currentPins,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (previousEventPins.added.length === 0 && previousEventPins.removed.length === 0) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return [{
|
|
381
|
+
type: 'context-pin',
|
|
382
|
+
...meta,
|
|
383
|
+
...previousEventPins,
|
|
384
|
+
}];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private handleLayoutUpdate(payload: unknown): SemanticWatchEvent[] {
|
|
388
|
+
const record = payload && typeof payload === 'object' ? payload as Record<string, unknown> : {};
|
|
389
|
+
const layout = record.layout && typeof record.layout === 'object'
|
|
390
|
+
? record.layout as CanvasLayout
|
|
391
|
+
: null;
|
|
392
|
+
if (!layout) return [];
|
|
393
|
+
|
|
394
|
+
const meta = normalizeEventMeta(payload);
|
|
395
|
+
if (!this.currentLayout) {
|
|
396
|
+
this.currentLayout = layout;
|
|
397
|
+
this.previousSpatial = buildSpatialContext(layout.nodes, layout.edges, this.currentPins);
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const prevLayout = this.currentLayout;
|
|
402
|
+
const prevSpatial = this.previousSpatial ?? buildSpatialContext(
|
|
403
|
+
prevLayout.nodes,
|
|
404
|
+
prevLayout.edges,
|
|
405
|
+
this.currentPins,
|
|
406
|
+
);
|
|
407
|
+
const nextSpatial = buildSpatialContext(layout.nodes, layout.edges, this.currentPins);
|
|
408
|
+
const events: SemanticWatchEvent[] = [];
|
|
409
|
+
|
|
410
|
+
const prevNodeMap = toNodeMap(prevLayout.nodes);
|
|
411
|
+
const nextNodeMap = toNodeMap(layout.nodes);
|
|
412
|
+
const prevEdgeMap = toEdgeMap(prevLayout.edges);
|
|
413
|
+
const nextEdgeMap = toEdgeMap(layout.edges);
|
|
414
|
+
|
|
415
|
+
const addedEdges = sortIds(Array.from(nextEdgeMap.keys()).filter((id) => !prevEdgeMap.has(id)))
|
|
416
|
+
.map((id) => nextEdgeMap.get(id))
|
|
417
|
+
.filter((edge): edge is CanvasEdge => edge !== undefined);
|
|
418
|
+
if (addedEdges.length > 0) {
|
|
419
|
+
events.push({
|
|
420
|
+
type: 'connect',
|
|
421
|
+
...meta,
|
|
422
|
+
edges: addedEdges.map((edge) => summarizeEdge(edge, nextNodeMap)),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const removedNodeIds = sortIds(Array.from(prevNodeMap.keys()).filter((id) => !nextNodeMap.has(id)));
|
|
427
|
+
const removedEdgeIds = sortIds(Array.from(prevEdgeMap.keys()).filter((id) => !nextEdgeMap.has(id)));
|
|
428
|
+
if (removedNodeIds.length > 0 || removedEdgeIds.length > 0) {
|
|
429
|
+
events.push({
|
|
430
|
+
type: 'remove',
|
|
431
|
+
...meta,
|
|
432
|
+
nodes: removedNodeIds.map((id) => summarizeNode(prevNodeMap.get(id), id)),
|
|
433
|
+
edges: removedEdgeIds
|
|
434
|
+
.map((id) => prevEdgeMap.get(id))
|
|
435
|
+
.filter((edge): edge is CanvasEdge => edge !== undefined)
|
|
436
|
+
.map((edge) => summarizeEdge(edge, prevNodeMap)),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const groupEvent = this.buildGroupEvent(prevNodeMap, nextNodeMap, meta);
|
|
441
|
+
if (groupEvent) events.push(groupEvent);
|
|
442
|
+
|
|
443
|
+
const moveEvent = this.buildMoveEndEvent(
|
|
444
|
+
prevLayout,
|
|
445
|
+
layout,
|
|
446
|
+
prevNodeMap,
|
|
447
|
+
nextNodeMap,
|
|
448
|
+
prevSpatial,
|
|
449
|
+
nextSpatial,
|
|
450
|
+
meta,
|
|
451
|
+
);
|
|
452
|
+
if (moveEvent) events.push(moveEvent);
|
|
453
|
+
|
|
454
|
+
this.currentLayout = layout;
|
|
455
|
+
this.previousSpatial = nextSpatial;
|
|
456
|
+
return events;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private buildGroupEvent(
|
|
460
|
+
prevNodeMap: Map<string, CanvasNodeState>,
|
|
461
|
+
nextNodeMap: Map<string, CanvasNodeState>,
|
|
462
|
+
meta: EventMeta,
|
|
463
|
+
): GroupWatchEvent | null {
|
|
464
|
+
const prevGroupIds = sortIds(Array.from(prevNodeMap.values())
|
|
465
|
+
.filter((node) => node.type === 'group')
|
|
466
|
+
.map((node) => node.id));
|
|
467
|
+
const nextGroupIds = sortIds(Array.from(nextNodeMap.values())
|
|
468
|
+
.filter((node) => node.type === 'group')
|
|
469
|
+
.map((node) => node.id));
|
|
470
|
+
|
|
471
|
+
const created = nextGroupIds
|
|
472
|
+
.filter((id) => !prevGroupIds.includes(id))
|
|
473
|
+
.map((id) => {
|
|
474
|
+
const group = nextNodeMap.get(id);
|
|
475
|
+
const children = getGroupChildren(group);
|
|
476
|
+
return {
|
|
477
|
+
id,
|
|
478
|
+
title: getNodeTitle(group),
|
|
479
|
+
childCount: children.length,
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const updated: GroupUpdatedSummary[] = [];
|
|
484
|
+
for (const groupId of nextGroupIds.filter((id) => prevGroupIds.includes(id))) {
|
|
485
|
+
const prevChildren = getGroupChildren(prevNodeMap.get(groupId));
|
|
486
|
+
const nextChildren = getGroupChildren(nextNodeMap.get(groupId));
|
|
487
|
+
if (arraysEqual(prevChildren, nextChildren)) continue;
|
|
488
|
+
|
|
489
|
+
updated.push({
|
|
490
|
+
id: groupId,
|
|
491
|
+
title: getNodeTitle(nextNodeMap.get(groupId)),
|
|
492
|
+
addedChildIds: nextChildren.filter((id) => !prevChildren.includes(id)),
|
|
493
|
+
removedChildIds: prevChildren.filter((id) => !nextChildren.includes(id)),
|
|
494
|
+
childCount: nextChildren.length,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (created.length === 0 && updated.length === 0) return null;
|
|
499
|
+
return {
|
|
500
|
+
type: 'group',
|
|
501
|
+
...meta,
|
|
502
|
+
created,
|
|
503
|
+
updated,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private buildMoveEndEvent(
|
|
508
|
+
_prevLayout: CanvasLayout,
|
|
509
|
+
_nextLayout: CanvasLayout,
|
|
510
|
+
prevNodeMap: Map<string, CanvasNodeState>,
|
|
511
|
+
nextNodeMap: Map<string, CanvasNodeState>,
|
|
512
|
+
prevSpatial: SpatialContext,
|
|
513
|
+
nextSpatial: SpatialContext,
|
|
514
|
+
meta: EventMeta,
|
|
515
|
+
): MoveEndWatchEvent | null {
|
|
516
|
+
const movedIds = sortIds(
|
|
517
|
+
Array.from(nextNodeMap.keys()).filter((id) => {
|
|
518
|
+
const prev = prevNodeMap.get(id);
|
|
519
|
+
const next = nextNodeMap.get(id);
|
|
520
|
+
if (!prev || !next) return false;
|
|
521
|
+
return prev.position.x !== next.position.x || prev.position.y !== next.position.y;
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
if (movedIds.length === 0) return null;
|
|
526
|
+
|
|
527
|
+
const prevPeerMap = buildClusterPeerMap(prevSpatial);
|
|
528
|
+
const nextPeerMap = buildClusterPeerMap(nextSpatial);
|
|
529
|
+
const prevNeighborhoodMap = buildNeighborhoodMap(prevSpatial);
|
|
530
|
+
const nextNeighborhoodMap = buildNeighborhoodMap(nextSpatial);
|
|
531
|
+
const reasonsByNode = new Map<string, Set<string>>();
|
|
532
|
+
|
|
533
|
+
const pushReason = (nodeId: string, reason: string): void => {
|
|
534
|
+
const current = reasonsByNode.get(nodeId) ?? new Set<string>();
|
|
535
|
+
current.add(reason);
|
|
536
|
+
reasonsByNode.set(nodeId, current);
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
for (const nodeId of movedIds) {
|
|
540
|
+
const oldPeers = prevPeerMap.get(nodeId) ?? [];
|
|
541
|
+
const newPeers = nextPeerMap.get(nodeId) ?? [];
|
|
542
|
+
if (!arraysEqual(oldPeers, newPeers)) {
|
|
543
|
+
if (oldPeers.length === 0 && newPeers.length > 0) {
|
|
544
|
+
pushReason(nodeId, 'joined cluster');
|
|
545
|
+
} else if (oldPeers.length > 0 && newPeers.length === 0) {
|
|
546
|
+
pushReason(nodeId, 'left cluster');
|
|
547
|
+
} else {
|
|
548
|
+
pushReason(nodeId, 'cluster changed');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const pinIds = new Set<string>([
|
|
554
|
+
...prevNeighborhoodMap.keys(),
|
|
555
|
+
...nextNeighborhoodMap.keys(),
|
|
556
|
+
]);
|
|
557
|
+
for (const pinId of pinIds) {
|
|
558
|
+
const oldNeighbors = prevNeighborhoodMap.get(pinId) ?? [];
|
|
559
|
+
const newNeighbors = nextNeighborhoodMap.get(pinId) ?? [];
|
|
560
|
+
if (arraysEqual(oldNeighbors, newNeighbors)) continue;
|
|
561
|
+
|
|
562
|
+
const pinTitle = getNodeTitle(nextNodeMap.get(pinId) ?? prevNodeMap.get(pinId)) ?? pinId;
|
|
563
|
+
const added = newNeighbors.filter((id) => !oldNeighbors.includes(id));
|
|
564
|
+
const removed = oldNeighbors.filter((id) => !newNeighbors.includes(id));
|
|
565
|
+
|
|
566
|
+
for (const nodeId of added) {
|
|
567
|
+
if (movedIds.includes(nodeId)) {
|
|
568
|
+
pushReason(nodeId, `entered pinned neighborhood of "${pinTitle}"`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (const nodeId of removed) {
|
|
572
|
+
if (movedIds.includes(nodeId)) {
|
|
573
|
+
pushReason(nodeId, `left pinned neighborhood of "${pinTitle}"`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (movedIds.includes(pinId)) {
|
|
577
|
+
pushReason(pinId, 'pinned neighborhood changed');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const movedNodes = movedIds
|
|
582
|
+
.filter((id) => (reasonsByNode.get(id)?.size ?? 0) > 0)
|
|
583
|
+
.map((id) => {
|
|
584
|
+
const node = nextNodeMap.get(id);
|
|
585
|
+
return {
|
|
586
|
+
...summarizeNode(node, id),
|
|
587
|
+
reasons: Array.from(reasonsByNode.get(id) ?? []).sort((a, b) => a.localeCompare(b)),
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
if (movedNodes.length === 0) return null;
|
|
592
|
+
return {
|
|
593
|
+
type: 'move-end',
|
|
594
|
+
...meta,
|
|
595
|
+
nodes: movedNodes,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|