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/mcp/server.ts
CHANGED
|
@@ -165,9 +165,73 @@ function encodeBase64(bytes: Uint8Array): string {
|
|
|
165
165
|
return Buffer.from(bytes).toString('base64');
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
function wantsFullPayload(input: { full?: boolean; verbose?: boolean; includeData?: boolean } = {}): boolean {
|
|
169
|
+
return input.full === true || input.verbose === true || input.includeData === true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function compactNodePayload(node: Awaited<ReturnType<CanvasAccess['getNode']>>): Record<string, unknown> | null {
|
|
173
|
+
if (!node) return null;
|
|
174
|
+
const serialized = serializeCanvasNode(node);
|
|
175
|
+
return {
|
|
176
|
+
id: serialized.id,
|
|
177
|
+
type: serialized.type,
|
|
178
|
+
kind: serialized.kind,
|
|
179
|
+
title: serialized.title,
|
|
180
|
+
content: serialized.content,
|
|
181
|
+
position: serialized.position,
|
|
182
|
+
size: serialized.size,
|
|
183
|
+
pinned: serialized.pinned,
|
|
184
|
+
collapsed: serialized.collapsed,
|
|
185
|
+
dockPosition: serialized.dockPosition,
|
|
186
|
+
provenance: serialized.provenance,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function compactLayoutPayload(layout: Awaited<ReturnType<CanvasAccess['getLayout']>>, pinnedIds: string[]): Record<string, unknown> {
|
|
191
|
+
return {
|
|
192
|
+
summary: buildSummaryFromLayout(layout, pinnedIds),
|
|
193
|
+
viewport: layout.viewport,
|
|
194
|
+
nodes: layout.nodes.map((node) => compactNodePayload(node)).filter((node): node is Record<string, unknown> => node !== null),
|
|
195
|
+
edges: layout.edges.map((edge) => ({
|
|
196
|
+
id: edge.id,
|
|
197
|
+
from: edge.from,
|
|
198
|
+
to: edge.to,
|
|
199
|
+
type: edge.type,
|
|
200
|
+
...(edge.label ? { label: edge.label } : {}),
|
|
201
|
+
...(edge.style ? { style: edge.style } : {}),
|
|
202
|
+
...(edge.animated !== undefined ? { animated: edge.animated } : {}),
|
|
203
|
+
})),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function compactBatchValue(value: unknown): unknown {
|
|
208
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
|
|
209
|
+
const record = value as Record<string, unknown>;
|
|
210
|
+
const nodeLike = typeof record.id === 'string' && typeof record.type === 'string';
|
|
211
|
+
const compact: Record<string, unknown> = {};
|
|
212
|
+
for (const key of ['ok', 'id', 'type', 'kind', 'title', 'content', 'position', 'size', 'fetch', 'error', 'from', 'to', 'groupId', 'nodeIds', 'snapshot', 'arranged', 'layout']) {
|
|
213
|
+
if (record[key] !== undefined) compact[key] = record[key];
|
|
214
|
+
}
|
|
215
|
+
if (nodeLike) return compact;
|
|
216
|
+
return record;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function compactBatchResult(result: { ok: boolean; results: Array<Record<string, unknown>>; refs: Record<string, unknown>; failedIndex?: number; error?: string }): Record<string, unknown> {
|
|
220
|
+
return {
|
|
221
|
+
ok: result.ok,
|
|
222
|
+
...(result.failedIndex !== undefined ? { failedIndex: result.failedIndex } : {}),
|
|
223
|
+
...(result.error ? { error: result.error } : {}),
|
|
224
|
+
results: result.results.map((entry) => compactBatchValue(entry)),
|
|
225
|
+
refs: Object.fromEntries(Object.entries(result.refs).map(([key, value]) => [key, compactBatchValue(value)])),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function createdNodePayload(c: CanvasAccess, id: string, options: { full?: boolean; verbose?: boolean; includeData?: boolean } = {}): Promise<Record<string, unknown>> {
|
|
169
230
|
const node = await c.getNode(id);
|
|
170
231
|
if (!node) return { ok: true, id };
|
|
232
|
+
if (!wantsFullPayload(options)) {
|
|
233
|
+
return { ok: true, node: compactNodePayload(node), id };
|
|
234
|
+
}
|
|
171
235
|
const serialized = serializeCanvasNode(node);
|
|
172
236
|
return { ok: true, node: serialized, ...serialized };
|
|
173
237
|
}
|
|
@@ -214,13 +278,19 @@ export async function startMcpServer(): Promise<void> {
|
|
|
214
278
|
// ── canvas_get_layout ──────────────────────────────────────────
|
|
215
279
|
server.tool(
|
|
216
280
|
'canvas_get_layout',
|
|
217
|
-
'Get the
|
|
218
|
-
{
|
|
219
|
-
|
|
281
|
+
'Get the canvas layout. Defaults to a compact agent-safe projection; pass full:true for full node data.',
|
|
282
|
+
{
|
|
283
|
+
full: z.boolean().optional().describe('Return the full layout including node data. Default false keeps responses compact.'),
|
|
284
|
+
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
285
|
+
},
|
|
286
|
+
async (input) => {
|
|
220
287
|
const c = await ensureCanvas();
|
|
221
|
-
const layout =
|
|
288
|
+
const layout = await c.getLayout();
|
|
289
|
+
const payload = wantsFullPayload(input)
|
|
290
|
+
? serializeCanvasLayout(layout)
|
|
291
|
+
: compactLayoutPayload(layout, await c.getPinnedNodeIds());
|
|
222
292
|
return {
|
|
223
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
293
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
224
294
|
};
|
|
225
295
|
},
|
|
226
296
|
);
|
|
@@ -228,19 +298,24 @@ export async function startMcpServer(): Promise<void> {
|
|
|
228
298
|
// ── canvas_get_node ────────────────────────────────────────────
|
|
229
299
|
server.tool(
|
|
230
300
|
'canvas_get_node',
|
|
231
|
-
'Get a single node by ID
|
|
232
|
-
{
|
|
233
|
-
|
|
301
|
+
'Get a single node by ID. Defaults to compact metadata; pass full:true to include full data/tool results.',
|
|
302
|
+
{
|
|
303
|
+
id: z.string().describe('The node ID to retrieve'),
|
|
304
|
+
full: z.boolean().optional().describe('Include full node data, including mcp-app tool results. Default false.'),
|
|
305
|
+
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
306
|
+
},
|
|
307
|
+
async (input) => {
|
|
234
308
|
const c = await ensureCanvas();
|
|
235
|
-
const node = await c.getNode(id);
|
|
309
|
+
const node = await c.getNode(input.id);
|
|
236
310
|
if (!node) {
|
|
237
311
|
return {
|
|
238
|
-
content: [{ type: 'text', text: `Node "${id}" not found.` }],
|
|
312
|
+
content: [{ type: 'text', text: `Node "${input.id}" not found.` }],
|
|
239
313
|
isError: true,
|
|
240
314
|
};
|
|
241
315
|
}
|
|
316
|
+
const payload = wantsFullPayload(input) ? serializeCanvasNode(node) : compactNodePayload(node);
|
|
242
317
|
return {
|
|
243
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
318
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
244
319
|
};
|
|
245
320
|
},
|
|
246
321
|
);
|
|
@@ -248,9 +323,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
248
323
|
// ── canvas_add_node ────────────────────────────────────────────
|
|
249
324
|
server.tool(
|
|
250
325
|
'canvas_add_node',
|
|
251
|
-
'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, image, webpage, mcp-app, group. Dedicated node tools: json-render -> canvas_add_json_render_node, graph -> canvas_add_graph_node, web-artifact -> canvas_build_web_artifact, external apps -> canvas_open_mcp_app, groups -> canvas_create_group. Call canvas_describe_schema for the full nodeTypeRouting table.',
|
|
326
|
+
'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, image, webpage, mcp-app, html, group. Dedicated node tools: json-render -> canvas_add_json_render_node, graph -> canvas_add_graph_node, web-artifact -> canvas_build_web_artifact, external apps -> canvas_open_mcp_app, html (preferred) -> canvas_add_html_node, groups -> canvas_create_group. Call canvas_describe_schema for the full nodeTypeRouting table.',
|
|
252
327
|
{
|
|
253
|
-
type: z.enum(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'webpage', 'mcp-app', 'group'])
|
|
328
|
+
type: z.enum(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'webpage', 'mcp-app', 'html', 'group'])
|
|
254
329
|
.describe('Node type (prefer canvas_create_group for groups)'),
|
|
255
330
|
title: z.string().optional().describe('Node title'),
|
|
256
331
|
content: z.string().optional().describe('Node content (markdown for markdown nodes, file path for file nodes, image path/URL/data-URI for image nodes, URL for webpage nodes)'),
|
|
@@ -261,6 +336,14 @@ export async function startMcpServer(): Promise<void> {
|
|
|
261
336
|
width: z.number().optional().describe('Width in pixels (default: 720)'),
|
|
262
337
|
height: z.number().optional().describe('Height in pixels (default: 600)'),
|
|
263
338
|
strictSize: z.boolean().optional().describe('Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting'),
|
|
339
|
+
toolName: z.string().optional().describe('Trace node tool or operation label'),
|
|
340
|
+
category: z.string().optional().describe('Trace node category: mcp, file, subagent, or other'),
|
|
341
|
+
status: z.string().optional().describe('Trace node status: running, success, or failed'),
|
|
342
|
+
duration: z.string().optional().describe('Trace node duration badge text'),
|
|
343
|
+
resultSummary: z.string().optional().describe('Trace node result summary'),
|
|
344
|
+
error: z.string().optional().describe('Trace node error message'),
|
|
345
|
+
full: z.boolean().optional().describe('Return the full created node payload. Default false returns compact metadata.'),
|
|
346
|
+
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
264
347
|
},
|
|
265
348
|
async (input) => {
|
|
266
349
|
const c = await ensureCanvas();
|
|
@@ -291,7 +374,39 @@ export async function startMcpServer(): Promise<void> {
|
|
|
291
374
|
: input;
|
|
292
375
|
const id = await c.addNode(nodeInput);
|
|
293
376
|
return {
|
|
294
|
-
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id), null, 2) }],
|
|
377
|
+
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id, input), null, 2) }],
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// ── canvas_add_html_node ────────────────────────────────────────
|
|
383
|
+
server.tool(
|
|
384
|
+
'canvas_add_html_node',
|
|
385
|
+
'Add an html node: a self-contained HTML document (with optional inline <script> and CDN <script src="...">) rendered inside a sandboxed iframe (sandbox="allow-scripts"). Use this for moderate-complexity visualizations or interactive widgets that need real JS but do not warrant a full React/shadcn build. The iframe inherits canvas theme tokens via injected CSS custom properties (both --c-* and common --color-* aliases) so authored HTML using var(--color-text-secondary), var(--color-bg), etc. renders cohesively. No same-origin access; no top-navigation; no forms. For declarative-only views with zero JS, prefer canvas_add_json_render_node. For React + shadcn + routing or multi-component apps, use canvas_build_web_artifact.',
|
|
386
|
+
{
|
|
387
|
+
html: z.string().describe('HTML document or fragment. Full <html>...</html> documents are passed through with theme styles injected into <head>; bare fragments are wrapped in a minimal document. Inline <script> and remote CDN <script src="..."> are allowed.'),
|
|
388
|
+
title: z.string().optional().describe('Node title shown in the canvas titlebar.'),
|
|
389
|
+
x: z.number().optional().describe('X position (auto-placed if omitted).'),
|
|
390
|
+
y: z.number().optional().describe('Y position (auto-placed if omitted).'),
|
|
391
|
+
width: z.number().optional().describe('Width in pixels (default: 720).'),
|
|
392
|
+
height: z.number().optional().describe('Height in pixels (default: 640).'),
|
|
393
|
+
strictSize: z.boolean().optional().describe('Keep explicit width/height fixed; iframe scrolls overflow internally.'),
|
|
394
|
+
full: z.boolean().optional().describe('Return the full created node payload. Default false returns compact metadata.'),
|
|
395
|
+
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
396
|
+
},
|
|
397
|
+
async (input) => {
|
|
398
|
+
const c = await ensureCanvas();
|
|
399
|
+
const id = await c.addHtmlNode({
|
|
400
|
+
html: input.html,
|
|
401
|
+
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
402
|
+
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
403
|
+
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
404
|
+
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
405
|
+
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
406
|
+
...(input.strictSize === true ? { strictSize: true } : {}),
|
|
407
|
+
});
|
|
408
|
+
return {
|
|
409
|
+
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id, input), null, 2) }],
|
|
295
410
|
};
|
|
296
411
|
},
|
|
297
412
|
);
|
|
@@ -303,11 +418,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
303
418
|
toolName: z.string().describe('Tool name on the external MCP server'),
|
|
304
419
|
serverName: z.string().optional().describe('Optional display name for the external MCP server'),
|
|
305
420
|
toolArguments: z.record(z.string(), z.unknown()).optional().describe('Arguments passed to the external tool call'),
|
|
421
|
+
nodeId: z.string().optional().describe('Existing mcp-app node ID to update in place instead of creating a new node.'),
|
|
306
422
|
title: z.string().optional().describe('Optional canvas node title override'),
|
|
307
423
|
x: z.number().optional().describe('X position (auto-placed if omitted)'),
|
|
308
424
|
y: z.number().optional().describe('Y position (auto-placed if omitted)'),
|
|
309
425
|
width: z.number().optional().describe('Width in pixels (default: 720)'),
|
|
310
426
|
height: z.number().optional().describe('Height in pixels (default: 500)'),
|
|
427
|
+
timeoutMs: z.number().optional().describe('Optional MCP request timeout in milliseconds for cold external app servers'),
|
|
311
428
|
transport: z.union([
|
|
312
429
|
z.object({
|
|
313
430
|
type: z.literal('stdio'),
|
|
@@ -331,11 +448,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
331
448
|
toolName: input.toolName,
|
|
332
449
|
...(typeof input.serverName === 'string' ? { serverName: input.serverName } : {}),
|
|
333
450
|
...(input.toolArguments ? { toolArguments: input.toolArguments } : {}),
|
|
451
|
+
...(typeof input.nodeId === 'string' ? { nodeId: input.nodeId } : {}),
|
|
334
452
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
335
453
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
336
454
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
337
455
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
338
456
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
457
|
+
...(typeof input.timeoutMs === 'number' ? { timeoutMs: input.timeoutMs } : {}),
|
|
339
458
|
});
|
|
340
459
|
return {
|
|
341
460
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -357,27 +476,37 @@ export async function startMcpServer(): Promise<void> {
|
|
|
357
476
|
z.string().describe('JSON array string of Excalidraw elements'),
|
|
358
477
|
z.array(z.record(z.string(), z.unknown())).describe('Array of Excalidraw elements'),
|
|
359
478
|
]).describe('Excalidraw elements to render. See https://github.com/excalidraw/excalidraw-mcp for the element format.'),
|
|
479
|
+
nodeId: z.string().optional().describe('Existing Excalidraw mcp-app node ID to update in place instead of creating a new node.'),
|
|
360
480
|
title: z.string().optional().describe('Optional canvas node title override'),
|
|
361
481
|
x: z.number().optional().describe('X position (auto-placed if omitted)'),
|
|
362
482
|
y: z.number().optional().describe('Y position (auto-placed if omitted)'),
|
|
363
483
|
width: z.number().optional().describe('Width in pixels (default: 720)'),
|
|
364
484
|
height: z.number().optional().describe('Height in pixels (default: 500)'),
|
|
485
|
+
timeoutMs: z.number().optional().describe('Optional MCP request timeout in milliseconds for Excalidraw cold starts. Client-side MCP hosts may still enforce their own total request timeout.'),
|
|
365
486
|
},
|
|
366
|
-
async (input) => {
|
|
487
|
+
async (input, extra) => {
|
|
367
488
|
const c = await ensureCanvas();
|
|
368
489
|
try {
|
|
369
490
|
const result = await c.addDiagram({
|
|
370
491
|
elements: input.elements,
|
|
492
|
+
...(typeof input.nodeId === 'string' ? { nodeId: input.nodeId } : {}),
|
|
371
493
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
372
494
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
373
495
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
374
496
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
375
497
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
498
|
+
...(typeof input.timeoutMs === 'number' ? { timeoutMs: input.timeoutMs } : {}),
|
|
376
499
|
});
|
|
377
500
|
return {
|
|
378
501
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
379
502
|
};
|
|
380
503
|
} catch (error) {
|
|
504
|
+
if (extra.signal.aborted) {
|
|
505
|
+
return {
|
|
506
|
+
content: [{ type: 'text', text: 'canvas_add_diagram was cancelled by the MCP client before Excalidraw finished. Retry with a higher client request timeout and pass timeoutMs to PMX Canvas for the downstream Excalidraw call.' }],
|
|
507
|
+
isError: true,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
381
510
|
return {
|
|
382
511
|
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
383
512
|
isError: true,
|
|
@@ -534,7 +663,10 @@ export async function startMcpServer(): Promise<void> {
|
|
|
534
663
|
bytes: result.fileSize,
|
|
535
664
|
projectPath: result.projectPath,
|
|
536
665
|
openedInCanvas: result.openedInCanvas,
|
|
666
|
+
startedAt: result.startedAt,
|
|
537
667
|
completedAt: result.completedAt,
|
|
668
|
+
durationMs: result.durationMs,
|
|
669
|
+
timeoutMs: result.timeoutMs,
|
|
538
670
|
// `id` only present when a canvas node was actually created.
|
|
539
671
|
// See the matching block in src/server/server.ts handleCanvasBuildWebArtifact.
|
|
540
672
|
...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
|
|
@@ -700,10 +832,19 @@ export async function startMcpServer(): Promise<void> {
|
|
|
700
832
|
xKey: z.string().optional().describe('Graph x/category key'),
|
|
701
833
|
yKey: z.string().optional().describe('Graph y/value key'),
|
|
702
834
|
chartHeight: z.number().optional().describe('Graph chart content height, distinct from node height'),
|
|
835
|
+
toolName: z.string().optional().describe('Trace node tool or operation label'),
|
|
836
|
+
category: z.string().optional().describe('Trace node category: mcp, file, subagent, or other'),
|
|
837
|
+
status: z.string().optional().describe('Trace node status: running, success, or failed'),
|
|
838
|
+
duration: z.string().optional().describe('Trace node duration badge text'),
|
|
839
|
+
resultSummary: z.string().optional().describe('Trace node result summary'),
|
|
840
|
+
error: z.string().optional().describe('Trace node error message'),
|
|
703
841
|
collapsed: z.boolean().optional().describe('Collapse or expand the node'),
|
|
704
842
|
arrangeLocked: z.boolean().optional().describe('Prevent auto-arrange from moving this node. Pinned nodes are also skipped.'),
|
|
843
|
+
full: z.boolean().optional().describe('Return the full updated node payload. Default false returns compact metadata.'),
|
|
844
|
+
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
705
845
|
},
|
|
706
|
-
async (
|
|
846
|
+
async (input) => {
|
|
847
|
+
const { id, title, content, x, y, width, height, spec, graphType, data, xKey, yKey, chartHeight, collapsed, arrangeLocked, toolName, category, status, duration, resultSummary, error } = input;
|
|
707
848
|
const c = await ensureCanvas();
|
|
708
849
|
const node = await c.getNode(id);
|
|
709
850
|
if (!node) {
|
|
@@ -730,13 +871,19 @@ export async function startMcpServer(): Promise<void> {
|
|
|
730
871
|
if (xKey !== undefined) patch.xKey = xKey;
|
|
731
872
|
if (yKey !== undefined) patch.yKey = yKey;
|
|
732
873
|
if (chartHeight !== undefined) patch.chartHeight = chartHeight;
|
|
874
|
+
if (toolName !== undefined) patch.toolName = toolName;
|
|
875
|
+
if (category !== undefined) patch.category = category;
|
|
876
|
+
if (status !== undefined) patch.status = status;
|
|
877
|
+
if (duration !== undefined) patch.duration = duration;
|
|
878
|
+
if (resultSummary !== undefined) patch.resultSummary = resultSummary;
|
|
879
|
+
if (error !== undefined) patch.error = error;
|
|
733
880
|
if (arrangeLocked !== undefined) {
|
|
734
881
|
patch.arrangeLocked = arrangeLocked;
|
|
735
882
|
}
|
|
736
883
|
await c.updateNode(id, patch);
|
|
737
884
|
const updated = await c.getNode(id);
|
|
738
885
|
return {
|
|
739
|
-
content: [{ type: 'text', text: JSON.stringify(updated ? await createdNodePayload(c, id) : { ok: true, id }, null, 2) }],
|
|
886
|
+
content: [{ type: 'text', text: JSON.stringify(updated ? await createdNodePayload(c, id, input) : { ok: true, id }, null, 2) }],
|
|
740
887
|
};
|
|
741
888
|
},
|
|
742
889
|
);
|
|
@@ -1443,12 +1590,14 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1443
1590
|
width: z.number().optional().describe('Width (auto-computed from children if omitted)'),
|
|
1444
1591
|
height: z.number().optional().describe('Height (auto-computed from children if omitted)'),
|
|
1445
1592
|
childLayout: z.enum(['grid', 'column', 'flow']).optional().describe('Optional child auto-layout. Omit to preserve current child positions.'),
|
|
1593
|
+
full: z.boolean().optional().describe('Return the full created group payload. Default false returns compact metadata.'),
|
|
1594
|
+
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
1446
1595
|
},
|
|
1447
1596
|
async (input) => {
|
|
1448
1597
|
const c = await ensureCanvas();
|
|
1449
1598
|
const id = await c.createGroup(input);
|
|
1450
1599
|
return {
|
|
1451
|
-
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id), null, 2) }],
|
|
1600
|
+
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id, input), null, 2) }],
|
|
1452
1601
|
};
|
|
1453
1602
|
},
|
|
1454
1603
|
);
|
|
@@ -1481,12 +1630,15 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1481
1630
|
assign: z.string().optional().describe('Optional reference name for later operations'),
|
|
1482
1631
|
args: z.record(z.string(), z.unknown()).optional().describe('Operation arguments'),
|
|
1483
1632
|
})).describe('Ordered array of batch operations'),
|
|
1633
|
+
full: z.boolean().optional().describe('Return full batch operation results. Default false compacts node-like payloads.'),
|
|
1634
|
+
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
1484
1635
|
},
|
|
1485
|
-
async (
|
|
1636
|
+
async (input) => {
|
|
1486
1637
|
const c = await ensureCanvas();
|
|
1487
|
-
const result = await c.runBatch(operations);
|
|
1638
|
+
const result = await c.runBatch(input.operations);
|
|
1639
|
+
const payload = wantsFullPayload(input) ? result : compactBatchResult(result);
|
|
1488
1640
|
return {
|
|
1489
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
1641
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1490
1642
|
...(result.ok ? {} : { isError: true }),
|
|
1491
1643
|
};
|
|
1492
1644
|
},
|
|
@@ -1566,12 +1718,33 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1566
1718
|
// ── canvas_list_snapshots ───────────────────────────────────
|
|
1567
1719
|
server.tool(
|
|
1568
1720
|
'canvas_list_snapshots',
|
|
1569
|
-
'List
|
|
1570
|
-
{
|
|
1571
|
-
|
|
1721
|
+
'List saved canvas snapshots with IDs, names, timestamps, and node/edge counts. Defaults to the 20 newest snapshots; pass all=true to return every snapshot.',
|
|
1722
|
+
{
|
|
1723
|
+
limit: z.number().optional().describe('Maximum snapshots to return (default: 20)'),
|
|
1724
|
+
query: z.string().optional().describe('Optional case-insensitive ID/name filter'),
|
|
1725
|
+
all: z.boolean().optional().describe('Return all snapshots instead of the default limit'),
|
|
1726
|
+
},
|
|
1727
|
+
async (input) => {
|
|
1728
|
+
const c = await ensureCanvas();
|
|
1729
|
+
return {
|
|
1730
|
+
content: [{ type: 'text', text: JSON.stringify({ snapshots: await c.listSnapshots(input) }, null, 2) }],
|
|
1731
|
+
};
|
|
1732
|
+
},
|
|
1733
|
+
);
|
|
1734
|
+
|
|
1735
|
+
// ── canvas_gc_snapshots ─────────────────────────────────────
|
|
1736
|
+
server.tool(
|
|
1737
|
+
'canvas_gc_snapshots',
|
|
1738
|
+
'Delete old saved canvas snapshots, keeping the newest N snapshots. Use dryRun=true to preview deletions.',
|
|
1739
|
+
{
|
|
1740
|
+
keep: z.number().optional().describe('Number of newest snapshots to keep (default: 20)'),
|
|
1741
|
+
dryRun: z.boolean().optional().describe('Preview deletions without removing snapshot files'),
|
|
1742
|
+
},
|
|
1743
|
+
async (input) => {
|
|
1572
1744
|
const c = await ensureCanvas();
|
|
1745
|
+
const result = await c.gcSnapshots(input);
|
|
1573
1746
|
return {
|
|
1574
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
1747
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1575
1748
|
};
|
|
1576
1749
|
},
|
|
1577
1750
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CanvasNodeState } from './canvas-state.js';
|
|
2
|
+
import { getCanvasNodeKind } from '../shared/canvas-node-kind.js';
|
|
2
3
|
|
|
3
4
|
const DEFAULT_CONTEXT_TEXT_LENGTH = 700;
|
|
4
5
|
const DEFAULT_WEBPAGE_CONTEXT_TEXT_LENGTH = 1600;
|
|
@@ -6,6 +7,7 @@ const DEFAULT_WEBPAGE_CONTEXT_TEXT_LENGTH = 1600;
|
|
|
6
7
|
export interface AgentContextNode {
|
|
7
8
|
id: string;
|
|
8
9
|
type: CanvasNodeState['type'];
|
|
10
|
+
kind: string;
|
|
9
11
|
title: string | null;
|
|
10
12
|
content: string | null;
|
|
11
13
|
metadata?: Record<string, unknown>;
|
|
@@ -118,6 +120,41 @@ function summarizeMcpAppData(data: Record<string, unknown>, maxLength: number):
|
|
|
118
120
|
return truncateContextText(parts.join('\n'), maxLength);
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
function summarizeWebArtifactData(data: Record<string, unknown>, maxLength: number): string {
|
|
124
|
+
const parts: string[] = [];
|
|
125
|
+
const content = typeof data.content === 'string' ? data.content : '';
|
|
126
|
+
const title = typeof data.title === 'string' ? data.title : '';
|
|
127
|
+
const path = typeof data.path === 'string' ? data.path : '';
|
|
128
|
+
const url = typeof data.url === 'string' ? data.url : '';
|
|
129
|
+
const projectPath = typeof data.projectPath === 'string' ? data.projectPath : '';
|
|
130
|
+
const artifactBytes = typeof data.artifactBytes === 'number' ? data.artifactBytes : null;
|
|
131
|
+
const sourceFileCount = typeof data.sourceFileCount === 'number' ? data.sourceFileCount : null;
|
|
132
|
+
const sourceFiles = Array.isArray(data.sourceFiles)
|
|
133
|
+
? data.sourceFiles.filter((file): file is string => typeof file === 'string')
|
|
134
|
+
: [];
|
|
135
|
+
const deps = Array.isArray(data.deps)
|
|
136
|
+
? data.deps.filter((dep): dep is string => typeof dep === 'string')
|
|
137
|
+
: [];
|
|
138
|
+
|
|
139
|
+
if (content) parts.push(content);
|
|
140
|
+
if (!content && title) parts.push(`Web artifact: ${title}`);
|
|
141
|
+
if (sourceFiles.length > 0 && !content.includes('Source files:')) {
|
|
142
|
+
const remaining = sourceFileCount !== null ? Math.max(0, sourceFileCount - sourceFiles.length) : 0;
|
|
143
|
+
parts.push(`Source files: ${sourceFiles.join(', ')}${remaining > 0 ? `, +${remaining} more` : ''}`);
|
|
144
|
+
}
|
|
145
|
+
if (artifactBytes !== null && !content.includes('Artifact bytes:')) {
|
|
146
|
+
parts.push(`Artifact bytes: ${artifactBytes}`);
|
|
147
|
+
}
|
|
148
|
+
if (deps.length > 0 && !content.includes('Dependencies:')) {
|
|
149
|
+
parts.push(`Dependencies: ${deps.join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
if (path) parts.push(`Path: ${path}`);
|
|
152
|
+
if (projectPath) parts.push(`Project: ${projectPath}`);
|
|
153
|
+
if (url) parts.push(`URL: ${url}`);
|
|
154
|
+
|
|
155
|
+
return parts.length > 0 ? truncateContextText(parts.join('\n'), maxLength) : 'Web artifact node';
|
|
156
|
+
}
|
|
157
|
+
|
|
121
158
|
function metadataForNode(node: CanvasNodeState): Record<string, unknown> | undefined {
|
|
122
159
|
switch (node.type) {
|
|
123
160
|
case 'webpage': {
|
|
@@ -146,9 +183,13 @@ function metadataForNode(node: CanvasNodeState): Record<string, unknown> | undef
|
|
|
146
183
|
}
|
|
147
184
|
case 'mcp-app': {
|
|
148
185
|
const metadata: Record<string, unknown> = {};
|
|
149
|
-
for (const key of ['url', 'path', 'mode', 'hostMode', 'serverName', 'toolName', 'resourceUri', 'sessionStatus']) {
|
|
186
|
+
for (const key of ['url', 'path', 'mode', 'hostMode', 'viewerType', 'serverName', 'toolName', 'resourceUri', 'sessionStatus', 'projectPath', 'artifactBytes', 'sourceFiles', 'sourceFileCount', 'deps']) {
|
|
150
187
|
const value = node.data[key];
|
|
151
|
-
if (value
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
if (value.length > 0) metadata[key] = value;
|
|
190
|
+
} else if (value !== undefined && value !== null && value !== '') {
|
|
191
|
+
metadata[key] = value;
|
|
192
|
+
}
|
|
152
193
|
}
|
|
153
194
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
154
195
|
}
|
|
@@ -170,6 +211,9 @@ export function summarizeNodeForAgentContext(
|
|
|
170
211
|
return truncateContextText(content, defaultTextLength);
|
|
171
212
|
}
|
|
172
213
|
case 'mcp-app': {
|
|
214
|
+
if (node.data.viewerType === 'web-artifact') {
|
|
215
|
+
return summarizeWebArtifactData(node.data, defaultTextLength);
|
|
216
|
+
}
|
|
173
217
|
const chartCfg = node.data.chartConfig as Record<string, unknown> | undefined;
|
|
174
218
|
if (chartCfg) {
|
|
175
219
|
const chartTitle = (chartCfg.title as string) || 'Untitled chart';
|
|
@@ -218,6 +262,7 @@ export function serializeNodeForAgentContext(
|
|
|
218
262
|
return {
|
|
219
263
|
id: node.id,
|
|
220
264
|
type: node.type,
|
|
265
|
+
kind: getCanvasNodeKind(node),
|
|
221
266
|
title: typeof node.data.title === 'string' ? node.data.title : null,
|
|
222
267
|
content: summarizeNodeForAgentContext(node, options) || null,
|
|
223
268
|
...(metadata ? { metadata } : {}),
|
|
@@ -234,7 +279,9 @@ export function buildAgentContextPreamble(
|
|
|
234
279
|
const title = (typeof node.data.title === 'string' && node.data.title) ? node.data.title : node.id;
|
|
235
280
|
const content = summarizeNodeForAgentContext(node, options);
|
|
236
281
|
if (!content) return '';
|
|
237
|
-
|
|
282
|
+
const kind = getCanvasNodeKind(node);
|
|
283
|
+
const typeLabel = kind === node.type ? node.type : `${node.type}/${kind}`;
|
|
284
|
+
return `[Context from "${title}" (${typeLabel})]\n${content}\n`;
|
|
238
285
|
})
|
|
239
286
|
.filter((section) => section.length > 0);
|
|
240
287
|
|
|
@@ -43,6 +43,16 @@ import { buildExcalidrawRestoreCheckpointToolInput, ensureExcalidrawCheckpointId
|
|
|
43
43
|
export type CanvasArrangeMode = 'grid' | 'column' | 'flow';
|
|
44
44
|
export type CanvasPinMode = 'set' | 'add' | 'remove';
|
|
45
45
|
|
|
46
|
+
let canvasLayoutUpdateEmitter: (() => void) | null = null;
|
|
47
|
+
|
|
48
|
+
export function setCanvasLayoutUpdateEmitter(emitter: (() => void) | null): void {
|
|
49
|
+
canvasLayoutUpdateEmitter = emitter;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function emitCanvasLayoutUpdate(): void {
|
|
53
|
+
canvasLayoutUpdateEmitter?.();
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
export interface CanvasFitViewOptions {
|
|
47
57
|
width?: number;
|
|
48
58
|
height?: number;
|
|
@@ -76,6 +86,12 @@ interface CanvasAddNodeInput {
|
|
|
76
86
|
title?: string;
|
|
77
87
|
content?: string;
|
|
78
88
|
data?: Record<string, unknown>;
|
|
89
|
+
toolName?: string;
|
|
90
|
+
category?: string;
|
|
91
|
+
status?: string;
|
|
92
|
+
duration?: string;
|
|
93
|
+
resultSummary?: string;
|
|
94
|
+
error?: string;
|
|
79
95
|
x?: number;
|
|
80
96
|
y?: number;
|
|
81
97
|
width?: number;
|
|
@@ -109,6 +125,7 @@ interface CanvasNodeLookupInput {
|
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
const MAX_CONTEXT_PINS = 20;
|
|
128
|
+
const TRACE_DATA_FIELDS = ['toolName', 'category', 'status', 'duration', 'resultSummary', 'error'] as const;
|
|
112
129
|
|
|
113
130
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
114
131
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -757,10 +774,23 @@ function buildWebpageNodeData(input: CanvasAddNodeInput): Record<string, unknown
|
|
|
757
774
|
};
|
|
758
775
|
}
|
|
759
776
|
|
|
777
|
+
function normalizeTraceNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
|
|
778
|
+
const data: Record<string, unknown> = { ...(input.data ?? {}) };
|
|
779
|
+
for (const field of TRACE_DATA_FIELDS) {
|
|
780
|
+
const value = input[field];
|
|
781
|
+
if (typeof value === 'string') data[field] = value;
|
|
782
|
+
}
|
|
783
|
+
if (input.title) data.title = input.title;
|
|
784
|
+
if (input.content) data.content = input.content;
|
|
785
|
+
if (input.strictSize) data.strictSize = true;
|
|
786
|
+
return data;
|
|
787
|
+
}
|
|
788
|
+
|
|
760
789
|
function buildNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
|
|
761
790
|
if (input.type === 'file') return buildFileNodeData(input);
|
|
762
791
|
if (input.type === 'image') return buildImageNodeData(input);
|
|
763
792
|
if (input.type === 'webpage') return buildWebpageNodeData(input);
|
|
793
|
+
if (input.type === 'trace') return normalizeTraceNodeData(input);
|
|
764
794
|
return {
|
|
765
795
|
...(input.data ?? {}),
|
|
766
796
|
...(input.title ? { title: input.title } : {}),
|
|
@@ -769,6 +799,21 @@ function buildNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
|
|
|
769
799
|
};
|
|
770
800
|
}
|
|
771
801
|
|
|
802
|
+
export function mergeTraceNodeDataFields(
|
|
803
|
+
base: Record<string, unknown>,
|
|
804
|
+
input: Record<string, unknown>,
|
|
805
|
+
): Record<string, unknown> {
|
|
806
|
+
const next: Record<string, unknown> = { ...base };
|
|
807
|
+
for (const field of TRACE_DATA_FIELDS) {
|
|
808
|
+
if (typeof input[field] === 'string') next[field] = input[field];
|
|
809
|
+
}
|
|
810
|
+
return next;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export function hasTraceNodeDataFields(input: Record<string, unknown>): boolean {
|
|
814
|
+
return TRACE_DATA_FIELDS.some((field) => typeof input[field] === 'string');
|
|
815
|
+
}
|
|
816
|
+
|
|
772
817
|
export function scheduleCodeGraphRecompute(onComplete?: () => void): void {
|
|
773
818
|
if (codeGraphTimer) clearTimeout(codeGraphTimer);
|
|
774
819
|
codeGraphTimer = setTimeout(() => {
|
|
@@ -1114,8 +1159,8 @@ export function setCanvasContextPins(
|
|
|
1114
1159
|
};
|
|
1115
1160
|
}
|
|
1116
1161
|
|
|
1117
|
-
export function listCanvasSnapshots(): CanvasSnapshot[] {
|
|
1118
|
-
return canvasState.listSnapshots();
|
|
1162
|
+
export function listCanvasSnapshots(options?: Parameters<typeof canvasState.listSnapshots>[0]): CanvasSnapshot[] {
|
|
1163
|
+
return canvasState.listSnapshots(options);
|
|
1119
1164
|
}
|
|
1120
1165
|
|
|
1121
1166
|
export function saveCanvasSnapshot(name: string): CanvasSnapshot | null {
|
|
@@ -1125,7 +1170,10 @@ export function saveCanvasSnapshot(name: string): CanvasSnapshot | null {
|
|
|
1125
1170
|
export async function restoreCanvasSnapshot(idOrName: string): Promise<{ ok: boolean }> {
|
|
1126
1171
|
const ok = canvasState.restoreSnapshot(idOrName);
|
|
1127
1172
|
if (ok) {
|
|
1128
|
-
|
|
1173
|
+
primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
|
|
1174
|
+
void syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true }).finally(() => {
|
|
1175
|
+
emitCanvasLayoutUpdate();
|
|
1176
|
+
});
|
|
1129
1177
|
canvasState.flushToDisk();
|
|
1130
1178
|
}
|
|
1131
1179
|
return { ok };
|
|
@@ -1135,6 +1183,10 @@ export function deleteCanvasSnapshot(id: string): { ok: boolean } {
|
|
|
1135
1183
|
return { ok: canvasState.deleteSnapshot(id) };
|
|
1136
1184
|
}
|
|
1137
1185
|
|
|
1186
|
+
export function gcCanvasSnapshots(options?: Parameters<typeof canvasState.gcSnapshots>[0]): ReturnType<typeof canvasState.gcSnapshots> {
|
|
1187
|
+
return canvasState.gcSnapshots(options);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1138
1190
|
export function addCanvasEdge(input: {
|
|
1139
1191
|
from?: string;
|
|
1140
1192
|
to?: string;
|
|
@@ -14,6 +14,7 @@ export type CanvasNodeType =
|
|
|
14
14
|
| 'trace'
|
|
15
15
|
| 'file'
|
|
16
16
|
| 'image'
|
|
17
|
+
| 'html'
|
|
17
18
|
| 'group';
|
|
18
19
|
|
|
19
20
|
export type CanvasNodeProvenanceSourceKind =
|
|
@@ -199,7 +200,7 @@ function inferMcpAppProvenance(data: Record<string, unknown>): CanvasNodeProvena
|
|
|
199
200
|
if (resourceUri) details.resourceUri = resourceUri;
|
|
200
201
|
const transportType = isRecord(data.transportConfig) ? pickString(data.transportConfig.type) : null;
|
|
201
202
|
if (transportType) details.transportType = transportType;
|
|
202
|
-
if (isRecord(data.toolInput)) details.toolInput = data.toolInput;
|
|
203
|
+
if (isRecord(data.toolInput) && data.toolInput.__pmxCanvasBlob !== 'v1') details.toolInput = data.toolInput;
|
|
203
204
|
|
|
204
205
|
return {
|
|
205
206
|
sourceKind: 'mcp-tool',
|