pmx-canvas 0.1.12 → 0.1.14
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 +151 -0
- package/dist/canvas/index.js +42 -42
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -3
- package/dist/types/client/nodes/McpAppNode.d.ts +2 -1
- package/dist/types/client/nodes/trace-model.d.ts +9 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +1 -0
- package/dist/types/server/canvas-operations.d.ts +8 -0
- package/dist/types/server/diagram-presets.d.ts +4 -0
- package/dist/types/server/index.d.ts +8 -0
- package/dist/types/server/mcp-app-runtime.d.ts +1 -0
- package/dist/types/server/web-artifacts.d.ts +1 -0
- package/package.json +1 -1
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +9 -8
- package/src/cli/agent.ts +15 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
- package/src/client/nodes/ExtAppFrame.tsx +10 -35
- package/src/client/nodes/McpAppNode.tsx +2 -2
- package/src/client/nodes/TraceNode.tsx +2 -6
- package/src/client/nodes/trace-model.ts +19 -0
- package/src/client/state/canvas-store.ts +5 -2
- package/src/client/state/sse-bridge.ts +3 -1
- package/src/mcp/canvas-access.ts +28 -3
- package/src/mcp/server.ts +51 -9
- package/src/server/canvas-operations.ts +36 -0
- package/src/server/canvas-schema.ts +11 -0
- package/src/server/diagram-presets.ts +44 -46
- package/src/server/index.ts +31 -4
- package/src/server/mcp-app-runtime.ts +15 -5
- package/src/server/server.ts +96 -50
- package/src/server/web-artifacts/scripts/init-artifact.sh +9 -8
- package/src/server/web-artifacts.ts +14 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { AppBridge } from '@modelcontextprotocol/ext-apps/app-bridge';
|
|
3
3
|
import type { CanvasNodeState } from '../types';
|
|
4
|
-
type IframeLoadTarget = Pick<HTMLIFrameElement, 'addEventListener' | 'removeEventListener' | 'contentDocument'>;
|
|
5
4
|
type ExtAppBridgeNotifications = Pick<AppBridge, 'sendToolInput' | 'sendToolResult'>;
|
|
6
5
|
type DisplayMode = 'inline' | 'fullscreen' | 'pip';
|
|
7
6
|
interface ExtAppHostDimensionsTarget {
|
|
@@ -9,7 +8,6 @@ interface ExtAppHostDimensionsTarget {
|
|
|
9
8
|
clientHeight?: number;
|
|
10
9
|
getBoundingClientRect(): Pick<DOMRectReadOnly, 'width' | 'height'>;
|
|
11
10
|
}
|
|
12
|
-
export declare function waitForExtAppFrameLoad(target: IframeLoadTarget): Promise<void>;
|
|
13
11
|
export declare function getExtAppBridgeInitKey(node: CanvasNodeState, retryKey: number): string;
|
|
14
12
|
export declare function resolveExtAppDisplayModeRequest(requestedMode: DisplayMode, isExpanded: boolean): {
|
|
15
13
|
nextMode: DisplayMode;
|
|
@@ -26,7 +24,8 @@ export declare function resolveExtAppContainerDimensions(target: ExtAppHostDimen
|
|
|
26
24
|
height: number;
|
|
27
25
|
};
|
|
28
26
|
export declare function shouldApplyExtAppSizeChange(height: unknown, isExpanded: boolean): height is number;
|
|
29
|
-
export declare function ExtAppFrame({ node }: {
|
|
27
|
+
export declare function ExtAppFrame({ node, expanded }: {
|
|
30
28
|
node: CanvasNodeState;
|
|
29
|
+
expanded?: boolean;
|
|
31
30
|
}): import("preact/jsx-runtime").JSX.Element;
|
|
32
31
|
export {};
|
|
@@ -54,6 +54,8 @@ export declare function replaceViewport(next: ViewportState): void;
|
|
|
54
54
|
export declare function commitViewport(next: ViewportState): void;
|
|
55
55
|
export declare function applyServerCanvasLayout(layout: Pick<CanvasLayout, 'nodes' | 'edges'> & {
|
|
56
56
|
viewport?: ViewportState;
|
|
57
|
+
}, options?: {
|
|
58
|
+
applyViewport?: boolean;
|
|
57
59
|
}): void;
|
|
58
60
|
/**
|
|
59
61
|
* Smoothly animate the viewport to a target state.
|
|
@@ -83,5 +83,6 @@ export interface CanvasAccess {
|
|
|
83
83
|
resizeAutomationWebView(width: number, height: number): Promise<AutomationWebViewStatus>;
|
|
84
84
|
screenshotAutomationWebView(options?: AutomationScreenshotOptions): Promise<Uint8Array>;
|
|
85
85
|
}
|
|
86
|
+
export declare function refreshCanvasAccess(access: CanvasAccess): Promise<CanvasAccess>;
|
|
86
87
|
export declare function createCanvasAccess(): Promise<CanvasAccess>;
|
|
87
88
|
export {};
|
|
@@ -40,6 +40,12 @@ interface CanvasAddNodeInput {
|
|
|
40
40
|
title?: string;
|
|
41
41
|
content?: string;
|
|
42
42
|
data?: Record<string, unknown>;
|
|
43
|
+
toolName?: string;
|
|
44
|
+
category?: string;
|
|
45
|
+
status?: string;
|
|
46
|
+
duration?: string;
|
|
47
|
+
resultSummary?: string;
|
|
48
|
+
error?: string;
|
|
43
49
|
x?: number;
|
|
44
50
|
y?: number;
|
|
45
51
|
width?: number;
|
|
@@ -106,6 +112,8 @@ export declare function validateCanvasNodePatch(patch: {
|
|
|
106
112
|
height: number;
|
|
107
113
|
};
|
|
108
114
|
}): string | null;
|
|
115
|
+
export declare function mergeTraceNodeDataFields(base: Record<string, unknown>, input: Record<string, unknown>): Record<string, unknown>;
|
|
116
|
+
export declare function hasTraceNodeDataFields(input: Record<string, unknown>): boolean;
|
|
109
117
|
export declare function scheduleCodeGraphRecompute(onComplete?: () => void): void;
|
|
110
118
|
export declare function addCanvasNode(input: CanvasAddNodeInput): {
|
|
111
119
|
id: string;
|
|
@@ -9,11 +9,13 @@ export declare const DEFAULT_EXCALIDRAW_ELEMENTS: ReadonlyArray<Record<string, u
|
|
|
9
9
|
export declare const EXCALIDRAW_MCP_TRANSPORT: ExternalMcpTransportConfig;
|
|
10
10
|
export interface DiagramPresetOpenInput {
|
|
11
11
|
elements: unknown;
|
|
12
|
+
nodeId?: string;
|
|
12
13
|
title?: string;
|
|
13
14
|
x?: number;
|
|
14
15
|
y?: number;
|
|
15
16
|
width?: number;
|
|
16
17
|
height?: number;
|
|
18
|
+
timeoutMs?: number;
|
|
17
19
|
}
|
|
18
20
|
export interface ExcalidrawOpenMcpAppInput {
|
|
19
21
|
transport: ExternalMcpTransportConfig;
|
|
@@ -22,11 +24,13 @@ export interface ExcalidrawOpenMcpAppInput {
|
|
|
22
24
|
toolArguments: {
|
|
23
25
|
elements: string;
|
|
24
26
|
};
|
|
27
|
+
nodeId?: string;
|
|
25
28
|
title?: string;
|
|
26
29
|
x?: number;
|
|
27
30
|
y?: number;
|
|
28
31
|
width?: number;
|
|
29
32
|
height?: number;
|
|
33
|
+
timeoutMs?: number;
|
|
30
34
|
}
|
|
31
35
|
export declare function inferExcalidrawCameraUpdate(elements: Array<Record<string, unknown>>): Record<string, unknown> | null;
|
|
32
36
|
export declare function normalizeExcalidrawElements(elements: unknown): string;
|
|
@@ -23,6 +23,12 @@ export declare class PmxCanvas extends EventEmitter {
|
|
|
23
23
|
type: CanvasNodeState['type'];
|
|
24
24
|
title?: string;
|
|
25
25
|
content?: string;
|
|
26
|
+
toolName?: string;
|
|
27
|
+
category?: string;
|
|
28
|
+
status?: string;
|
|
29
|
+
duration?: string;
|
|
30
|
+
resultSummary?: string;
|
|
31
|
+
error?: string;
|
|
26
32
|
x?: number;
|
|
27
33
|
y?: number;
|
|
28
34
|
width?: number;
|
|
@@ -198,12 +204,14 @@ export declare class PmxCanvas extends EventEmitter {
|
|
|
198
204
|
transport: ExternalMcpTransportConfig;
|
|
199
205
|
toolName: string;
|
|
200
206
|
toolArguments?: Record<string, unknown>;
|
|
207
|
+
nodeId?: string;
|
|
201
208
|
serverName?: string;
|
|
202
209
|
title?: string;
|
|
203
210
|
x?: number;
|
|
204
211
|
y?: number;
|
|
205
212
|
width?: number;
|
|
206
213
|
height?: number;
|
|
214
|
+
timeoutMs?: number;
|
|
207
215
|
}): Promise<{
|
|
208
216
|
ok: true;
|
|
209
217
|
id?: string;
|
|
@@ -38,6 +38,7 @@ export interface WebArtifactCanvasBuildResult extends WebArtifactBuildOutput {
|
|
|
38
38
|
openedInCanvas: boolean;
|
|
39
39
|
nodeId?: string;
|
|
40
40
|
url?: string;
|
|
41
|
+
completedAt: string;
|
|
41
42
|
}
|
|
42
43
|
export declare function resolveWorkspacePath(pathLike: string, cwd?: string): string;
|
|
43
44
|
export declare function resolveWebArtifactScriptPath(kind: 'init' | 'bundle'): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -115,12 +115,13 @@ else
|
|
|
115
115
|
echo "✅ Using Vite $VITE_VERSION (Node 18 compatible)"
|
|
116
116
|
fi
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
120
|
-
|
|
121
|
-
else
|
|
122
|
-
|
|
123
|
-
fi
|
|
118
|
+
function sed_in_place() {
|
|
119
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
120
|
+
sed -i '' "$@"
|
|
121
|
+
else
|
|
122
|
+
sed -i "$@"
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
124
125
|
|
|
125
126
|
declare -a PNPM_CMD
|
|
126
127
|
configure_pnpm
|
|
@@ -158,8 +159,8 @@ fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
|
158
159
|
"
|
|
159
160
|
|
|
160
161
|
echo "🧹 Cleaning up Vite template..."
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
sed_in_place '/<link rel="icon".*/d' index.html
|
|
163
|
+
sed_in_place 's/<title>.*<\/title>/<title>'"$PROJECT_NAME"'<\/title>/' index.html
|
|
163
164
|
|
|
164
165
|
echo "📦 Installing base dependencies..."
|
|
165
166
|
run_pnpm_quiet install
|
package/src/cli/agent.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
|
|
27
27
|
const DEFAULT_PORT = 4313;
|
|
28
28
|
const defaultConsoleLog = console.log;
|
|
29
|
+
const TRACE_NODE_FIELDS = ['toolName', 'category', 'status', 'duration', 'resultSummary', 'error'] as const;
|
|
29
30
|
|
|
30
31
|
interface CanvasSchemaField {
|
|
31
32
|
name: string;
|
|
@@ -1078,6 +1079,12 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1078
1079
|
height: 'Use a positive number, e.g. --height 280',
|
|
1079
1080
|
});
|
|
1080
1081
|
applyStrictSizeFlags(body, flags);
|
|
1082
|
+
if (type === 'trace') {
|
|
1083
|
+
for (const field of TRACE_NODE_FIELDS) {
|
|
1084
|
+
const value = getStringFlag(flags, field);
|
|
1085
|
+
if (value !== undefined) body[field] = value;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1081
1088
|
|
|
1082
1089
|
// Support --stdin for piping content
|
|
1083
1090
|
if (flags.stdin) {
|
|
@@ -1565,7 +1572,9 @@ cmd('external-app add', 'Create a hosted external app node', [
|
|
|
1565
1572
|
title: typeof flags.title === 'string' ? flags.title : 'Excalidraw Diagram',
|
|
1566
1573
|
elements: DEFAULT_EXCALIDRAW_ELEMENTS,
|
|
1567
1574
|
};
|
|
1568
|
-
const
|
|
1575
|
+
const nodeId = getStringFlag(flags, 'node-id', 'nodeId', 'id');
|
|
1576
|
+
if (nodeId) body.nodeId = nodeId;
|
|
1577
|
+
const elementsJson = getStringFlag(flags, 'elements-json', 'elements');
|
|
1569
1578
|
if (elementsJson !== undefined) body.elements = parseJsonValue(elementsJson, 'Excalidraw elements', 'Use --elements-json \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'');
|
|
1570
1579
|
const elementsFile = getStringFlag(flags, 'elements-file', 'initial-file');
|
|
1571
1580
|
if (elementsFile) body.elements = parseJsonValue(readFileSync(elementsFile, 'utf-8'), 'Excalidraw elements file', 'Use --elements-file ./scene.excalidraw');
|
|
@@ -1575,6 +1584,8 @@ cmd('external-app add', 'Create a hosted external app node', [
|
|
|
1575
1584
|
width: 'Use a positive number, e.g. --width 960',
|
|
1576
1585
|
height: 'Use a positive number, e.g. --height 720',
|
|
1577
1586
|
});
|
|
1587
|
+
const timeoutMs = optionalPositiveFiniteFlag(flags, 'timeout-ms', 'Use a positive number, e.g. --timeout-ms 120000');
|
|
1588
|
+
if (timeoutMs !== undefined) body.timeoutMs = timeoutMs;
|
|
1578
1589
|
|
|
1579
1590
|
const result = await api('POST', '/api/canvas/diagram', body);
|
|
1580
1591
|
output(result && typeof result === 'object' && !Array.isArray(result) && 'nodeId' in result && !('id' in result)
|
|
@@ -2303,9 +2314,12 @@ function showCommandHelp(name: string): void {
|
|
|
2303
2314
|
console.log('\nOptions:');
|
|
2304
2315
|
console.log(' --kind excalidraw External app kind to create');
|
|
2305
2316
|
console.log(' --title <title> Node title');
|
|
2317
|
+
console.log(' --node-id <id> Existing Excalidraw app node to update in place');
|
|
2318
|
+
console.log(' --elements <json> Optional Excalidraw elements array JSON');
|
|
2306
2319
|
console.log(' --elements-json <json> Optional Excalidraw elements array JSON');
|
|
2307
2320
|
console.log(' --elements-file <path> Optional file containing Excalidraw elements JSON');
|
|
2308
2321
|
console.log(' --initial-file <path> Alias for --elements-file');
|
|
2322
|
+
console.log(' --timeout-ms <number> Optional downstream MCP timeout for cold starts');
|
|
2309
2323
|
}
|
|
2310
2324
|
console.log('');
|
|
2311
2325
|
}
|
|
@@ -26,13 +26,13 @@ function renderContent(node: CanvasNodeState, expanded: boolean) {
|
|
|
26
26
|
case 'markdown':
|
|
27
27
|
return <MarkdownNode node={node} expanded={expanded} />;
|
|
28
28
|
case 'mcp-app':
|
|
29
|
-
return <McpAppNode node={node} />;
|
|
29
|
+
return <McpAppNode node={node} expanded={expanded} />;
|
|
30
30
|
case 'webpage':
|
|
31
31
|
return <WebpageNode node={node} expanded={expanded} />;
|
|
32
32
|
case 'json-render':
|
|
33
|
-
return <McpAppNode node={node} />;
|
|
33
|
+
return <McpAppNode node={node} expanded={expanded} />;
|
|
34
34
|
case 'graph':
|
|
35
|
-
return <McpAppNode node={node} />;
|
|
35
|
+
return <McpAppNode node={node} expanded={expanded} />;
|
|
36
36
|
case 'prompt':
|
|
37
37
|
return <PromptNode node={node} />;
|
|
38
38
|
case 'response':
|
|
@@ -13,11 +13,6 @@ import type { CanvasNodeState } from '../types';
|
|
|
13
13
|
|
|
14
14
|
type McpUiTheme = 'light' | 'dark';
|
|
15
15
|
|
|
16
|
-
type IframeLoadTarget = Pick<
|
|
17
|
-
HTMLIFrameElement,
|
|
18
|
-
'addEventListener' | 'removeEventListener' | 'contentDocument'
|
|
19
|
-
>;
|
|
20
|
-
|
|
21
16
|
type ExtAppBridgeNotifications = Pick<AppBridge, 'sendToolInput' | 'sendToolResult'>;
|
|
22
17
|
type DisplayMode = 'inline' | 'fullscreen' | 'pip';
|
|
23
18
|
const DEFAULT_EXT_APP_SANDBOX = 'allow-scripts allow-popups allow-popups-to-escape-sandbox';
|
|
@@ -43,21 +38,6 @@ async function postJson<T>(url: string, body: Record<string, unknown>): Promise<
|
|
|
43
38
|
return json.result as T;
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
export function waitForExtAppFrameLoad(target: IframeLoadTarget): Promise<void> {
|
|
47
|
-
const readyState = target.contentDocument?.readyState;
|
|
48
|
-
if (readyState === 'interactive' || readyState === 'complete') {
|
|
49
|
-
return Promise.resolve();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return new Promise<void>((resolve) => {
|
|
53
|
-
const onLoad = () => {
|
|
54
|
-
target.removeEventListener('load', onLoad);
|
|
55
|
-
resolve();
|
|
56
|
-
};
|
|
57
|
-
target.addEventListener('load', onLoad, { once: true });
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
41
|
export function getExtAppBridgeInitKey(node: CanvasNodeState, retryKey: number): string {
|
|
62
42
|
const html = typeof node.data.html === 'string' ? node.data.html : '';
|
|
63
43
|
const serverName = typeof node.data.serverName === 'string' ? node.data.serverName : '';
|
|
@@ -131,7 +111,7 @@ export function shouldApplyExtAppSizeChange(height: unknown, isExpanded: boolean
|
|
|
131
111
|
return typeof height === 'number' && Number.isFinite(height) && height > 0 && !isExpanded;
|
|
132
112
|
}
|
|
133
113
|
|
|
134
|
-
export function ExtAppFrame({ node }: { node: CanvasNodeState }) {
|
|
114
|
+
export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
135
115
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
136
116
|
const bridgeRef = useRef<AppBridge | null>(null);
|
|
137
117
|
const transportRef = useRef<PostMessageTransport | null>(null);
|
|
@@ -164,10 +144,9 @@ export function ExtAppFrame({ node }: { node: CanvasNodeState }) {
|
|
|
164
144
|
const sessionError = node.data.sessionError as string | undefined;
|
|
165
145
|
const maxHeight = node.size.height;
|
|
166
146
|
const nodeId = node.id;
|
|
167
|
-
const frameKey =
|
|
168
|
-
const bridgeInitKey = getExtAppBridgeInitKey(node, retryKey);
|
|
147
|
+
const frameKey = getExtAppBridgeInitKey(node, retryKey);
|
|
169
148
|
const toMcpTheme = (theme: string): McpUiTheme => (theme === 'light' ? 'light' : 'dark');
|
|
170
|
-
const isExpanded = expandedNodeId.value === nodeId;
|
|
149
|
+
const isExpanded = expanded || expandedNodeId.value === nodeId;
|
|
171
150
|
|
|
172
151
|
latestToolInputRef.current = toolInput;
|
|
173
152
|
latestToolResultRef.current = toolResult;
|
|
@@ -221,9 +200,9 @@ export function ExtAppFrame({ node }: { node: CanvasNodeState }) {
|
|
|
221
200
|
return sendPromise;
|
|
222
201
|
};
|
|
223
202
|
|
|
224
|
-
// Initialize
|
|
203
|
+
// Initialize as soon as HTML is mounted; some apps send initialize before iframe load fires.
|
|
225
204
|
useEffect(() => {
|
|
226
|
-
if (!html) return;
|
|
205
|
+
if (!html) return;
|
|
227
206
|
const iframe = iframeRef.current;
|
|
228
207
|
if (!iframe) return;
|
|
229
208
|
let disposed = false;
|
|
@@ -242,12 +221,8 @@ export function ExtAppFrame({ node }: { node: CanvasNodeState }) {
|
|
|
242
221
|
};
|
|
243
222
|
|
|
244
223
|
const init = async () => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
await waitForExtAppFrameLoad(iframe);
|
|
248
|
-
if (disposed) return;
|
|
249
|
-
contentWindow = iframe.contentWindow;
|
|
250
|
-
}
|
|
224
|
+
if (!html) return;
|
|
225
|
+
const contentWindow = iframe.contentWindow;
|
|
251
226
|
if (!contentWindow) {
|
|
252
227
|
throw new Error('Ext-app iframe window is unavailable');
|
|
253
228
|
}
|
|
@@ -397,8 +372,8 @@ export function ExtAppFrame({ node }: { node: CanvasNodeState }) {
|
|
|
397
372
|
bridgeReadyRef.current = true;
|
|
398
373
|
setStatus('ready');
|
|
399
374
|
setError(null);
|
|
400
|
-
|
|
401
|
-
|
|
375
|
+
void Promise.resolve(bridge.sendHostContextChange(buildHostContext(isExpanded ? 'fullscreen' : 'inline')))
|
|
376
|
+
.then(() => sendExtAppBootstrapState(bridge, latestToolInputRef.current, undefined))
|
|
402
377
|
.then(() => flushToolResult(bridge))
|
|
403
378
|
.catch((err) => {
|
|
404
379
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -481,7 +456,7 @@ export function ExtAppFrame({ node }: { node: CanvasNodeState }) {
|
|
|
481
456
|
transportRef.current = null;
|
|
482
457
|
}
|
|
483
458
|
};
|
|
484
|
-
}, [
|
|
459
|
+
}, [frameKey]);
|
|
485
460
|
|
|
486
461
|
// Forward tool result when it arrives after bridge is ready
|
|
487
462
|
useEffect(() => {
|
|
@@ -13,9 +13,9 @@ function withTheme(url: string): string {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function McpAppNode({ node }: { node: CanvasNodeState }) {
|
|
16
|
+
export function McpAppNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
17
17
|
if (node.data.mode === 'ext-app') {
|
|
18
|
-
return <ExtAppFrame node={node} />;
|
|
18
|
+
return <ExtAppFrame node={node} expanded={expanded} />;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const url = withTheme((node.data.url as string) || '');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CanvasNodeState } from '../types';
|
|
2
|
+
import { buildTraceDisplayModel } from './trace-model';
|
|
2
3
|
|
|
3
4
|
const CATEGORY_COLORS: Record<string, string> = {
|
|
4
5
|
mcp: 'var(--c-accent)',
|
|
@@ -20,12 +21,7 @@ const STATUS_COLORS: Record<string, string> = {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export function TraceNode({ node }: { node: CanvasNodeState }) {
|
|
23
|
-
const toolName = (node.data
|
|
24
|
-
const category = (node.data.category as string) || 'other';
|
|
25
|
-
const status = (node.data.status as string) || 'running';
|
|
26
|
-
const duration = (node.data.duration as string) || '';
|
|
27
|
-
const resultSummary = (node.data.resultSummary as string) || '';
|
|
28
|
-
const error = (node.data.error as string) || '';
|
|
24
|
+
const { toolName, category, status, duration, resultSummary, error } = buildTraceDisplayModel(node.data);
|
|
29
25
|
|
|
30
26
|
const catColor = CATEGORY_COLORS[category] ?? CATEGORY_COLORS.other;
|
|
31
27
|
const statusIcon = STATUS_ICONS[status] ?? '◌';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface TraceDisplayModel {
|
|
2
|
+
toolName: string;
|
|
3
|
+
category: string;
|
|
4
|
+
status: string;
|
|
5
|
+
duration: string;
|
|
6
|
+
resultSummary: string;
|
|
7
|
+
error: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildTraceDisplayModel(data: Record<string, unknown>): TraceDisplayModel {
|
|
11
|
+
return {
|
|
12
|
+
toolName: (data.toolName as string) || (data.title as string) || 'unknown',
|
|
13
|
+
category: (data.category as string) || 'other',
|
|
14
|
+
status: (data.status as string) || 'running',
|
|
15
|
+
duration: (data.duration as string) || '',
|
|
16
|
+
resultSummary: (data.resultSummary as string) || (data.content as string) || '',
|
|
17
|
+
error: (data.error as string) || '',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -313,7 +313,10 @@ export function commitViewport(next: ViewportState): void {
|
|
|
313
313
|
void updateViewportFromClient(next);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
export function applyServerCanvasLayout(
|
|
316
|
+
export function applyServerCanvasLayout(
|
|
317
|
+
layout: Pick<CanvasLayout, 'nodes' | 'edges'> & { viewport?: ViewportState },
|
|
318
|
+
options: { applyViewport?: boolean } = {},
|
|
319
|
+
): void {
|
|
317
320
|
const nextNodes = new Map<string, CanvasNodeState>();
|
|
318
321
|
let nextMaxZ = 1;
|
|
319
322
|
for (const node of layout.nodes) {
|
|
@@ -337,7 +340,7 @@ export function applyServerCanvasLayout(layout: Pick<CanvasLayout, 'nodes' | 'ed
|
|
|
337
340
|
const nextContextPinnedNodeIds = filterNodeIdSet(contextPinnedNodeIds.value, nextNodes);
|
|
338
341
|
|
|
339
342
|
batch(() => {
|
|
340
|
-
if (layout.viewport) {
|
|
343
|
+
if (options.applyViewport === true && layout.viewport) {
|
|
341
344
|
viewport.value = layout.viewport;
|
|
342
345
|
}
|
|
343
346
|
maxZ = nextMaxZ;
|
|
@@ -529,6 +529,7 @@ function handleExtAppOpen(data: Record<string, unknown>): void {
|
|
|
529
529
|
if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
|
|
530
530
|
ensureExtAppNode({
|
|
531
531
|
toolCallId: data.toolCallId,
|
|
532
|
+
...(typeof data.nodeId === 'string' && data.nodeId.length > 0 ? { nodeId: data.nodeId } : {}),
|
|
532
533
|
title: data.title,
|
|
533
534
|
html: data.html,
|
|
534
535
|
toolInput: data.toolInput,
|
|
@@ -803,6 +804,7 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
|
|
|
803
804
|
}
|
|
804
805
|
| undefined;
|
|
805
806
|
if (!layout?.nodes) return;
|
|
807
|
+
const shouldApplyViewport = !hasInitialServerLayout.value;
|
|
806
808
|
hasInitialServerLayout.value = true;
|
|
807
809
|
|
|
808
810
|
const serverNodes = layout.nodes
|
|
@@ -824,7 +826,7 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
|
|
|
824
826
|
...(nextViewport ? { viewport: nextViewport } : {}),
|
|
825
827
|
nodes: serverNodes,
|
|
826
828
|
edges: serverEdges,
|
|
827
|
-
});
|
|
829
|
+
}, { applyViewport: shouldApplyViewport });
|
|
828
830
|
|
|
829
831
|
syncAttentionFromSse({ event: 'canvas-layout-update', data });
|
|
830
832
|
}
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -135,7 +135,11 @@ export interface CanvasAccess {
|
|
|
135
135
|
class LocalCanvasAccess implements CanvasAccess {
|
|
136
136
|
readonly remoteBaseUrl = null;
|
|
137
137
|
|
|
138
|
-
constructor(
|
|
138
|
+
constructor(
|
|
139
|
+
private readonly canvas: PmxCanvas,
|
|
140
|
+
readonly workspaceRoot: string,
|
|
141
|
+
readonly targetPort: number,
|
|
142
|
+
) {}
|
|
139
143
|
|
|
140
144
|
get port(): number {
|
|
141
145
|
return this.canvas.port;
|
|
@@ -335,6 +339,9 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
335
339
|
const error = parsed && typeof parsed === 'object' && 'error' in parsed
|
|
336
340
|
? String((parsed as { error?: unknown }).error)
|
|
337
341
|
: `HTTP ${response.status}`;
|
|
342
|
+
if (path === '/api/canvas/batch' && parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
343
|
+
return parsed as T;
|
|
344
|
+
}
|
|
338
345
|
throw new Error(error);
|
|
339
346
|
}
|
|
340
347
|
return parsed as T;
|
|
@@ -617,6 +624,10 @@ function candidateBaseUrls(port: number): string[] {
|
|
|
617
624
|
return urls;
|
|
618
625
|
}
|
|
619
626
|
|
|
627
|
+
function localBaseUrls(port: number): string[] {
|
|
628
|
+
return [`http://127.0.0.1:${port}`, `http://localhost:${port}`];
|
|
629
|
+
}
|
|
630
|
+
|
|
620
631
|
async function readHealth(baseUrl: string): Promise<HealthResponse | null> {
|
|
621
632
|
try {
|
|
622
633
|
const response = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(400) });
|
|
@@ -627,9 +638,15 @@ async function readHealth(baseUrl: string): Promise<HealthResponse | null> {
|
|
|
627
638
|
}
|
|
628
639
|
}
|
|
629
640
|
|
|
630
|
-
async function findExistingCanvasServer(
|
|
641
|
+
async function findExistingCanvasServer(
|
|
642
|
+
workspaceRoot: string,
|
|
643
|
+
port: number,
|
|
644
|
+
options: { excludeBaseUrls?: string[] } = {},
|
|
645
|
+
): Promise<string | null> {
|
|
631
646
|
const canonicalWorkspaceRoot = canonicalWorkspacePath(workspaceRoot);
|
|
647
|
+
const excluded = new Set((options.excludeBaseUrls ?? []).map((baseUrl) => baseUrl.replace(/\/$/, '')));
|
|
632
648
|
for (const baseUrl of candidateBaseUrls(port)) {
|
|
649
|
+
if (excluded.has(baseUrl)) continue;
|
|
633
650
|
const health = await readHealth(baseUrl);
|
|
634
651
|
if (health?.ok !== true) continue;
|
|
635
652
|
const healthWorkspace = typeof health.workspace === 'string' ? canonicalWorkspacePath(health.workspace) : '';
|
|
@@ -639,6 +656,14 @@ async function findExistingCanvasServer(workspaceRoot: string, port: number): Pr
|
|
|
639
656
|
return null;
|
|
640
657
|
}
|
|
641
658
|
|
|
659
|
+
export async function refreshCanvasAccess(access: CanvasAccess): Promise<CanvasAccess> {
|
|
660
|
+
if (!(access instanceof LocalCanvasAccess)) return access;
|
|
661
|
+
const remoteBaseUrl = await findExistingCanvasServer(access.workspaceRoot, access.targetPort, {
|
|
662
|
+
excludeBaseUrls: localBaseUrls(access.port),
|
|
663
|
+
});
|
|
664
|
+
return remoteBaseUrl ? new RemoteCanvasAccess(remoteBaseUrl) : access;
|
|
665
|
+
}
|
|
666
|
+
|
|
642
667
|
export async function createCanvasAccess(): Promise<CanvasAccess> {
|
|
643
668
|
const workspaceRoot = resolve(process.cwd());
|
|
644
669
|
const port = targetPort();
|
|
@@ -647,5 +672,5 @@ export async function createCanvasAccess(): Promise<CanvasAccess> {
|
|
|
647
672
|
|
|
648
673
|
const canvas = createCanvas({ port });
|
|
649
674
|
await canvas.start({ open: true });
|
|
650
|
-
return new LocalCanvasAccess(canvas);
|
|
675
|
+
return new LocalCanvasAccess(canvas, workspaceRoot, port);
|
|
651
676
|
}
|