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,1279 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { recomputeCodeGraph } from './code-graph.js';
|
|
4
|
+
import {
|
|
5
|
+
canvasState,
|
|
6
|
+
type CanvasEdge,
|
|
7
|
+
IMAGE_MIME_MAP,
|
|
8
|
+
type CanvasNodeState,
|
|
9
|
+
type CanvasNodeUpdate,
|
|
10
|
+
type CanvasSnapshot,
|
|
11
|
+
} from './canvas-state.js';
|
|
12
|
+
import { rewatchAllFileNodes, unwatchAll, unwatchFileForNode, watchFileForNode } from './file-watcher.js';
|
|
13
|
+
import {
|
|
14
|
+
closeMcpAppSession,
|
|
15
|
+
hasMcpAppSession,
|
|
16
|
+
listMcpAppSessionIds,
|
|
17
|
+
openMcpApp,
|
|
18
|
+
type ExternalMcpTransportConfig,
|
|
19
|
+
} from './mcp-app-runtime.js';
|
|
20
|
+
import { mutationHistory } from './mutation-history.js';
|
|
21
|
+
import { computeGroupBounds, findOpenCanvasPosition } from './placement.js';
|
|
22
|
+
import { searchNodes } from './spatial-analysis.js';
|
|
23
|
+
import { getCanvasNodeTitle, serializeCanvasNode, type SerializedCanvasNode } from './canvas-serialization.js';
|
|
24
|
+
import {
|
|
25
|
+
buildGraphSpec,
|
|
26
|
+
createJsonRenderNodeData,
|
|
27
|
+
GRAPH_NODE_SIZE,
|
|
28
|
+
JSON_RENDER_NODE_SIZE,
|
|
29
|
+
normalizeAndValidateJsonRenderSpec,
|
|
30
|
+
type GraphNodeInput,
|
|
31
|
+
type JsonRenderNodeInput,
|
|
32
|
+
type JsonRenderSpec,
|
|
33
|
+
} from '../json-render/server.js';
|
|
34
|
+
import {
|
|
35
|
+
fetchWebpageSnapshot,
|
|
36
|
+
getWebpageFetchErrorDetails,
|
|
37
|
+
normalizeWebpageUrl,
|
|
38
|
+
} from './webpage-node.js';
|
|
39
|
+
|
|
40
|
+
export type CanvasArrangeMode = 'grid' | 'column' | 'flow';
|
|
41
|
+
export type CanvasPinMode = 'set' | 'add' | 'remove';
|
|
42
|
+
|
|
43
|
+
interface CanvasAddNodeInput {
|
|
44
|
+
type: CanvasNodeState['type'];
|
|
45
|
+
title?: string;
|
|
46
|
+
content?: string;
|
|
47
|
+
data?: Record<string, unknown>;
|
|
48
|
+
x?: number;
|
|
49
|
+
y?: number;
|
|
50
|
+
width?: number;
|
|
51
|
+
height?: number;
|
|
52
|
+
defaultWidth?: number;
|
|
53
|
+
defaultHeight?: number;
|
|
54
|
+
fileMode?: 'path' | 'inline' | 'auto';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface CanvasCreateGroupInput {
|
|
58
|
+
title?: string;
|
|
59
|
+
childIds?: string[];
|
|
60
|
+
x?: number;
|
|
61
|
+
y?: number;
|
|
62
|
+
width?: number;
|
|
63
|
+
height?: number;
|
|
64
|
+
color?: string;
|
|
65
|
+
childLayout?: CanvasArrangeMode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CanvasBatchOperation {
|
|
69
|
+
op: string;
|
|
70
|
+
assign?: string;
|
|
71
|
+
args?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface CanvasNodeLookupInput {
|
|
75
|
+
id?: string;
|
|
76
|
+
search?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const MAX_CONTEXT_PINS = 20;
|
|
80
|
+
|
|
81
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
82
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isExtAppNode(node: CanvasNodeState | undefined): node is CanvasNodeState {
|
|
86
|
+
return node?.type === 'mcp-app' && node.data.mode === 'ext-app';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getExtAppSessionId(node: CanvasNodeState | undefined): string | null {
|
|
90
|
+
if (!isExtAppNode(node)) return null;
|
|
91
|
+
const sessionId = node.data.appSessionId;
|
|
92
|
+
return typeof sessionId === 'string' && sessionId.trim().length > 0 ? sessionId.trim() : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeTransportConfig(value: unknown): ExternalMcpTransportConfig | null {
|
|
96
|
+
if (!isRecord(value) || typeof value.type !== 'string') return null;
|
|
97
|
+
|
|
98
|
+
if (value.type === 'http') {
|
|
99
|
+
const url = typeof value.url === 'string' ? value.url.trim() : '';
|
|
100
|
+
if (!url) return null;
|
|
101
|
+
const headers = isRecord(value.headers)
|
|
102
|
+
? Object.fromEntries(
|
|
103
|
+
Object.entries(value.headers)
|
|
104
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
105
|
+
.map(([key, headerValue]) => [key, headerValue]),
|
|
106
|
+
)
|
|
107
|
+
: undefined;
|
|
108
|
+
return {
|
|
109
|
+
type: 'http',
|
|
110
|
+
url,
|
|
111
|
+
...(headers && Object.keys(headers).length > 0 ? { headers } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (value.type === 'stdio') {
|
|
116
|
+
const command = typeof value.command === 'string' ? value.command.trim() : '';
|
|
117
|
+
if (!command) return null;
|
|
118
|
+
const args = Array.isArray(value.args)
|
|
119
|
+
? value.args.filter((entry): entry is string => typeof entry === 'string')
|
|
120
|
+
: undefined;
|
|
121
|
+
const env = isRecord(value.env)
|
|
122
|
+
? Object.fromEntries(
|
|
123
|
+
Object.entries(value.env)
|
|
124
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
125
|
+
.map(([key, envValue]) => [key, envValue]),
|
|
126
|
+
)
|
|
127
|
+
: undefined;
|
|
128
|
+
return {
|
|
129
|
+
type: 'stdio',
|
|
130
|
+
command,
|
|
131
|
+
...(args && args.length > 0 ? { args } : {}),
|
|
132
|
+
...(typeof value.cwd === 'string' && value.cwd.trim().length > 0 ? { cwd: value.cwd.trim() } : {}),
|
|
133
|
+
...(env && Object.keys(env).length > 0 ? { env } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function setExtAppRuntimeState(
|
|
141
|
+
nodeId: string,
|
|
142
|
+
patch: {
|
|
143
|
+
appSessionId?: string | null;
|
|
144
|
+
html?: string;
|
|
145
|
+
toolInput?: Record<string, unknown>;
|
|
146
|
+
toolResult?: unknown;
|
|
147
|
+
resourceUri?: string;
|
|
148
|
+
toolDefinition?: unknown;
|
|
149
|
+
resourceMeta?: unknown;
|
|
150
|
+
serverName?: string;
|
|
151
|
+
toolName?: string;
|
|
152
|
+
transportConfig?: ExternalMcpTransportConfig;
|
|
153
|
+
sessionStatus?: 'ready' | 'rehydrating' | 'error';
|
|
154
|
+
sessionError?: string | null;
|
|
155
|
+
},
|
|
156
|
+
): void {
|
|
157
|
+
const current = canvasState.getNode(nodeId);
|
|
158
|
+
if (!isExtAppNode(current)) return;
|
|
159
|
+
|
|
160
|
+
const nextData: Record<string, unknown> = { ...current.data };
|
|
161
|
+
if ('appSessionId' in patch) {
|
|
162
|
+
if (typeof patch.appSessionId === 'string' && patch.appSessionId.trim().length > 0) {
|
|
163
|
+
nextData.appSessionId = patch.appSessionId;
|
|
164
|
+
} else {
|
|
165
|
+
delete nextData.appSessionId;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if ('html' in patch && typeof patch.html === 'string') nextData.html = patch.html;
|
|
169
|
+
if ('toolInput' in patch && patch.toolInput) nextData.toolInput = patch.toolInput;
|
|
170
|
+
if ('toolResult' in patch && patch.toolResult !== undefined) nextData.toolResult = patch.toolResult;
|
|
171
|
+
if ('resourceUri' in patch && typeof patch.resourceUri === 'string') nextData.resourceUri = patch.resourceUri;
|
|
172
|
+
if ('toolDefinition' in patch && patch.toolDefinition !== undefined) nextData.toolDefinition = patch.toolDefinition;
|
|
173
|
+
if ('resourceMeta' in patch && patch.resourceMeta !== undefined) nextData.resourceMeta = patch.resourceMeta;
|
|
174
|
+
if ('serverName' in patch && typeof patch.serverName === 'string') nextData.serverName = patch.serverName;
|
|
175
|
+
if ('toolName' in patch && typeof patch.toolName === 'string') nextData.toolName = patch.toolName;
|
|
176
|
+
if ('transportConfig' in patch && patch.transportConfig) nextData.transportConfig = patch.transportConfig;
|
|
177
|
+
if ('sessionStatus' in patch && patch.sessionStatus) nextData.sessionStatus = patch.sessionStatus;
|
|
178
|
+
if ('sessionError' in patch) {
|
|
179
|
+
if (typeof patch.sessionError === 'string' && patch.sessionError.trim().length > 0) {
|
|
180
|
+
nextData.sessionError = patch.sessionError;
|
|
181
|
+
} else {
|
|
182
|
+
delete nextData.sessionError;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
canvasState.updateNode(nodeId, { data: nextData });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function prepareExtAppNodesForSessionSync(forceRehydrate: boolean): string[] {
|
|
190
|
+
const currentLayout = canvasState.getLayout();
|
|
191
|
+
const targetIds: string[] = [];
|
|
192
|
+
|
|
193
|
+
canvasState.withSuppressedRecording(() => {
|
|
194
|
+
for (const node of currentLayout.nodes) {
|
|
195
|
+
if (!isExtAppNode(node)) continue;
|
|
196
|
+
const sessionId = getExtAppSessionId(node);
|
|
197
|
+
const needsRehydrate = forceRehydrate || !sessionId || !hasMcpAppSession(sessionId);
|
|
198
|
+
if (!needsRehydrate) continue;
|
|
199
|
+
|
|
200
|
+
const transportConfig = normalizeTransportConfig(node.data.transportConfig);
|
|
201
|
+
if (!transportConfig) {
|
|
202
|
+
setExtAppRuntimeState(node.id, {
|
|
203
|
+
appSessionId: null,
|
|
204
|
+
sessionStatus: 'error',
|
|
205
|
+
sessionError: 'Saved app session cannot be restored because its transport details are missing. Reopen the app to restore interactivity.',
|
|
206
|
+
});
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
setExtAppRuntimeState(node.id, {
|
|
211
|
+
appSessionId: null,
|
|
212
|
+
transportConfig,
|
|
213
|
+
sessionStatus: 'rehydrating',
|
|
214
|
+
sessionError: null,
|
|
215
|
+
});
|
|
216
|
+
targetIds.push(node.id);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return targetIds;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function primeCanvasRuntimeBackends(
|
|
224
|
+
options: { forceRehydrateExtApps?: boolean } = {},
|
|
225
|
+
): { targetIds: string[] } {
|
|
226
|
+
const forceRehydrateExtApps = options.forceRehydrateExtApps === true;
|
|
227
|
+
rewatchAllFileNodes();
|
|
228
|
+
|
|
229
|
+
const layout = canvasState.getLayout();
|
|
230
|
+
const referencedSessionIds = new Set(
|
|
231
|
+
layout.nodes
|
|
232
|
+
.map((node) => getExtAppSessionId(node))
|
|
233
|
+
.filter((sessionId): sessionId is string => typeof sessionId === 'string' && sessionId.length > 0),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
for (const sessionId of listMcpAppSessionIds()) {
|
|
237
|
+
if (forceRehydrateExtApps || !referencedSessionIds.has(sessionId)) {
|
|
238
|
+
closeMcpAppSession(sessionId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { targetIds: prepareExtAppNodesForSessionSync(forceRehydrateExtApps) };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function syncCanvasRuntimeBackends(
|
|
246
|
+
options: { forceRehydrateExtApps?: boolean; alreadyPrimed?: boolean } = {},
|
|
247
|
+
): Promise<{ rehydrated: number; failed: number }> {
|
|
248
|
+
const targetIds = options.alreadyPrimed === true
|
|
249
|
+
? canvasState.getLayout().nodes
|
|
250
|
+
.filter((node) => isExtAppNode(node) && node.data.sessionStatus === 'rehydrating')
|
|
251
|
+
.map((node) => node.id)
|
|
252
|
+
: primeCanvasRuntimeBackends(options).targetIds;
|
|
253
|
+
let rehydrated = 0;
|
|
254
|
+
let failed = 0;
|
|
255
|
+
|
|
256
|
+
for (const nodeId of targetIds) {
|
|
257
|
+
const current = canvasState.getNode(nodeId);
|
|
258
|
+
if (!isExtAppNode(current)) continue;
|
|
259
|
+
|
|
260
|
+
const transport = normalizeTransportConfig(current.data.transportConfig);
|
|
261
|
+
const toolName = typeof current.data.toolName === 'string' ? current.data.toolName.trim() : '';
|
|
262
|
+
if (!transport || !toolName) {
|
|
263
|
+
canvasState.withSuppressedRecording(() => {
|
|
264
|
+
setExtAppRuntimeState(nodeId, {
|
|
265
|
+
appSessionId: null,
|
|
266
|
+
sessionStatus: 'error',
|
|
267
|
+
sessionError: 'Saved app session cannot be restored because its launch metadata is incomplete. Reopen the app to restore interactivity.',
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
failed++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const opened = await openMcpApp({
|
|
276
|
+
transport,
|
|
277
|
+
toolName,
|
|
278
|
+
...(isRecord(current.data.toolInput) ? { toolArguments: current.data.toolInput } : {}),
|
|
279
|
+
...(typeof current.data.serverName === 'string' && current.data.serverName.trim().length > 0
|
|
280
|
+
? { serverName: current.data.serverName.trim() }
|
|
281
|
+
: {}),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
canvasState.withSuppressedRecording(() => {
|
|
285
|
+
setExtAppRuntimeState(nodeId, {
|
|
286
|
+
appSessionId: opened.sessionId,
|
|
287
|
+
html: opened.html,
|
|
288
|
+
toolInput: opened.toolInput,
|
|
289
|
+
toolResult: opened.toolResult,
|
|
290
|
+
resourceUri: opened.resourceUri,
|
|
291
|
+
toolDefinition: opened.tool,
|
|
292
|
+
resourceMeta: opened.resourceMeta,
|
|
293
|
+
serverName: opened.serverName,
|
|
294
|
+
toolName: opened.toolName,
|
|
295
|
+
transportConfig: transport,
|
|
296
|
+
sessionStatus: 'ready',
|
|
297
|
+
sessionError: null,
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
rehydrated++;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
canvasState.withSuppressedRecording(() => {
|
|
303
|
+
setExtAppRuntimeState(nodeId, {
|
|
304
|
+
appSessionId: null,
|
|
305
|
+
sessionStatus: 'error',
|
|
306
|
+
sessionError: error instanceof Error ? error.message : String(error),
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
failed++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { rehydrated, failed };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function validateCanvasNodePatch(patch: {
|
|
317
|
+
position?: { x: number; y: number };
|
|
318
|
+
size?: { width: number; height: number };
|
|
319
|
+
}): string | null {
|
|
320
|
+
if (patch.position) {
|
|
321
|
+
if (!Number.isFinite(patch.position.x) || !Number.isFinite(patch.position.y)) {
|
|
322
|
+
return 'Position must contain finite x and y values.';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (patch.size) {
|
|
326
|
+
if (!Number.isFinite(patch.size.width) || !Number.isFinite(patch.size.height)) {
|
|
327
|
+
return 'Size must contain finite width and height values.';
|
|
328
|
+
}
|
|
329
|
+
if (patch.size.width <= 0 || patch.size.height <= 0) {
|
|
330
|
+
return 'Size width and height must be greater than zero.';
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let codeGraphTimer: ReturnType<typeof setTimeout> | null = null;
|
|
337
|
+
|
|
338
|
+
function shouldTreatFileContentAsPath(input: CanvasAddNodeInput): boolean {
|
|
339
|
+
if (input.fileMode === 'path') return true;
|
|
340
|
+
if (input.fileMode === 'inline') return false;
|
|
341
|
+
|
|
342
|
+
const content = input.content?.trim() ?? '';
|
|
343
|
+
if (!content || content.includes('\n') || content.includes('\r')) return false;
|
|
344
|
+
if (typeof input.data?.path === 'string' && input.data.path.length > 0) return true;
|
|
345
|
+
if (existsSync(resolve(content))) return true;
|
|
346
|
+
if (!input.title) return true;
|
|
347
|
+
return content.startsWith('/') || content.startsWith('./') || content.startsWith('../') || content.includes('/');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildFileNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
|
|
351
|
+
if (!shouldTreatFileContentAsPath(input)) {
|
|
352
|
+
return {
|
|
353
|
+
...(input.data ?? {}),
|
|
354
|
+
...(input.title ? { title: input.title } : {}),
|
|
355
|
+
...(input.content ? { content: input.content } : {}),
|
|
356
|
+
...(input.content && input.title
|
|
357
|
+
? {
|
|
358
|
+
fileContent: input.content,
|
|
359
|
+
lineCount: input.content.split('\n').length,
|
|
360
|
+
}
|
|
361
|
+
: {}),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const rawPath = typeof input.data?.path === 'string' && input.data.path.length > 0
|
|
366
|
+
? input.data.path
|
|
367
|
+
: (input.content ?? '');
|
|
368
|
+
const resolved = resolve(rawPath);
|
|
369
|
+
const fileName = resolved.split('/').pop() ?? rawPath;
|
|
370
|
+
const data: Record<string, unknown> = {
|
|
371
|
+
...(input.data ?? {}),
|
|
372
|
+
path: resolved,
|
|
373
|
+
title: input.title ?? fileName,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
if (existsSync(resolved)) {
|
|
378
|
+
const fileContent = readFileSync(resolved, 'utf-8');
|
|
379
|
+
const stat = statSync(resolved);
|
|
380
|
+
data.fileContent = fileContent;
|
|
381
|
+
data.lineCount = fileContent.split('\n').length;
|
|
382
|
+
data.updatedAt = new Date(stat.mtimeMs).toISOString();
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
// Missing or unreadable files still render as path-backed file nodes.
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return data;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function buildImageNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
|
|
392
|
+
const src = input.content ?? '';
|
|
393
|
+
const isDataUri = src.startsWith('data:');
|
|
394
|
+
const isUrl = src.startsWith('http://') || src.startsWith('https://');
|
|
395
|
+
if (!isDataUri && !isUrl && src) {
|
|
396
|
+
const resolved = resolve(src);
|
|
397
|
+
const fileName = resolved.split('/').pop() ?? src;
|
|
398
|
+
return {
|
|
399
|
+
...(input.data ?? {}),
|
|
400
|
+
src: resolved,
|
|
401
|
+
title: input.title ?? fileName,
|
|
402
|
+
path: resolved,
|
|
403
|
+
...(IMAGE_MIME_MAP[resolved.split('.').pop()?.toLowerCase() ?? '']
|
|
404
|
+
? { mimeType: IMAGE_MIME_MAP[resolved.split('.').pop()?.toLowerCase() ?? ''] }
|
|
405
|
+
: {}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
...(input.data ?? {}),
|
|
411
|
+
src,
|
|
412
|
+
title: input.title ?? (isUrl ? src.split('/').pop() ?? 'Image' : 'Image'),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function buildWebpageNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
|
|
417
|
+
const rawUrl = typeof input.data?.url === 'string' && input.data.url.length > 0
|
|
418
|
+
? input.data.url
|
|
419
|
+
: (input.content ?? '');
|
|
420
|
+
const url = normalizeWebpageUrl(rawUrl);
|
|
421
|
+
const explicitTitle = typeof input.title === 'string' && input.title.trim().length > 0
|
|
422
|
+
? input.title.trim()
|
|
423
|
+
: typeof input.data?.title === 'string' && input.data.title.trim().length > 0
|
|
424
|
+
? input.data.title.trim()
|
|
425
|
+
: '';
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
...(input.data ?? {}),
|
|
429
|
+
url,
|
|
430
|
+
title: explicitTitle || url,
|
|
431
|
+
titleSource: explicitTitle ? 'user' : 'page',
|
|
432
|
+
status: 'idle',
|
|
433
|
+
content: typeof input.data?.content === 'string' ? input.data.content : '',
|
|
434
|
+
excerpt: typeof input.data?.excerpt === 'string' ? input.data.excerpt : '',
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function buildNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
|
|
439
|
+
if (input.type === 'file') return buildFileNodeData(input);
|
|
440
|
+
if (input.type === 'image') return buildImageNodeData(input);
|
|
441
|
+
if (input.type === 'webpage') return buildWebpageNodeData(input);
|
|
442
|
+
return {
|
|
443
|
+
...(input.data ?? {}),
|
|
444
|
+
...(input.title ? { title: input.title } : {}),
|
|
445
|
+
...(input.content ? { content: input.content } : {}),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function scheduleCodeGraphRecompute(onComplete?: () => void): void {
|
|
450
|
+
if (codeGraphTimer) clearTimeout(codeGraphTimer);
|
|
451
|
+
codeGraphTimer = setTimeout(() => {
|
|
452
|
+
codeGraphTimer = null;
|
|
453
|
+
recomputeCodeGraph();
|
|
454
|
+
onComplete?.();
|
|
455
|
+
}, 300);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function addCanvasNode(input: CanvasAddNodeInput): {
|
|
459
|
+
id: string;
|
|
460
|
+
node: CanvasNodeState;
|
|
461
|
+
needsCodeGraphRecompute: boolean;
|
|
462
|
+
} {
|
|
463
|
+
if (input.type === 'json-render' || input.type === 'graph') {
|
|
464
|
+
throw new Error(`Use the dedicated ${input.type} node APIs for structured viewer nodes.`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const width = input.width ?? input.defaultWidth ?? 720;
|
|
468
|
+
const height = input.height ?? input.defaultHeight ?? 600;
|
|
469
|
+
const position = input.x !== undefined && input.y !== undefined
|
|
470
|
+
? { x: input.x, y: input.y }
|
|
471
|
+
: findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
|
|
472
|
+
const id = `node-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
473
|
+
const data = buildNodeData(input);
|
|
474
|
+
const node: CanvasNodeState = {
|
|
475
|
+
id,
|
|
476
|
+
type: input.type,
|
|
477
|
+
position,
|
|
478
|
+
size: { width, height },
|
|
479
|
+
zIndex: 1,
|
|
480
|
+
collapsed: false,
|
|
481
|
+
pinned: false,
|
|
482
|
+
dockPosition: null,
|
|
483
|
+
data,
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
canvasState.addNode(node);
|
|
487
|
+
const storedNode = canvasState.getNode(id) ?? node;
|
|
488
|
+
|
|
489
|
+
const filePath = input.type === 'file' && typeof data.path === 'string' ? data.path : null;
|
|
490
|
+
if (filePath) {
|
|
491
|
+
watchFileForNode(id, filePath);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return { id, node: storedNode, needsCodeGraphRecompute: input.type === 'file' };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput): {
|
|
498
|
+
ok: true;
|
|
499
|
+
node: CanvasNodeState;
|
|
500
|
+
} | {
|
|
501
|
+
ok: false;
|
|
502
|
+
error: string;
|
|
503
|
+
} {
|
|
504
|
+
if (typeof nodeRef.id === 'string' && nodeRef.id.trim().length > 0) {
|
|
505
|
+
const node = canvasState.getNode(nodeRef.id.trim());
|
|
506
|
+
if (!node) {
|
|
507
|
+
return { ok: false, error: `Node "${nodeRef.id}" not found.` };
|
|
508
|
+
}
|
|
509
|
+
return { ok: true, node };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (typeof nodeRef.search === 'string' && nodeRef.search.trim().length > 0) {
|
|
513
|
+
const query = nodeRef.search.trim();
|
|
514
|
+
const layout = canvasState.getLayout();
|
|
515
|
+
const exactTitleMatches = layout.nodes.filter((node) => {
|
|
516
|
+
const title = getCanvasNodeTitle(node);
|
|
517
|
+
return title !== null && title.toLowerCase() === query.toLowerCase();
|
|
518
|
+
});
|
|
519
|
+
if (exactTitleMatches.length === 1) {
|
|
520
|
+
return { ok: true, node: exactTitleMatches[0]! };
|
|
521
|
+
}
|
|
522
|
+
if (exactTitleMatches.length > 1) {
|
|
523
|
+
return {
|
|
524
|
+
ok: false,
|
|
525
|
+
error: `Search "${query}" is ambiguous. Exact title matches: ${exactTitleMatches.map((node) => `${getCanvasNodeTitle(node) ?? node.id} (${node.id})`).join(', ')}`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const matches = searchNodes(layout.nodes, query);
|
|
530
|
+
if (matches.length === 0) {
|
|
531
|
+
return { ok: false, error: `No node matches search "${query}".` };
|
|
532
|
+
}
|
|
533
|
+
if (matches.length > 1) {
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
error: `Search "${query}" is ambiguous. Matches: ${matches.slice(0, 5).map((match) => `${match.title ?? match.id} (${match.id})`).join(', ')}`,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const node = canvasState.getNode(matches[0]!.id);
|
|
540
|
+
if (!node) {
|
|
541
|
+
return { ok: false, error: `Resolved node "${matches[0]!.id}" disappeared.` };
|
|
542
|
+
}
|
|
543
|
+
return { ok: true, node };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return { ok: false, error: 'Missing node reference. Provide either an id or a search query.' };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export async function refreshCanvasWebpageNode(
|
|
550
|
+
id: string,
|
|
551
|
+
options: { url?: string } = {},
|
|
552
|
+
): Promise<{ ok: boolean; id: string; error?: string }> {
|
|
553
|
+
const existing = canvasState.getNode(id);
|
|
554
|
+
if (!existing || existing.type !== 'webpage') {
|
|
555
|
+
return { ok: false, id, error: `Webpage node "${id}" not found.` };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const currentData = existing.data;
|
|
559
|
+
const configuredUrl = typeof options.url === 'string' && options.url.trim().length > 0
|
|
560
|
+
? options.url
|
|
561
|
+
: typeof currentData.url === 'string'
|
|
562
|
+
? currentData.url
|
|
563
|
+
: '';
|
|
564
|
+
|
|
565
|
+
let normalizedUrl: string;
|
|
566
|
+
try {
|
|
567
|
+
normalizedUrl = normalizeWebpageUrl(configuredUrl);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
canvasState.updateNode(id, {
|
|
570
|
+
data: {
|
|
571
|
+
...currentData,
|
|
572
|
+
status: 'error',
|
|
573
|
+
error: error instanceof Error ? error.message : 'Invalid webpage URL.',
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
return {
|
|
577
|
+
ok: false,
|
|
578
|
+
id,
|
|
579
|
+
error: error instanceof Error ? error.message : 'Invalid webpage URL.',
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const fetchingData: Record<string, unknown> = {
|
|
584
|
+
...currentData,
|
|
585
|
+
url: normalizedUrl,
|
|
586
|
+
status: 'fetching',
|
|
587
|
+
};
|
|
588
|
+
delete fetchingData.error;
|
|
589
|
+
canvasState.updateNode(id, { data: fetchingData });
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
const snapshot = await fetchWebpageSnapshot(normalizedUrl);
|
|
593
|
+
const latest = canvasState.getNode(id);
|
|
594
|
+
if (!latest || latest.type !== 'webpage') {
|
|
595
|
+
return { ok: false, id, error: `Webpage node "${id}" disappeared during refresh.` };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const latestData = latest.data;
|
|
599
|
+
const titleSource = latestData.titleSource === 'user' ? 'user' : 'page';
|
|
600
|
+
const currentTitle = typeof latestData.title === 'string' ? latestData.title.trim() : '';
|
|
601
|
+
const nextTitle = titleSource === 'user' && currentTitle.length > 0
|
|
602
|
+
? currentTitle
|
|
603
|
+
: snapshot.pageTitle ?? snapshot.url;
|
|
604
|
+
|
|
605
|
+
const nextData: Record<string, unknown> = {
|
|
606
|
+
...latestData,
|
|
607
|
+
url: snapshot.url,
|
|
608
|
+
title: nextTitle,
|
|
609
|
+
titleSource,
|
|
610
|
+
pageTitle: snapshot.pageTitle,
|
|
611
|
+
description: snapshot.description,
|
|
612
|
+
imageUrl: snapshot.imageUrl,
|
|
613
|
+
content: snapshot.content,
|
|
614
|
+
excerpt: snapshot.excerpt,
|
|
615
|
+
fetchedAt: snapshot.fetchedAt,
|
|
616
|
+
status: 'ready',
|
|
617
|
+
statusCode: snapshot.statusCode,
|
|
618
|
+
contentType: snapshot.contentType,
|
|
619
|
+
frameBlocked: snapshot.frameBlocked,
|
|
620
|
+
frameBlockedReason: snapshot.frameBlockedReason,
|
|
621
|
+
};
|
|
622
|
+
delete nextData.error;
|
|
623
|
+
|
|
624
|
+
canvasState.updateNode(id, { data: nextData });
|
|
625
|
+
return { ok: true, id };
|
|
626
|
+
} catch (error) {
|
|
627
|
+
const details = getWebpageFetchErrorDetails(error);
|
|
628
|
+
const latest = canvasState.getNode(id);
|
|
629
|
+
if (latest?.type === 'webpage') {
|
|
630
|
+
canvasState.updateNode(id, {
|
|
631
|
+
data: {
|
|
632
|
+
...latest.data,
|
|
633
|
+
url: normalizedUrl,
|
|
634
|
+
fetchedAt: new Date().toISOString(),
|
|
635
|
+
status: 'error',
|
|
636
|
+
error: details.message,
|
|
637
|
+
...(details.statusCode !== null ? { statusCode: details.statusCode } : {}),
|
|
638
|
+
...(details.contentType !== null ? { contentType: details.contentType } : {}),
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return { ok: false, id, error: details.message };
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function removeCanvasNode(id: string): {
|
|
647
|
+
removed: boolean;
|
|
648
|
+
needsCodeGraphRecompute: boolean;
|
|
649
|
+
} {
|
|
650
|
+
const existing = canvasState.getNode(id);
|
|
651
|
+
if (!existing) {
|
|
652
|
+
return { removed: false, needsCodeGraphRecompute: false };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (existing.type === 'file') {
|
|
656
|
+
unwatchFileForNode(id, typeof existing.data.path === 'string' ? existing.data.path : undefined);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
canvasState.removeNode(id);
|
|
660
|
+
return { removed: true, needsCodeGraphRecompute: existing.type === 'file' };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function isArrangeLocked(node: CanvasNodeState): boolean {
|
|
664
|
+
return node.pinned || node.data.arrangeLocked === true;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function collectArrangeExcludedNodeIds(nodes: CanvasNodeState[]): Set<string> {
|
|
668
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
669
|
+
const excludedGroupIds = new Set<string>();
|
|
670
|
+
|
|
671
|
+
for (const node of nodes) {
|
|
672
|
+
if (node.type !== 'group') continue;
|
|
673
|
+
const childIds = Array.isArray(node.data.children)
|
|
674
|
+
? node.data.children.filter((id): id is string => typeof id === 'string')
|
|
675
|
+
: [];
|
|
676
|
+
const hasLockedChild = childIds.some((childId) => {
|
|
677
|
+
const child = nodesById.get(childId);
|
|
678
|
+
return child ? isArrangeLocked(child) : false;
|
|
679
|
+
});
|
|
680
|
+
if (isArrangeLocked(node) || hasLockedChild) {
|
|
681
|
+
excludedGroupIds.add(node.id);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const excluded = new Set<string>();
|
|
686
|
+
for (const node of nodes) {
|
|
687
|
+
const parentGroup = typeof node.data.parentGroup === 'string' ? node.data.parentGroup : null;
|
|
688
|
+
if (isArrangeLocked(node) || (parentGroup && excludedGroupIds.has(parentGroup))) {
|
|
689
|
+
excluded.add(node.id);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
for (const groupId of excludedGroupIds) {
|
|
694
|
+
excluded.add(groupId);
|
|
695
|
+
const group = nodesById.get(groupId);
|
|
696
|
+
const childIds = Array.isArray(group?.data.children)
|
|
697
|
+
? group.data.children.filter((id): id is string => typeof id === 'string')
|
|
698
|
+
: [];
|
|
699
|
+
for (const childId of childIds) excluded.add(childId);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return excluded;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function arrangeCanvasNodes(layout: CanvasArrangeMode): { arranged: number; layout: CanvasArrangeMode } {
|
|
706
|
+
const nodes = canvasState.getLayout().nodes;
|
|
707
|
+
const excludedIds = collectArrangeExcludedNodeIds(nodes);
|
|
708
|
+
const movableNodes = nodes.filter((node) => !excludedIds.has(node.id));
|
|
709
|
+
const gap = 24;
|
|
710
|
+
const oldPositions = nodes.map((node) => ({ id: node.id, position: { ...node.position } }));
|
|
711
|
+
|
|
712
|
+
canvasState.withSuppressedRecording(() => {
|
|
713
|
+
if (layout === 'column') {
|
|
714
|
+
let y = 80;
|
|
715
|
+
for (const node of movableNodes) {
|
|
716
|
+
canvasState.updateNode(node.id, { position: { x: 40, y } });
|
|
717
|
+
y += node.size.height + gap;
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (layout === 'flow') {
|
|
723
|
+
let x = 40;
|
|
724
|
+
for (const node of movableNodes) {
|
|
725
|
+
canvasState.updateNode(node.id, { position: { x, y: 80 } });
|
|
726
|
+
x += node.size.width + gap;
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const cols = Math.max(1, Math.floor(1440 / (360 + gap)));
|
|
732
|
+
let col = 0;
|
|
733
|
+
let rowY = 80;
|
|
734
|
+
let rowMaxHeight = 0;
|
|
735
|
+
for (const node of movableNodes) {
|
|
736
|
+
const x = 40 + col * (360 + gap);
|
|
737
|
+
canvasState.updateNode(node.id, { position: { x, y: rowY } });
|
|
738
|
+
rowMaxHeight = Math.max(rowMaxHeight, node.size.height);
|
|
739
|
+
col++;
|
|
740
|
+
if (col >= cols) {
|
|
741
|
+
col = 0;
|
|
742
|
+
rowY += rowMaxHeight + gap;
|
|
743
|
+
rowMaxHeight = 0;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const newPositions = nodes.map((node) => {
|
|
749
|
+
const updated = canvasState.getNode(node.id);
|
|
750
|
+
return { id: node.id, position: updated ? { ...updated.position } : { ...node.position } };
|
|
751
|
+
});
|
|
752
|
+
mutationHistory.record({
|
|
753
|
+
description: `Auto-arranged ${movableNodes.length} nodes (${layout})`,
|
|
754
|
+
operationType: 'arrange',
|
|
755
|
+
forward: () => canvasState.withSuppressedRecording(() => {
|
|
756
|
+
for (const position of newPositions) canvasState.updateNode(position.id, { position: position.position });
|
|
757
|
+
}),
|
|
758
|
+
inverse: () => canvasState.withSuppressedRecording(() => {
|
|
759
|
+
for (const position of oldPositions) canvasState.updateNode(position.id, { position: position.position });
|
|
760
|
+
}),
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
return { arranged: movableNodes.length, layout };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function applyCanvasNodeUpdates(updates: CanvasNodeUpdate[]): { applied: number; skipped: number } {
|
|
767
|
+
const safe = updates.filter((update) => validateCanvasNodePatch(update) === null);
|
|
768
|
+
return canvasState.applyUpdates(safe);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export function setCanvasContextPins(
|
|
772
|
+
nodeIds: string[],
|
|
773
|
+
mode: CanvasPinMode = 'set',
|
|
774
|
+
): { count: number; nodeIds: string[] } {
|
|
775
|
+
const normalizePins = (ids: string[]): string[] => ids.filter((id, index) => ids.indexOf(id) === index).slice(0, MAX_CONTEXT_PINS);
|
|
776
|
+
const normalizedNodeIds = normalizePins(nodeIds);
|
|
777
|
+
if (mode === 'set') {
|
|
778
|
+
canvasState.setContextPins(normalizedNodeIds);
|
|
779
|
+
} else if (mode === 'add') {
|
|
780
|
+
const current = Array.from(canvasState.contextPinnedNodeIds);
|
|
781
|
+
canvasState.setContextPins(normalizePins([...current, ...normalizedNodeIds]));
|
|
782
|
+
} else {
|
|
783
|
+
const current = Array.from(canvasState.contextPinnedNodeIds);
|
|
784
|
+
canvasState.setContextPins(current.filter((id) => !normalizedNodeIds.includes(id)));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return {
|
|
788
|
+
count: canvasState.contextPinnedNodeIds.size,
|
|
789
|
+
nodeIds: Array.from(canvasState.contextPinnedNodeIds),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export function listCanvasSnapshots(): CanvasSnapshot[] {
|
|
794
|
+
return canvasState.listSnapshots();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export function saveCanvasSnapshot(name: string): CanvasSnapshot | null {
|
|
798
|
+
return canvasState.saveSnapshot(name);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export async function restoreCanvasSnapshot(idOrName: string): Promise<{ ok: boolean }> {
|
|
802
|
+
const ok = canvasState.restoreSnapshot(idOrName);
|
|
803
|
+
if (ok) {
|
|
804
|
+
await syncCanvasRuntimeBackends({ forceRehydrateExtApps: true });
|
|
805
|
+
canvasState.flushToDisk();
|
|
806
|
+
}
|
|
807
|
+
return { ok };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export function deleteCanvasSnapshot(id: string): { ok: boolean } {
|
|
811
|
+
return { ok: canvasState.deleteSnapshot(id) };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export function addCanvasEdge(input: {
|
|
815
|
+
from?: string;
|
|
816
|
+
to?: string;
|
|
817
|
+
fromSearch?: string;
|
|
818
|
+
toSearch?: string;
|
|
819
|
+
type: CanvasEdge['type'];
|
|
820
|
+
label?: string;
|
|
821
|
+
style?: CanvasEdge['style'];
|
|
822
|
+
animated?: boolean;
|
|
823
|
+
}): { id: string; from: string; to: string } {
|
|
824
|
+
const fromResult = resolveCanvasNode({
|
|
825
|
+
...(typeof input.from === 'string' ? { id: input.from } : {}),
|
|
826
|
+
...(typeof input.fromSearch === 'string' ? { search: input.fromSearch } : {}),
|
|
827
|
+
});
|
|
828
|
+
if (!fromResult.ok) {
|
|
829
|
+
throw new Error(fromResult.error);
|
|
830
|
+
}
|
|
831
|
+
const toResult = resolveCanvasNode({
|
|
832
|
+
...(typeof input.to === 'string' ? { id: input.to } : {}),
|
|
833
|
+
...(typeof input.toSearch === 'string' ? { search: input.toSearch } : {}),
|
|
834
|
+
});
|
|
835
|
+
if (!toResult.ok) {
|
|
836
|
+
throw new Error(toResult.error);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const id = `edge-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
840
|
+
const edge: CanvasEdge = {
|
|
841
|
+
id,
|
|
842
|
+
from: fromResult.node.id,
|
|
843
|
+
to: toResult.node.id,
|
|
844
|
+
type: input.type,
|
|
845
|
+
...(input.label ? { label: input.label } : {}),
|
|
846
|
+
...(input.style ? { style: input.style } : {}),
|
|
847
|
+
...(input.animated !== undefined ? { animated: input.animated } : {}),
|
|
848
|
+
};
|
|
849
|
+
const added = canvasState.addEdge(edge);
|
|
850
|
+
if (!added) {
|
|
851
|
+
throw new Error('Duplicate or self-edge.');
|
|
852
|
+
}
|
|
853
|
+
return { id, from: fromResult.node.id, to: toResult.node.id };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export function removeCanvasEdge(id: string): { removed: boolean } {
|
|
857
|
+
return { removed: canvasState.removeEdge(id) };
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export function createCanvasGroup(input: CanvasCreateGroupInput): { id: string; node: CanvasNodeState } {
|
|
861
|
+
let x = input.x;
|
|
862
|
+
let y = input.y;
|
|
863
|
+
let width = input.width ?? 600;
|
|
864
|
+
let height = input.height ?? 400;
|
|
865
|
+
const explicitFrame = input.x !== undefined || input.y !== undefined || input.width !== undefined || input.height !== undefined;
|
|
866
|
+
|
|
867
|
+
const childIds = input.childIds ?? [];
|
|
868
|
+
if (childIds.length > 0 && x === undefined && y === undefined) {
|
|
869
|
+
const childRects = childIds
|
|
870
|
+
.map((cid) => canvasState.getNode(cid))
|
|
871
|
+
.filter((node): node is CanvasNodeState => node !== undefined);
|
|
872
|
+
const bounds = computeGroupBounds(childRects);
|
|
873
|
+
if (bounds) {
|
|
874
|
+
x = bounds.x;
|
|
875
|
+
y = bounds.y;
|
|
876
|
+
width = bounds.width;
|
|
877
|
+
height = bounds.height;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const position = x !== undefined && y !== undefined
|
|
882
|
+
? { x, y }
|
|
883
|
+
: findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
|
|
884
|
+
|
|
885
|
+
const id = `group-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
886
|
+
const data: Record<string, unknown> = {
|
|
887
|
+
title: input.title ?? 'Group',
|
|
888
|
+
children: [],
|
|
889
|
+
frameMode: explicitFrame ? 'manual' : 'fit',
|
|
890
|
+
...(input.color ? { color: input.color } : {}),
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
canvasState.addNode({
|
|
894
|
+
id,
|
|
895
|
+
type: 'group',
|
|
896
|
+
position,
|
|
897
|
+
size: { width, height },
|
|
898
|
+
zIndex: 0,
|
|
899
|
+
collapsed: false,
|
|
900
|
+
pinned: false,
|
|
901
|
+
dockPosition: null,
|
|
902
|
+
data,
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
if (childIds.length > 0) {
|
|
906
|
+
canvasState.groupNodes(id, childIds, {
|
|
907
|
+
preservePositions: input.childLayout === undefined,
|
|
908
|
+
...(input.childLayout ? { layout: input.childLayout } : {}),
|
|
909
|
+
keepGroupFrame: explicitFrame,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const node = canvasState.getNode(id);
|
|
914
|
+
if (!node) {
|
|
915
|
+
throw new Error(`Group "${id}" was not created.`);
|
|
916
|
+
}
|
|
917
|
+
return { id, node };
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
export function groupCanvasNodes(
|
|
921
|
+
groupId: string,
|
|
922
|
+
childIds: string[],
|
|
923
|
+
options: { childLayout?: CanvasArrangeMode } = {},
|
|
924
|
+
): { ok: boolean } {
|
|
925
|
+
return {
|
|
926
|
+
ok: canvasState.groupNodes(groupId, childIds, {
|
|
927
|
+
...(options.childLayout ? { layout: options.childLayout } : {}),
|
|
928
|
+
}),
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function ungroupCanvasNodes(groupId: string): { ok: boolean } {
|
|
933
|
+
return { ok: canvasState.ungroupNodes(groupId) };
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
export function clearCanvas(): { ok: boolean } {
|
|
937
|
+
unwatchAll();
|
|
938
|
+
canvasState.clear();
|
|
939
|
+
return { ok: true };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export function createCanvasJsonRenderNode(
|
|
943
|
+
input: JsonRenderNodeInput,
|
|
944
|
+
): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState } {
|
|
945
|
+
const spec = normalizeAndValidateJsonRenderSpec(input.spec);
|
|
946
|
+
const width = input.width ?? JSON_RENDER_NODE_SIZE.width;
|
|
947
|
+
const height = input.height ?? JSON_RENDER_NODE_SIZE.height;
|
|
948
|
+
const position =
|
|
949
|
+
input.x !== undefined && input.y !== undefined
|
|
950
|
+
? { x: input.x, y: input.y }
|
|
951
|
+
: findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
|
|
952
|
+
const id = `ui-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
953
|
+
const node: CanvasNodeState = {
|
|
954
|
+
id,
|
|
955
|
+
type: 'json-render',
|
|
956
|
+
position,
|
|
957
|
+
size: { width, height },
|
|
958
|
+
zIndex: 1,
|
|
959
|
+
collapsed: false,
|
|
960
|
+
pinned: false,
|
|
961
|
+
dockPosition: null,
|
|
962
|
+
data: createJsonRenderNodeData(id, input.title, spec, {
|
|
963
|
+
viewerType: 'json-render',
|
|
964
|
+
}),
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
canvasState.addJsonRenderNode(node);
|
|
968
|
+
return { id, url: String(node.data.url), spec, node };
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export function createCanvasGraphNode(
|
|
972
|
+
input: GraphNodeInput,
|
|
973
|
+
): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState } {
|
|
974
|
+
const title = input.title?.trim() || 'Graph';
|
|
975
|
+
const spec = buildGraphSpec(input);
|
|
976
|
+
const width = input.width ?? GRAPH_NODE_SIZE.width;
|
|
977
|
+
const height = input.heightPx ?? GRAPH_NODE_SIZE.height;
|
|
978
|
+
const position =
|
|
979
|
+
input.x !== undefined && input.y !== undefined
|
|
980
|
+
? { x: input.x, y: input.y }
|
|
981
|
+
: findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
|
|
982
|
+
const id = `graph-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
983
|
+
const node: CanvasNodeState = {
|
|
984
|
+
id,
|
|
985
|
+
type: 'graph',
|
|
986
|
+
position,
|
|
987
|
+
size: { width, height },
|
|
988
|
+
zIndex: 1,
|
|
989
|
+
collapsed: false,
|
|
990
|
+
pinned: false,
|
|
991
|
+
dockPosition: null,
|
|
992
|
+
data: createJsonRenderNodeData(id, title, spec, {
|
|
993
|
+
viewerType: 'graph',
|
|
994
|
+
graphConfig: {
|
|
995
|
+
title,
|
|
996
|
+
graphType: input.graphType,
|
|
997
|
+
data: input.data,
|
|
998
|
+
...(input.xKey ? { xKey: input.xKey } : {}),
|
|
999
|
+
...(input.yKey ? { yKey: input.yKey } : {}),
|
|
1000
|
+
...(input.zKey ? { zKey: input.zKey } : {}),
|
|
1001
|
+
...(input.nameKey ? { nameKey: input.nameKey } : {}),
|
|
1002
|
+
...(input.valueKey ? { valueKey: input.valueKey } : {}),
|
|
1003
|
+
...(input.axisKey ? { axisKey: input.axisKey } : {}),
|
|
1004
|
+
...(input.metrics?.length ? { metrics: input.metrics } : {}),
|
|
1005
|
+
...(input.series?.length ? { series: input.series } : {}),
|
|
1006
|
+
...(input.barKey ? { barKey: input.barKey } : {}),
|
|
1007
|
+
...(input.lineKey ? { lineKey: input.lineKey } : {}),
|
|
1008
|
+
...(input.aggregate ? { aggregate: input.aggregate } : {}),
|
|
1009
|
+
...(input.color ? { color: input.color } : {}),
|
|
1010
|
+
...(input.barColor ? { barColor: input.barColor } : {}),
|
|
1011
|
+
...(input.lineColor ? { lineColor: input.lineColor } : {}),
|
|
1012
|
+
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
1013
|
+
},
|
|
1014
|
+
}),
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
canvasState.addGraphNode(node);
|
|
1018
|
+
return { id, url: String(node.data.url), spec, node };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
1022
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function resolveBatchRefs(value: unknown, refs: Record<string, unknown>): unknown {
|
|
1026
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
1027
|
+
const path = value.slice(1).split('.');
|
|
1028
|
+
let current: unknown = refs[path[0] ?? ''];
|
|
1029
|
+
for (const segment of path.slice(1)) {
|
|
1030
|
+
if (!isPlainRecord(current) && !Array.isArray(current)) return undefined;
|
|
1031
|
+
current = (current as Record<string, unknown>)[segment];
|
|
1032
|
+
}
|
|
1033
|
+
return current;
|
|
1034
|
+
}
|
|
1035
|
+
if (Array.isArray(value)) return value.map((item) => resolveBatchRefs(item, refs));
|
|
1036
|
+
if (isPlainRecord(value)) {
|
|
1037
|
+
const resolved: Record<string, unknown> = {};
|
|
1038
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1039
|
+
resolved[key] = resolveBatchRefs(child, refs);
|
|
1040
|
+
}
|
|
1041
|
+
return resolved;
|
|
1042
|
+
}
|
|
1043
|
+
return value;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function serializeCreatedNode(node: CanvasNodeState): SerializedCanvasNode {
|
|
1047
|
+
return serializeCanvasNode(node);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export async function executeCanvasBatch(
|
|
1051
|
+
operations: CanvasBatchOperation[],
|
|
1052
|
+
): Promise<{
|
|
1053
|
+
ok: boolean;
|
|
1054
|
+
results: Array<Record<string, unknown>>;
|
|
1055
|
+
refs: Record<string, unknown>;
|
|
1056
|
+
failedIndex?: number;
|
|
1057
|
+
error?: string;
|
|
1058
|
+
}> {
|
|
1059
|
+
const refs: Record<string, unknown> = {};
|
|
1060
|
+
const results: Array<Record<string, unknown>> = [];
|
|
1061
|
+
|
|
1062
|
+
for (let index = 0; index < operations.length; index++) {
|
|
1063
|
+
const operation = operations[index];
|
|
1064
|
+
const args = isPlainRecord(operation.args) ? resolveBatchRefs(operation.args, refs) : {};
|
|
1065
|
+
if (!isPlainRecord(args)) {
|
|
1066
|
+
return {
|
|
1067
|
+
ok: false,
|
|
1068
|
+
failedIndex: index,
|
|
1069
|
+
error: `Operation ${index} has invalid args.`,
|
|
1070
|
+
results,
|
|
1071
|
+
refs,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
try {
|
|
1076
|
+
let result: Record<string, unknown>;
|
|
1077
|
+
switch (operation.op) {
|
|
1078
|
+
case 'node.add': {
|
|
1079
|
+
const type = typeof args.type === 'string' ? args.type : 'markdown';
|
|
1080
|
+
if (type === 'webpage') {
|
|
1081
|
+
const created = addCanvasNode({
|
|
1082
|
+
type: 'webpage',
|
|
1083
|
+
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1084
|
+
...(typeof args.content === 'string' ? { content: args.content } : {}),
|
|
1085
|
+
...(isPlainRecord(args.data) ? { data: args.data } : {}),
|
|
1086
|
+
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1087
|
+
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1088
|
+
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1089
|
+
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1090
|
+
defaultWidth: 520,
|
|
1091
|
+
defaultHeight: 420,
|
|
1092
|
+
});
|
|
1093
|
+
const fetch = await refreshCanvasWebpageNode(created.id);
|
|
1094
|
+
const refreshed = canvasState.getNode(created.id) ?? created.node;
|
|
1095
|
+
result = {
|
|
1096
|
+
ok: true,
|
|
1097
|
+
...serializeCreatedNode(refreshed),
|
|
1098
|
+
fetch: fetch.ok
|
|
1099
|
+
? { ok: true }
|
|
1100
|
+
: { ok: false, error: fetch.error ?? 'Failed to fetch webpage content.' },
|
|
1101
|
+
...(fetch.ok ? {} : { error: fetch.error }),
|
|
1102
|
+
};
|
|
1103
|
+
} else {
|
|
1104
|
+
const created = addCanvasNode({
|
|
1105
|
+
type: type as CanvasNodeState['type'],
|
|
1106
|
+
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1107
|
+
...(typeof args.content === 'string' ? { content: args.content } : {}),
|
|
1108
|
+
...(isPlainRecord(args.data) ? { data: args.data } : {}),
|
|
1109
|
+
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1110
|
+
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1111
|
+
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1112
|
+
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1113
|
+
defaultWidth: 360,
|
|
1114
|
+
defaultHeight: 200,
|
|
1115
|
+
fileMode: 'auto',
|
|
1116
|
+
});
|
|
1117
|
+
result = { ok: true, ...serializeCreatedNode(created.node) };
|
|
1118
|
+
}
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
case 'node.update': {
|
|
1122
|
+
const id = typeof args.id === 'string' ? args.id : '';
|
|
1123
|
+
const node = canvasState.getNode(id);
|
|
1124
|
+
if (!node) throw new Error(`Node "${id}" not found.`);
|
|
1125
|
+
const patch: Partial<CanvasNodeState> = {};
|
|
1126
|
+
if (typeof args.x === 'number' || typeof args.y === 'number') {
|
|
1127
|
+
patch.position = {
|
|
1128
|
+
x: typeof args.x === 'number' ? args.x : node.position.x,
|
|
1129
|
+
y: typeof args.y === 'number' ? args.y : node.position.y,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
if (typeof args.width === 'number' || typeof args.height === 'number') {
|
|
1133
|
+
patch.size = {
|
|
1134
|
+
width: typeof args.width === 'number' ? args.width : node.size.width,
|
|
1135
|
+
height: typeof args.height === 'number' ? args.height : node.size.height,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof args.collapsed === 'boolean') patch.collapsed = args.collapsed;
|
|
1139
|
+
if (typeof args.pinned === 'boolean') patch.pinned = args.pinned;
|
|
1140
|
+
if (args.dockPosition === null || args.dockPosition === 'left' || args.dockPosition === 'right') {
|
|
1141
|
+
patch.dockPosition = args.dockPosition;
|
|
1142
|
+
}
|
|
1143
|
+
if (typeof args.title === 'string' || typeof args.content === 'string' || typeof args.arrangeLocked === 'boolean' || isPlainRecord(args.data)) {
|
|
1144
|
+
patch.data = {
|
|
1145
|
+
...node.data,
|
|
1146
|
+
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1147
|
+
...(typeof args.content === 'string' ? { content: args.content } : {}),
|
|
1148
|
+
...(typeof args.arrangeLocked === 'boolean' ? { arrangeLocked: args.arrangeLocked } : {}),
|
|
1149
|
+
...(isPlainRecord(args.data) ? args.data : {}),
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
canvasState.updateNode(id, patch);
|
|
1153
|
+
const updated = canvasState.getNode(id);
|
|
1154
|
+
result = { ok: true, ...(updated ? serializeCreatedNode(updated) : { id }) };
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
case 'graph.add': {
|
|
1158
|
+
const created = createCanvasGraphNode({
|
|
1159
|
+
graphType: String(args.graphType ?? 'line'),
|
|
1160
|
+
data: Array.isArray(args.data) ? args.data.filter((item): item is Record<string, unknown> => isPlainRecord(item)) : [],
|
|
1161
|
+
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1162
|
+
...(typeof args.xKey === 'string' ? { xKey: args.xKey } : {}),
|
|
1163
|
+
...(typeof args.yKey === 'string' ? { yKey: args.yKey } : {}),
|
|
1164
|
+
...(typeof args.nameKey === 'string' ? { nameKey: args.nameKey } : {}),
|
|
1165
|
+
...(typeof args.valueKey === 'string' ? { valueKey: args.valueKey } : {}),
|
|
1166
|
+
...(args.aggregate === 'sum' || args.aggregate === 'count' || args.aggregate === 'avg'
|
|
1167
|
+
? { aggregate: args.aggregate }
|
|
1168
|
+
: {}),
|
|
1169
|
+
...(typeof args.color === 'string' ? { color: args.color } : {}),
|
|
1170
|
+
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1171
|
+
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1172
|
+
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1173
|
+
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1174
|
+
...(typeof args.nodeHeight === 'number' ? { heightPx: args.nodeHeight } : {}),
|
|
1175
|
+
});
|
|
1176
|
+
result = {
|
|
1177
|
+
ok: true,
|
|
1178
|
+
...serializeCreatedNode(created.node),
|
|
1179
|
+
url: created.url,
|
|
1180
|
+
spec: created.spec,
|
|
1181
|
+
};
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
case 'edge.add': {
|
|
1185
|
+
const added = addCanvasEdge({
|
|
1186
|
+
...(typeof args.from === 'string' ? { from: args.from } : {}),
|
|
1187
|
+
...(typeof args.to === 'string' ? { to: args.to } : {}),
|
|
1188
|
+
...(typeof args.fromSearch === 'string' ? { fromSearch: args.fromSearch } : {}),
|
|
1189
|
+
...(typeof args.toSearch === 'string' ? { toSearch: args.toSearch } : {}),
|
|
1190
|
+
type: String(args.type) as CanvasEdge['type'],
|
|
1191
|
+
...(typeof args.label === 'string' ? { label: args.label } : {}),
|
|
1192
|
+
...(typeof args.style === 'string' ? { style: args.style as CanvasEdge['style'] } : {}),
|
|
1193
|
+
...(typeof args.animated === 'boolean' ? { animated: args.animated } : {}),
|
|
1194
|
+
});
|
|
1195
|
+
result = { ok: true, ...added };
|
|
1196
|
+
break;
|
|
1197
|
+
}
|
|
1198
|
+
case 'group.create': {
|
|
1199
|
+
const created = createCanvasGroup({
|
|
1200
|
+
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1201
|
+
...(Array.isArray(args.childIds) ? { childIds: args.childIds.filter((id): id is string => typeof id === 'string') } : {}),
|
|
1202
|
+
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1203
|
+
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1204
|
+
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1205
|
+
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1206
|
+
...(typeof args.color === 'string' ? { color: args.color } : {}),
|
|
1207
|
+
...(args.childLayout === 'grid' || args.childLayout === 'column' || args.childLayout === 'flow'
|
|
1208
|
+
? { childLayout: args.childLayout }
|
|
1209
|
+
: {}),
|
|
1210
|
+
});
|
|
1211
|
+
result = { ok: true, ...serializeCreatedNode(created.node) };
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
case 'group.add': {
|
|
1215
|
+
const groupId = typeof args.groupId === 'string' ? args.groupId : '';
|
|
1216
|
+
const childIds = Array.isArray(args.childIds) ? args.childIds.filter((id): id is string => typeof id === 'string') : [];
|
|
1217
|
+
const ok = canvasState.groupNodes(groupId, childIds, {
|
|
1218
|
+
preservePositions: args.childLayout === undefined,
|
|
1219
|
+
...(args.childLayout === 'grid' || args.childLayout === 'column' || args.childLayout === 'flow'
|
|
1220
|
+
? { layout: args.childLayout }
|
|
1221
|
+
: {}),
|
|
1222
|
+
});
|
|
1223
|
+
if (!ok) throw new Error('Group not found or no valid children.');
|
|
1224
|
+
const group = canvasState.getNode(groupId);
|
|
1225
|
+
result = { ok: true, ...(group ? serializeCreatedNode(group) : { id: groupId }) };
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
case 'group.remove': {
|
|
1229
|
+
const groupId = typeof args.groupId === 'string' ? args.groupId : '';
|
|
1230
|
+
const ok = canvasState.ungroupNodes(groupId);
|
|
1231
|
+
if (!ok) throw new Error('Group not found or empty.');
|
|
1232
|
+
result = { ok: true, groupId };
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
case 'pin.set':
|
|
1236
|
+
case 'pin.add':
|
|
1237
|
+
case 'pin.remove': {
|
|
1238
|
+
const ids = Array.isArray(args.nodeIds) ? args.nodeIds.filter((id): id is string => typeof id === 'string') : [];
|
|
1239
|
+
result = {
|
|
1240
|
+
ok: true,
|
|
1241
|
+
...setCanvasContextPins(ids, operation.op === 'pin.set' ? 'set' : operation.op === 'pin.add' ? 'add' : 'remove'),
|
|
1242
|
+
};
|
|
1243
|
+
break;
|
|
1244
|
+
}
|
|
1245
|
+
case 'snapshot.save': {
|
|
1246
|
+
const snapshot = saveCanvasSnapshot(typeof args.name === 'string' ? args.name : '');
|
|
1247
|
+
if (!snapshot) throw new Error('Failed to save snapshot.');
|
|
1248
|
+
result = { ok: true, snapshot };
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
case 'arrange': {
|
|
1252
|
+
const layout =
|
|
1253
|
+
args.layout === 'column' || args.layout === 'flow' || args.layout === 'grid'
|
|
1254
|
+
? args.layout
|
|
1255
|
+
: 'grid';
|
|
1256
|
+
result = { ok: true, ...arrangeCanvasNodes(layout) };
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
default:
|
|
1260
|
+
throw new Error(`Unsupported batch operation "${operation.op}".`);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
results.push(result);
|
|
1264
|
+
if (typeof operation.assign === 'string' && operation.assign.trim().length > 0) {
|
|
1265
|
+
refs[operation.assign] = result;
|
|
1266
|
+
}
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
return {
|
|
1269
|
+
ok: false,
|
|
1270
|
+
failedIndex: index,
|
|
1271
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1272
|
+
results,
|
|
1273
|
+
refs,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return { ok: true, results, refs };
|
|
1279
|
+
}
|