pmx-canvas 0.1.13 → 0.1.15
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 +163 -0
- package/Readme.md +108 -1058
- package/dist/canvas/global.css +141 -0
- package/dist/canvas/index.js +137 -87
- package/dist/json-render/index.css +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -3
- package/dist/types/client/nodes/HtmlNode.d.ts +5 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +2 -1
- package/dist/types/client/state/canvas-store.d.ts +5 -1
- package/dist/types/client/state/intent-bridge.d.ts +3 -1
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +7 -1
- package/dist/types/server/agent-context.d.ts +1 -0
- package/dist/types/server/canvas-operations.d.ts +12 -2
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-serialization.d.ts +3 -0
- package/dist/types/server/canvas-state.d.ts +51 -4
- package/dist/types/server/demo.d.ts +5 -0
- package/dist/types/server/diagram-presets.d.ts +4 -0
- package/dist/types/server/index.d.ts +21 -3
- package/dist/types/server/mcp-app-runtime.d.ts +1 -0
- package/dist/types/server/web-artifacts.d.ts +18 -0
- package/dist/types/shared/canvas-node-kind.d.ts +5 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +43 -0
- package/skills/pmx-canvas-testing/SKILL.md +17 -0
- package/src/cli/agent.ts +66 -5
- package/src/cli/index.ts +2 -23
- package/src/client/canvas/AttentionHistory.tsx +14 -1
- package/src/client/canvas/CanvasNode.tsx +1 -1
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/DockedNode.tsx +110 -12
- package/src/client/canvas/ExpandedNodeOverlay.tsx +8 -3
- package/src/client/canvas/Minimap.tsx +1 -0
- package/src/client/icons.tsx +1 -0
- package/src/client/nodes/ExtAppFrame.tsx +10 -35
- package/src/client/nodes/HtmlNode.tsx +151 -0
- package/src/client/nodes/McpAppNode.tsx +2 -2
- package/src/client/state/canvas-store.ts +24 -2
- package/src/client/state/intent-bridge.ts +4 -3
- package/src/client/state/sse-bridge.ts +2 -0
- package/src/client/theme/global.css +141 -0
- package/src/client/types.ts +3 -0
- package/src/mcp/canvas-access.ts +34 -7
- package/src/mcp/server.ts +199 -26
- package/src/server/agent-context.ts +50 -3
- package/src/server/canvas-operations.ts +55 -3
- package/src/server/canvas-provenance.ts +2 -1
- package/src/server/canvas-serialization.ts +38 -13
- package/src/server/canvas-state.ts +305 -34
- package/src/server/demo.ts +792 -0
- package/src/server/diagram-presets.ts +45 -25
- package/src/server/index.ts +64 -7
- package/src/server/mcp-app-runtime.ts +15 -5
- package/src/server/server.ts +169 -63
- package/src/server/web-artifacts.ts +116 -3
- package/src/shared/canvas-node-kind.ts +14 -0
package/src/server/server.ts
CHANGED
|
@@ -65,7 +65,13 @@ import {
|
|
|
65
65
|
import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
|
|
66
66
|
import { searchNodes, buildSpatialContext } from './spatial-analysis.js';
|
|
67
67
|
import { diffLayouts, formatDiff, mutationHistory } from './mutation-history.js';
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
buildCanvasSummary,
|
|
70
|
+
serializeCanvasLayout,
|
|
71
|
+
serializeCanvasLayoutWithBlobSummaries,
|
|
72
|
+
serializeCanvasNode,
|
|
73
|
+
serializeCanvasNodeWithBlobSummaries,
|
|
74
|
+
} from './canvas-serialization.js';
|
|
69
75
|
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
70
76
|
import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
|
|
71
77
|
import { validateLocalImageFile } from './image-source.js';
|
|
@@ -82,6 +88,7 @@ import {
|
|
|
82
88
|
deleteCanvasSnapshot,
|
|
83
89
|
executeCanvasBatch,
|
|
84
90
|
fitCanvasView,
|
|
91
|
+
gcCanvasSnapshots,
|
|
85
92
|
groupCanvasNodes,
|
|
86
93
|
listCanvasSnapshots,
|
|
87
94
|
refreshCanvasWebpageNode,
|
|
@@ -91,11 +98,14 @@ import {
|
|
|
91
98
|
saveCanvasSnapshot,
|
|
92
99
|
scheduleCodeGraphRecompute,
|
|
93
100
|
primeCanvasRuntimeBackends,
|
|
101
|
+
setCanvasLayoutUpdateEmitter,
|
|
94
102
|
syncCanvasRuntimeBackends,
|
|
95
103
|
setCanvasContextPins,
|
|
96
104
|
ungroupCanvasNodes,
|
|
97
105
|
validateCanvasNodePatch,
|
|
98
106
|
hasStructuredNodeUpdateFields,
|
|
107
|
+
hasTraceNodeDataFields,
|
|
108
|
+
mergeTraceNodeDataFields,
|
|
99
109
|
} from './canvas-operations.js';
|
|
100
110
|
import { validateCanvasLayout } from './canvas-validation.js';
|
|
101
111
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
@@ -1184,7 +1194,15 @@ function serveCanvasFavicon(): Response {
|
|
|
1184
1194
|
async function handleCanvasUpdate(req: Request): Promise<Response> {
|
|
1185
1195
|
const body = await readJson(req);
|
|
1186
1196
|
const updates = Array.isArray(body.updates) ? body.updates : [];
|
|
1187
|
-
const result =
|
|
1197
|
+
const result = body.recordHistory === false
|
|
1198
|
+
? (() => {
|
|
1199
|
+
let suppressedResult: ReturnType<typeof applyCanvasNodeUpdates> = { applied: 0, skipped: updates.length };
|
|
1200
|
+
canvasState.withSuppressedRecording(() => {
|
|
1201
|
+
suppressedResult = applyCanvasNodeUpdates(updates);
|
|
1202
|
+
});
|
|
1203
|
+
return suppressedResult;
|
|
1204
|
+
})()
|
|
1205
|
+
: applyCanvasNodeUpdates(updates);
|
|
1188
1206
|
if (result.applied > 0) {
|
|
1189
1207
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1190
1208
|
}
|
|
@@ -1234,7 +1252,7 @@ async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
|
1234
1252
|
}
|
|
1235
1253
|
|
|
1236
1254
|
// ── Add node from client ─────────────────────────────────────
|
|
1237
|
-
const VALID_NODE_TYPES = new Set(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'mcp-app', 'webpage', 'group']);
|
|
1255
|
+
const VALID_NODE_TYPES = new Set(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'mcp-app', 'webpage', 'html', 'group']);
|
|
1238
1256
|
|
|
1239
1257
|
function buildNodeResponse(node: CanvasNodeState): Record<string, unknown> {
|
|
1240
1258
|
const serialized = serializeCanvasNode(node);
|
|
@@ -1323,6 +1341,11 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1323
1341
|
const content = type === 'image' && typeof body.path === 'string' && typeof body.content !== 'string'
|
|
1324
1342
|
? body.path
|
|
1325
1343
|
: body.content;
|
|
1344
|
+
// For html nodes, accept top-level `html` field and merge into data so callers
|
|
1345
|
+
// can POST { type: 'html', title, html } without nesting under `data`.
|
|
1346
|
+
const htmlMergedData = type === 'html' && typeof body.html === 'string'
|
|
1347
|
+
? { ...(extraData ?? {}), html: body.html }
|
|
1348
|
+
: extraData;
|
|
1326
1349
|
let added: ReturnType<typeof addCanvasNode>;
|
|
1327
1350
|
const geometry = resolveCreateGeometry(body);
|
|
1328
1351
|
try {
|
|
@@ -1330,11 +1353,17 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1330
1353
|
type: type as CanvasNodeState['type'],
|
|
1331
1354
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1332
1355
|
...(typeof content === 'string' ? { content } : {}),
|
|
1333
|
-
...(
|
|
1356
|
+
...(htmlMergedData ? { data: htmlMergedData } : {}),
|
|
1357
|
+
...(type === 'trace' && typeof body.toolName === 'string' ? { toolName: body.toolName } : {}),
|
|
1358
|
+
...(type === 'trace' && typeof body.category === 'string' ? { category: body.category } : {}),
|
|
1359
|
+
...(type === 'trace' && typeof body.status === 'string' ? { status: body.status } : {}),
|
|
1360
|
+
...(type === 'trace' && typeof body.duration === 'string' ? { duration: body.duration } : {}),
|
|
1361
|
+
...(type === 'trace' && typeof body.resultSummary === 'string' ? { resultSummary: body.resultSummary } : {}),
|
|
1362
|
+
...(type === 'trace' && typeof body.error === 'string' ? { error: body.error } : {}),
|
|
1334
1363
|
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1335
1364
|
...geometry,
|
|
1336
|
-
defaultWidth: 360,
|
|
1337
|
-
defaultHeight: 200,
|
|
1365
|
+
defaultWidth: type === 'html' ? 720 : 360,
|
|
1366
|
+
defaultHeight: type === 'html' ? 640 : 200,
|
|
1338
1367
|
fileMode: 'auto',
|
|
1339
1368
|
});
|
|
1340
1369
|
} catch (error) {
|
|
@@ -1495,7 +1524,14 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
1495
1524
|
} catch (error) {
|
|
1496
1525
|
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1497
1526
|
}
|
|
1498
|
-
} else if (
|
|
1527
|
+
} else if (
|
|
1528
|
+
body.title !== undefined ||
|
|
1529
|
+
body.content !== undefined ||
|
|
1530
|
+
body.data ||
|
|
1531
|
+
typeof body.arrangeLocked === 'boolean' ||
|
|
1532
|
+
typeof body.strictSize === 'boolean' ||
|
|
1533
|
+
(existing.type === 'trace' && hasTraceNodeDataFields(body))
|
|
1534
|
+
) {
|
|
1499
1535
|
const data = { ...existing.data };
|
|
1500
1536
|
if (body.title !== undefined) {
|
|
1501
1537
|
data.title = String(body.title);
|
|
@@ -1524,7 +1560,9 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
1524
1560
|
}
|
|
1525
1561
|
}
|
|
1526
1562
|
}
|
|
1527
|
-
patch.data =
|
|
1563
|
+
patch.data = existing.type === 'trace'
|
|
1564
|
+
? mergeTraceNodeDataFields(data, body)
|
|
1565
|
+
: data;
|
|
1528
1566
|
}
|
|
1529
1567
|
const error = validateCanvasNodePatch({
|
|
1530
1568
|
...(patch.position ? { position: patch.position as { x: number; y: number } } : {}),
|
|
@@ -1639,7 +1677,10 @@ async function handleCanvasBuildWebArtifact(req: Request): Promise<Response> {
|
|
|
1639
1677
|
bytes: result.fileSize,
|
|
1640
1678
|
projectPath: result.projectPath,
|
|
1641
1679
|
openedInCanvas: result.openedInCanvas,
|
|
1680
|
+
startedAt: result.startedAt,
|
|
1642
1681
|
completedAt: result.completedAt,
|
|
1682
|
+
durationMs: result.durationMs,
|
|
1683
|
+
timeoutMs: result.timeoutMs,
|
|
1643
1684
|
// `id` is the canvas node id alias used by every other add-style
|
|
1644
1685
|
// response. It is only present when a canvas node was actually
|
|
1645
1686
|
// created (i.e. openInCanvas was not explicitly disabled). When
|
|
@@ -1888,6 +1929,13 @@ function responseText(text: string, status = 400): Response {
|
|
|
1888
1929
|
});
|
|
1889
1930
|
}
|
|
1890
1931
|
|
|
1932
|
+
function parsePositiveIntegerParam(value: string | null): number | undefined {
|
|
1933
|
+
if (!value) return undefined;
|
|
1934
|
+
const parsed = Number(value);
|
|
1935
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
1936
|
+
return Math.floor(parsed);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1891
1939
|
function handleArtifactView(url: URL): Response {
|
|
1892
1940
|
const pathLike = url.searchParams.get('path') ?? '';
|
|
1893
1941
|
const safePath = resolveWorkspaceArtifactPath(pathLike);
|
|
@@ -2111,29 +2159,44 @@ interface RunAndEmitOpenMcpAppParams {
|
|
|
2111
2159
|
transport: ExternalMcpTransportConfig;
|
|
2112
2160
|
toolName: string;
|
|
2113
2161
|
toolArguments?: Record<string, unknown>;
|
|
2162
|
+
nodeId?: string;
|
|
2114
2163
|
serverName?: string;
|
|
2115
2164
|
title?: string;
|
|
2116
2165
|
x?: number;
|
|
2117
2166
|
y?: number;
|
|
2118
2167
|
width?: number;
|
|
2119
2168
|
height?: number;
|
|
2169
|
+
timeoutMs?: number;
|
|
2120
2170
|
}
|
|
2121
2171
|
|
|
2122
2172
|
async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise<Response> {
|
|
2123
2173
|
try {
|
|
2174
|
+
const targetNode = params.nodeId ? canvasState.getNode(params.nodeId) : undefined;
|
|
2175
|
+
if (params.nodeId && !targetNode) {
|
|
2176
|
+
return responseJson({ ok: false, error: `Node "${params.nodeId}" not found.` }, 404);
|
|
2177
|
+
}
|
|
2178
|
+
if (targetNode && (targetNode.type !== 'mcp-app' || targetNode.data.mode !== 'ext-app')) {
|
|
2179
|
+
return responseJson({ ok: false, error: `Node "${params.nodeId}" is not an external app node.` }, 400);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2124
2182
|
const opened = await openMcpApp({
|
|
2125
2183
|
transport: params.transport,
|
|
2126
2184
|
toolName: params.toolName,
|
|
2127
2185
|
...(params.toolArguments ? { toolArguments: params.toolArguments } : {}),
|
|
2128
2186
|
...(params.serverName ? { serverName: params.serverName } : {}),
|
|
2187
|
+
...(typeof params.timeoutMs === 'number' ? { timeoutMs: params.timeoutMs } : {}),
|
|
2129
2188
|
});
|
|
2130
2189
|
|
|
2131
2190
|
const toolCallId = randomExtAppToolCallId();
|
|
2132
|
-
|
|
2191
|
+
if (params.nodeId) closeNodeAppSession(targetNode);
|
|
2192
|
+
const nodeIdSeed = params.nodeId ?? (toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`);
|
|
2133
2193
|
const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
|
|
2134
2194
|
? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
|
|
2135
2195
|
: opened.toolResult;
|
|
2136
|
-
const nodeTitle = params.title
|
|
2196
|
+
const nodeTitle = params.title
|
|
2197
|
+
?? (typeof targetNode?.data.title === 'string' ? targetNode.data.title : undefined)
|
|
2198
|
+
?? opened.tool.title
|
|
2199
|
+
?? opened.tool.name;
|
|
2137
2200
|
|
|
2138
2201
|
emitPrimaryWorkbenchEvent('ext-app-open', {
|
|
2139
2202
|
toolCallId,
|
|
@@ -2163,7 +2226,7 @@ async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise
|
|
|
2163
2226
|
success: toolResult.isError !== true,
|
|
2164
2227
|
result: toolResult,
|
|
2165
2228
|
});
|
|
2166
|
-
const nodeId = findCanvasExtAppNodeId(toolCallId);
|
|
2229
|
+
const nodeId = params.nodeId ?? findCanvasExtAppNodeId(toolCallId);
|
|
2167
2230
|
|
|
2168
2231
|
return responseJson({
|
|
2169
2232
|
ok: true,
|
|
@@ -2202,17 +2265,22 @@ async function handleCanvasOpenMcpApp(req: Request): Promise<Response> {
|
|
|
2202
2265
|
const requestedServerName = typeof body.serverName === 'string' && body.serverName.trim().length > 0
|
|
2203
2266
|
? body.serverName.trim()
|
|
2204
2267
|
: undefined;
|
|
2268
|
+
const requestedNodeId = typeof body.nodeId === 'string' && body.nodeId.trim().length > 0
|
|
2269
|
+
? body.nodeId.trim()
|
|
2270
|
+
: undefined;
|
|
2205
2271
|
|
|
2206
2272
|
return runAndEmitOpenMcpApp({
|
|
2207
2273
|
transport,
|
|
2208
2274
|
toolName,
|
|
2209
2275
|
...(toolArguments ? { toolArguments } : {}),
|
|
2276
|
+
...(requestedNodeId ? { nodeId: requestedNodeId } : {}),
|
|
2210
2277
|
...(requestedServerName ? { serverName: requestedServerName } : {}),
|
|
2211
2278
|
...(requestedTitle ? { title: requestedTitle } : {}),
|
|
2212
2279
|
...(typeof body.x === 'number' ? { x: body.x } : {}),
|
|
2213
2280
|
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
2214
2281
|
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
2215
2282
|
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
2283
|
+
...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
|
|
2216
2284
|
});
|
|
2217
2285
|
}
|
|
2218
2286
|
|
|
@@ -2222,11 +2290,13 @@ async function handleCanvasAddDiagram(req: Request): Promise<Response> {
|
|
|
2222
2290
|
try {
|
|
2223
2291
|
built = buildExcalidrawOpenMcpAppInput({
|
|
2224
2292
|
elements: body.elements,
|
|
2293
|
+
...(typeof body.nodeId === 'string' ? { nodeId: body.nodeId } : {}),
|
|
2225
2294
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
2226
2295
|
...(typeof body.x === 'number' ? { x: body.x } : {}),
|
|
2227
2296
|
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
2228
2297
|
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
2229
2298
|
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
2299
|
+
...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
|
|
2230
2300
|
});
|
|
2231
2301
|
} catch (error) {
|
|
2232
2302
|
return responseJson({
|
|
@@ -2239,11 +2309,13 @@ async function handleCanvasAddDiagram(req: Request): Promise<Response> {
|
|
|
2239
2309
|
toolName: built.toolName,
|
|
2240
2310
|
toolArguments: built.toolArguments,
|
|
2241
2311
|
serverName: built.serverName,
|
|
2312
|
+
...(built.nodeId ? { nodeId: built.nodeId } : {}),
|
|
2242
2313
|
...(built.title ? { title: built.title } : {}),
|
|
2243
2314
|
...(typeof built.x === 'number' ? { x: built.x } : {}),
|
|
2244
2315
|
...(typeof built.y === 'number' ? { y: built.y } : {}),
|
|
2245
2316
|
...(typeof built.width === 'number' ? { width: built.width } : {}),
|
|
2246
2317
|
...(typeof built.height === 'number' ? { height: built.height } : {}),
|
|
2318
|
+
...(typeof built.timeoutMs === 'number' ? { timeoutMs: built.timeoutMs } : {}),
|
|
2247
2319
|
});
|
|
2248
2320
|
}
|
|
2249
2321
|
|
|
@@ -3044,6 +3116,18 @@ async function handleSnapshotSave(req: Request): Promise<Response> {
|
|
|
3044
3116
|
return responseJson({ ok: true, id: snapshot.id, snapshot });
|
|
3045
3117
|
}
|
|
3046
3118
|
|
|
3119
|
+
async function handleSnapshotGc(req: Request): Promise<Response> {
|
|
3120
|
+
const body = await readJson(req);
|
|
3121
|
+
const keepValue = body.keep;
|
|
3122
|
+
const keep = typeof keepValue === 'number'
|
|
3123
|
+
? keepValue
|
|
3124
|
+
: typeof keepValue === 'string'
|
|
3125
|
+
? Number(keepValue)
|
|
3126
|
+
: undefined;
|
|
3127
|
+
const dryRun = body.dryRun === true || body['dry-run'] === true;
|
|
3128
|
+
return responseJson(gcCanvasSnapshots({ keep, dryRun }));
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3047
3131
|
async function handleContextPinsUpdate(req: Request): Promise<Response> {
|
|
3048
3132
|
const body = await readJson(req);
|
|
3049
3133
|
const MAX_PINS = 20;
|
|
@@ -3284,9 +3368,9 @@ function syncContextNodeToCanvasState(
|
|
|
3284
3368
|
position: { x: 1130, y: 80 },
|
|
3285
3369
|
size: { width: 320, height: 400 },
|
|
3286
3370
|
zIndex: 1,
|
|
3287
|
-
collapsed:
|
|
3371
|
+
collapsed: true,
|
|
3288
3372
|
pinned: false,
|
|
3289
|
-
dockPosition:
|
|
3373
|
+
dockPosition: 'right',
|
|
3290
3374
|
data: mergedData,
|
|
3291
3375
|
});
|
|
3292
3376
|
return;
|
|
@@ -3408,52 +3492,56 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3408
3492
|
});
|
|
3409
3493
|
}
|
|
3410
3494
|
} else if (event === 'ext-app-update') {
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3495
|
+
canvasState.withSuppressedRecording(() => {
|
|
3496
|
+
const toolCallId = payload.toolCallId as string;
|
|
3497
|
+
if (!toolCallId) return;
|
|
3498
|
+
const payloadNodeId = typeof payload.nodeId === 'string' ? payload.nodeId : '';
|
|
3499
|
+
const id =
|
|
3500
|
+
(payloadNodeId && canvasState.getNode(payloadNodeId) ? payloadNodeId : null) ||
|
|
3501
|
+
findCanvasExtAppNodeId(toolCallId) ||
|
|
3502
|
+
(typeof payload.serverName === 'string' && typeof payload.toolName === 'string'
|
|
3503
|
+
? findOnlyPendingCanvasExtAppNodeId(payload.serverName, payload.toolName)
|
|
3504
|
+
: null);
|
|
3505
|
+
if (!id) return;
|
|
3506
|
+
const existing = canvasState.getNode(id);
|
|
3507
|
+
if (existing) {
|
|
3508
|
+
canvasState.updateNode(id, { data: { ...existing.data, html: payload.html } });
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3425
3511
|
} else if (event === 'ext-app-result') {
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3512
|
+
canvasState.withSuppressedRecording(() => {
|
|
3513
|
+
const toolCallId = payload.toolCallId as string;
|
|
3514
|
+
if (!toolCallId) return;
|
|
3515
|
+
const payloadNodeId = typeof payload.nodeId === 'string' ? payload.nodeId : '';
|
|
3516
|
+
const id =
|
|
3517
|
+
(payloadNodeId && canvasState.getNode(payloadNodeId) ? payloadNodeId : null) ||
|
|
3518
|
+
findCanvasExtAppNodeId(toolCallId) ||
|
|
3519
|
+
(typeof payload.serverName === 'string' && typeof payload.toolName === 'string'
|
|
3520
|
+
? findOnlyPendingCanvasExtAppNodeId(payload.serverName, payload.toolName)
|
|
3521
|
+
: null);
|
|
3522
|
+
if (!id) return;
|
|
3523
|
+
if (payload.success === false) {
|
|
3524
|
+
closeNodeAppSession(canvasState.getNode(id));
|
|
3525
|
+
canvasState.removeNode(id);
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
const existing = canvasState.getNode(id);
|
|
3529
|
+
if (existing) {
|
|
3530
|
+
canvasState.updateNode(id, {
|
|
3531
|
+
data: {
|
|
3532
|
+
...existing.data,
|
|
3533
|
+
toolResult: normalizeExtAppToolResult({
|
|
3534
|
+
result: payload.result,
|
|
3535
|
+
success: typeof payload.success === 'boolean' ? payload.success : undefined,
|
|
3536
|
+
error: typeof payload.error === 'string' ? payload.error : undefined,
|
|
3537
|
+
content: typeof payload.content === 'string' ? payload.content : undefined,
|
|
3538
|
+
detailedContent:
|
|
3539
|
+
typeof payload.detailedContent === 'string' ? payload.detailedContent : undefined,
|
|
3540
|
+
}),
|
|
3541
|
+
},
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
});
|
|
3457
3545
|
} else if (event === 'context-cards') {
|
|
3458
3546
|
syncContextNodeToCanvasState(
|
|
3459
3547
|
{ cards: Array.isArray(payload.cards) ? payload.cards : [] },
|
|
@@ -3755,6 +3843,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3755
3843
|
// ── Canvas persistence: set workspace root and load saved state ──
|
|
3756
3844
|
canvasState.setWorkspaceRoot(activeWorkspaceRoot);
|
|
3757
3845
|
const loaded = canvasState.loadFromDisk({ clearExisting: true });
|
|
3846
|
+
setCanvasLayoutUpdateEmitter(() => {
|
|
3847
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
3848
|
+
});
|
|
3758
3849
|
if (loaded) {
|
|
3759
3850
|
console.log(' Canvas state restored from .pmx-canvas/state.json');
|
|
3760
3851
|
primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
|
|
@@ -3858,7 +3949,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3858
3949
|
|
|
3859
3950
|
// Canvas state API
|
|
3860
3951
|
if (url.pathname === '/api/canvas/state' && req.method === 'GET') {
|
|
3861
|
-
|
|
3952
|
+
const includeBlobs = url.searchParams.get('includeBlobs') === 'true';
|
|
3953
|
+
return responseJson(includeBlobs
|
|
3954
|
+
? serializeCanvasLayout(canvasState.getLayout())
|
|
3955
|
+
: serializeCanvasLayoutWithBlobSummaries(canvasState.getLayoutForPersistence()));
|
|
3862
3956
|
}
|
|
3863
3957
|
|
|
3864
3958
|
if (url.pathname === '/api/canvas/summary' && req.method === 'GET') {
|
|
@@ -3908,10 +4002,13 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3908
4002
|
}
|
|
3909
4003
|
|
|
3910
4004
|
if (url.pathname.startsWith('/api/canvas/node/') && req.method === 'GET') {
|
|
3911
|
-
const nodeId = url.pathname.slice('/api/canvas/node/'.length);
|
|
3912
|
-
const
|
|
4005
|
+
const nodeId = decodeURIComponent(url.pathname.slice('/api/canvas/node/'.length));
|
|
4006
|
+
const includeBlobs = url.searchParams.get('includeBlobs') === 'true';
|
|
4007
|
+
const node = includeBlobs ? canvasState.getNode(nodeId) : canvasState.getNodeForPersistence(nodeId);
|
|
3913
4008
|
if (!node) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
3914
|
-
return responseJson(
|
|
4009
|
+
return responseJson(includeBlobs
|
|
4010
|
+
? serializeCanvasNode(node)
|
|
4011
|
+
: serializeCanvasNodeWithBlobSummaries(node));
|
|
3915
4012
|
}
|
|
3916
4013
|
|
|
3917
4014
|
if (url.pathname.startsWith('/api/canvas/node/') && req.method === 'PATCH') {
|
|
@@ -3947,13 +4044,21 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3947
4044
|
|
|
3948
4045
|
// Snapshot API
|
|
3949
4046
|
if (url.pathname === '/api/canvas/snapshots' && req.method === 'GET') {
|
|
3950
|
-
return responseJson(listCanvasSnapshots(
|
|
4047
|
+
return responseJson(listCanvasSnapshots({
|
|
4048
|
+
limit: parsePositiveIntegerParam(url.searchParams.get('limit')),
|
|
4049
|
+
query: url.searchParams.get('q') ?? url.searchParams.get('query') ?? undefined,
|
|
4050
|
+
all: url.searchParams.get('all') === 'true',
|
|
4051
|
+
}));
|
|
3951
4052
|
}
|
|
3952
4053
|
|
|
3953
4054
|
if (url.pathname === '/api/canvas/snapshots' && req.method === 'POST') {
|
|
3954
4055
|
return handleSnapshotSave(req);
|
|
3955
4056
|
}
|
|
3956
4057
|
|
|
4058
|
+
if (url.pathname === '/api/canvas/snapshots/gc' && req.method === 'POST') {
|
|
4059
|
+
return handleSnapshotGc(req);
|
|
4060
|
+
}
|
|
4061
|
+
|
|
3957
4062
|
if (url.pathname.startsWith('/api/canvas/snapshots/') && url.pathname.endsWith('/diff') && req.method === 'GET') {
|
|
3958
4063
|
const id = decodeURIComponent(url.pathname.slice('/api/canvas/snapshots/'.length, -'/diff'.length));
|
|
3959
4064
|
const snapshot = canvasState.getSnapshotData(id);
|
|
@@ -4137,6 +4242,7 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4137
4242
|
export function stopCanvasServer(): void {
|
|
4138
4243
|
canvasState.flushToDisk();
|
|
4139
4244
|
closeAllMcpAppSessions();
|
|
4245
|
+
setCanvasLayoutUpdateEmitter(null);
|
|
4140
4246
|
void closeCanvasAutomationWebViewInternal().catch((error) => {
|
|
4141
4247
|
logWorkbenchWarning('stopCanvasServer closeCanvasAutomationWebViewInternal', error);
|
|
4142
4248
|
});
|
|
@@ -28,6 +28,11 @@ const DEFAULT_TIMEOUT_MS = 10 * 60_000;
|
|
|
28
28
|
const DEFAULT_PACKAGE_MANAGER = 'pnpm@10.33.0';
|
|
29
29
|
const DEFAULT_WEB_ARTIFACT_NODE_SIZE = { width: 960, height: 720 };
|
|
30
30
|
const FALLBACK_PATH_DIRS = ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin'];
|
|
31
|
+
const WEB_ARTIFACT_CONTEXT_MAX_LENGTH = 1200;
|
|
32
|
+
const WEB_ARTIFACT_SOURCE_PREVIEW_MAX_LENGTH = 700;
|
|
33
|
+
const WEB_ARTIFACT_CSS_PREVIEW_MAX_LENGTH = 260;
|
|
34
|
+
const WEB_ARTIFACT_MAX_LISTED_SOURCE_FILES = 8;
|
|
35
|
+
const WEB_ARTIFACT_MAX_STORED_SOURCE_FILES = 32;
|
|
31
36
|
|
|
32
37
|
export interface WebArtifactBuildInput {
|
|
33
38
|
title: string;
|
|
@@ -49,6 +54,7 @@ export interface WebArtifactBuildOutput {
|
|
|
49
54
|
fileSize: number;
|
|
50
55
|
projectPath: string;
|
|
51
56
|
metadata: Record<string, unknown>;
|
|
57
|
+
sourceContext: WebArtifactSourceContext;
|
|
52
58
|
logs?: {
|
|
53
59
|
stdout?: WebArtifactLogSummary;
|
|
54
60
|
stderr?: WebArtifactLogSummary;
|
|
@@ -64,6 +70,14 @@ export interface WebArtifactLogSummary {
|
|
|
64
70
|
suppressedNoiseCount: number;
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
export interface WebArtifactSourceContext {
|
|
74
|
+
content: string;
|
|
75
|
+
sourceFiles: string[];
|
|
76
|
+
sourceFileCount: number;
|
|
77
|
+
sourcePreview: string;
|
|
78
|
+
deps?: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
export interface WebArtifactCanvasOpenResult {
|
|
68
82
|
nodeId: string;
|
|
69
83
|
url: string;
|
|
@@ -73,7 +87,10 @@ export interface WebArtifactCanvasBuildResult extends WebArtifactBuildOutput {
|
|
|
73
87
|
openedInCanvas: boolean;
|
|
74
88
|
nodeId?: string;
|
|
75
89
|
url?: string;
|
|
90
|
+
startedAt: string;
|
|
76
91
|
completedAt: string;
|
|
92
|
+
durationMs: number;
|
|
93
|
+
timeoutMs: number;
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
function currentWorkspaceRoot(): string {
|
|
@@ -114,6 +131,67 @@ function slugify(value: string): string {
|
|
|
114
131
|
return slug.length > 0 ? slug : 'web-artifact';
|
|
115
132
|
}
|
|
116
133
|
|
|
134
|
+
function normalizePreviewText(text: string): string {
|
|
135
|
+
return text
|
|
136
|
+
.replace(/\r\n?/g, '\n')
|
|
137
|
+
.replace(/[ \t\f\v]+/g, ' ')
|
|
138
|
+
.replace(/\n\s+/g, '\n')
|
|
139
|
+
.replace(/\s+\n/g, '\n')
|
|
140
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
141
|
+
.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function truncatePreviewText(text: string, maxLength: number): string {
|
|
145
|
+
if (maxLength <= 0) return '';
|
|
146
|
+
const normalized = normalizePreviewText(text);
|
|
147
|
+
if (normalized.length <= maxLength) return normalized;
|
|
148
|
+
if (maxLength <= 3) return normalized.slice(0, maxLength);
|
|
149
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatSourceFileList(files: string[]): string {
|
|
153
|
+
const visible = files.slice(0, WEB_ARTIFACT_MAX_LISTED_SOURCE_FILES);
|
|
154
|
+
const remaining = files.length - visible.length;
|
|
155
|
+
return `${visible.join(', ')}${remaining > 0 ? `, +${remaining} more` : ''}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildWebArtifactSourceContext(
|
|
159
|
+
input: Pick<WebArtifactBuildInput, 'title' | 'appTsx' | 'indexCss' | 'mainTsx' | 'indexHtml' | 'files'>,
|
|
160
|
+
options: { deps: string[]; fileSize: number },
|
|
161
|
+
): WebArtifactSourceContext {
|
|
162
|
+
const sourceFiles = new Set<string>(['src/App.tsx']);
|
|
163
|
+
if (typeof input.indexCss === 'string') sourceFiles.add('src/index.css');
|
|
164
|
+
if (typeof input.mainTsx === 'string') sourceFiles.add('src/main.tsx');
|
|
165
|
+
if (typeof input.indexHtml === 'string') sourceFiles.add('index.html');
|
|
166
|
+
for (const pathKey of Object.keys(input.files ?? {}).sort()) {
|
|
167
|
+
sourceFiles.add(pathKey);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const appPreview = truncatePreviewText(input.appTsx, WEB_ARTIFACT_SOURCE_PREVIEW_MAX_LENGTH);
|
|
171
|
+
const cssPreview = typeof input.indexCss === 'string'
|
|
172
|
+
? truncatePreviewText(input.indexCss, WEB_ARTIFACT_CSS_PREVIEW_MAX_LENGTH)
|
|
173
|
+
: '';
|
|
174
|
+
const allSourceFiles = [...sourceFiles];
|
|
175
|
+
const storedSourceFiles = allSourceFiles.slice(0, WEB_ARTIFACT_MAX_STORED_SOURCE_FILES);
|
|
176
|
+
const parts = [
|
|
177
|
+
`Web artifact: ${input.title}`,
|
|
178
|
+
`Source files: ${formatSourceFileList(allSourceFiles)}`,
|
|
179
|
+
`Artifact bytes: ${options.fileSize}`,
|
|
180
|
+
options.deps.length > 0 ? `Dependencies: ${options.deps.join(', ')}` : '',
|
|
181
|
+
`App source preview:\n${appPreview}`,
|
|
182
|
+
cssPreview ? `CSS source preview:\n${cssPreview}` : '',
|
|
183
|
+
].filter(Boolean);
|
|
184
|
+
|
|
185
|
+
const content = truncatePreviewText(parts.join('\n'), WEB_ARTIFACT_CONTEXT_MAX_LENGTH);
|
|
186
|
+
return {
|
|
187
|
+
content,
|
|
188
|
+
sourceFiles: storedSourceFiles,
|
|
189
|
+
sourceFileCount: allSourceFiles.length,
|
|
190
|
+
sourcePreview: appPreview,
|
|
191
|
+
...(options.deps.length > 0 ? { deps: options.deps } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
117
195
|
function isPathInside(base: string, candidate: string): boolean {
|
|
118
196
|
const rel = relative(base, candidate);
|
|
119
197
|
if (rel === '') return true;
|
|
@@ -458,19 +536,23 @@ export async function executeWebArtifactBuild(
|
|
|
458
536
|
// on a filesystem failure. A post-copy size check would be redundant
|
|
459
537
|
// defensive noise — see CLAUDE.md TypeScript Guardrail #3.
|
|
460
538
|
const fileSize = bundleSize;
|
|
539
|
+
const sourceContext = buildWebArtifactSourceContext(input, { deps, fileSize });
|
|
461
540
|
|
|
462
541
|
return {
|
|
463
542
|
filePath: outputPath,
|
|
464
543
|
fileSize,
|
|
465
544
|
projectPath,
|
|
545
|
+
sourceContext,
|
|
466
546
|
metadata: {
|
|
467
547
|
title: input.title,
|
|
468
548
|
bundlePath,
|
|
469
549
|
projectPath,
|
|
550
|
+
sourceFiles: sourceContext.sourceFiles,
|
|
551
|
+
sourceFileCount: sourceContext.sourceFileCount,
|
|
552
|
+
sourcePreview: sourceContext.sourcePreview,
|
|
470
553
|
hasIndexCss: typeof input.indexCss === 'string',
|
|
471
554
|
...(deps.length > 0 ? { deps } : {}),
|
|
472
555
|
extraFileCount: Object.keys(input.files ?? {}).length,
|
|
473
|
-
outputPreview: readFileSync(outputPath, 'utf-8').slice(0, 200),
|
|
474
556
|
},
|
|
475
557
|
logs: {
|
|
476
558
|
...(summarizeArtifactLog(stdout) ? { stdout: summarizeArtifactLog(stdout) } : {}),
|
|
@@ -484,6 +566,13 @@ export async function executeWebArtifactBuild(
|
|
|
484
566
|
export function openWebArtifactInCanvas(input: {
|
|
485
567
|
title: string;
|
|
486
568
|
filePath: string;
|
|
569
|
+
fileSize?: number;
|
|
570
|
+
projectPath?: string;
|
|
571
|
+
content?: string;
|
|
572
|
+
sourceFiles?: string[];
|
|
573
|
+
sourceFileCount?: number;
|
|
574
|
+
sourcePreview?: string;
|
|
575
|
+
deps?: string[];
|
|
487
576
|
}): WebArtifactCanvasOpenResult {
|
|
488
577
|
const width = DEFAULT_WEB_ARTIFACT_NODE_SIZE.width;
|
|
489
578
|
const height = DEFAULT_WEB_ARTIFACT_NODE_SIZE.height;
|
|
@@ -501,8 +590,15 @@ export function openWebArtifactInCanvas(input: {
|
|
|
501
590
|
dockPosition: null,
|
|
502
591
|
data: {
|
|
503
592
|
title: input.title,
|
|
593
|
+
...(typeof input.content === 'string' ? { content: input.content } : {}),
|
|
504
594
|
url,
|
|
505
595
|
path: input.filePath,
|
|
596
|
+
...(typeof input.fileSize === 'number' ? { artifactBytes: input.fileSize } : {}),
|
|
597
|
+
...(typeof input.projectPath === 'string' ? { projectPath: input.projectPath } : {}),
|
|
598
|
+
...(Array.isArray(input.sourceFiles) ? { sourceFiles: input.sourceFiles } : {}),
|
|
599
|
+
...(typeof input.sourceFileCount === 'number' ? { sourceFileCount: input.sourceFileCount } : {}),
|
|
600
|
+
...(typeof input.sourcePreview === 'string' ? { sourcePreview: input.sourcePreview } : {}),
|
|
601
|
+
...(Array.isArray(input.deps) && input.deps.length > 0 ? { deps: input.deps } : {}),
|
|
506
602
|
trustedDomain: true,
|
|
507
603
|
sourceServer: 'pmx-canvas',
|
|
508
604
|
hostMode: 'hosted',
|
|
@@ -518,19 +614,36 @@ export function openWebArtifactInCanvas(input: {
|
|
|
518
614
|
export async function buildWebArtifactOnCanvas(input: WebArtifactBuildInput & {
|
|
519
615
|
openInCanvas?: boolean;
|
|
520
616
|
}): Promise<WebArtifactCanvasBuildResult> {
|
|
617
|
+
const startedMs = Date.now();
|
|
618
|
+
const startedAt = new Date(startedMs).toISOString();
|
|
619
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
521
620
|
const build = await executeWebArtifactBuild(input);
|
|
621
|
+
const completedMs = Date.now();
|
|
622
|
+
const timing = {
|
|
623
|
+
startedAt,
|
|
624
|
+
completedAt: new Date(completedMs).toISOString(),
|
|
625
|
+
durationMs: completedMs - startedMs,
|
|
626
|
+
timeoutMs,
|
|
627
|
+
};
|
|
522
628
|
if (input.openInCanvas === false) {
|
|
523
|
-
return { ...build, openedInCanvas: false,
|
|
629
|
+
return { ...build, openedInCanvas: false, ...timing };
|
|
524
630
|
}
|
|
525
631
|
const opened = openWebArtifactInCanvas({
|
|
526
632
|
title: input.title,
|
|
527
633
|
filePath: build.filePath,
|
|
634
|
+
fileSize: build.fileSize,
|
|
635
|
+
projectPath: build.projectPath,
|
|
636
|
+
content: build.sourceContext.content,
|
|
637
|
+
sourceFiles: build.sourceContext.sourceFiles,
|
|
638
|
+
sourceFileCount: build.sourceContext.sourceFileCount,
|
|
639
|
+
sourcePreview: build.sourceContext.sourcePreview,
|
|
640
|
+
...(Array.isArray(build.sourceContext.deps) ? { deps: build.sourceContext.deps } : {}),
|
|
528
641
|
});
|
|
529
642
|
return {
|
|
530
643
|
...build,
|
|
531
644
|
openedInCanvas: true,
|
|
532
645
|
nodeId: opened.nodeId,
|
|
533
646
|
url: opened.url,
|
|
534
|
-
|
|
647
|
+
...timing,
|
|
535
648
|
};
|
|
536
649
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CanvasNodeKindInput {
|
|
2
|
+
type: string;
|
|
3
|
+
data: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function getCanvasNodeKind(node: CanvasNodeKindInput): string {
|
|
7
|
+
if (node.type !== 'mcp-app') return node.type;
|
|
8
|
+
|
|
9
|
+
const data = node.data;
|
|
10
|
+
if (data.viewerType === 'web-artifact') return 'web-artifact';
|
|
11
|
+
if (data.mode === 'ext-app') return 'external-app';
|
|
12
|
+
if (data.hostMode === 'hosted' && typeof data.path === 'string') return 'web-artifact';
|
|
13
|
+
return 'mcp-app';
|
|
14
|
+
}
|