pmx-canvas 0.1.14 → 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 +94 -0
- package/Readme.md +108 -1058
- package/dist/canvas/global.css +141 -0
- package/dist/canvas/index.js +129 -79
- package/dist/json-render/index.css +1 -1
- package/dist/types/client/nodes/HtmlNode.d.ts +5 -0
- 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 +4 -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/index.d.ts +13 -3
- 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 +52 -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 +5 -0
- package/src/client/canvas/Minimap.tsx +1 -0
- package/src/client/icons.tsx +1 -0
- package/src/client/nodes/HtmlNode.tsx +151 -0
- 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 +1 -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 +178 -25
- package/src/server/agent-context.ts +50 -3
- package/src/server/canvas-operations.ts +20 -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/index.ts +33 -3
- package/src/server/server.ts +74 -13
- package/src/server/web-artifacts.ts +116 -3
- package/src/shared/canvas-node-kind.ts +14 -0
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -18,6 +18,7 @@ type OpenMcpAppResult = Awaited<ReturnType<PmxCanvas['openMcpApp']>>;
|
|
|
18
18
|
type AddDiagramInput = Parameters<PmxCanvas['addDiagram']>[0];
|
|
19
19
|
type AddJsonRenderNodeInput = Parameters<PmxCanvas['addJsonRenderNode']>[0];
|
|
20
20
|
type AddJsonRenderNodeResult = ReturnType<PmxCanvas['addJsonRenderNode']>;
|
|
21
|
+
type AddHtmlNodeInput = Parameters<PmxCanvas['addHtmlNode']>[0];
|
|
21
22
|
type AddGraphNodeInput = Parameters<PmxCanvas['addGraphNode']>[0];
|
|
22
23
|
type AddGraphNodeResult = ReturnType<PmxCanvas['addGraphNode']>;
|
|
23
24
|
type UpdateNodePatch = Parameters<PmxCanvas['updateNode']>[1];
|
|
@@ -34,8 +35,11 @@ type HistoryResult = ReturnType<PmxCanvas['getHistory']>;
|
|
|
34
35
|
type SetContextPinsResult = ReturnType<PmxCanvas['setContextPins']>;
|
|
35
36
|
type RunBatchInput = Parameters<PmxCanvas['runBatch']>[0];
|
|
36
37
|
type RunBatchResult = Awaited<ReturnType<PmxCanvas['runBatch']>>;
|
|
38
|
+
type SnapshotListOptions = Parameters<PmxCanvas['listSnapshots']>[0];
|
|
37
39
|
type SnapshotList = ReturnType<PmxCanvas['listSnapshots']>;
|
|
38
40
|
type DeleteSnapshotResult = ReturnType<PmxCanvas['deleteSnapshot']>;
|
|
41
|
+
type GcSnapshotsOptions = Parameters<PmxCanvas['gcSnapshots']>[0];
|
|
42
|
+
type GcSnapshotsResult = ReturnType<PmxCanvas['gcSnapshots']>;
|
|
39
43
|
type DiffSnapshotResult = ReturnType<PmxCanvas['diffSnapshot']>;
|
|
40
44
|
type CodeGraphResult = ReturnType<PmxCanvas['getCodeGraph']>;
|
|
41
45
|
type ValidationResult = ReturnType<PmxCanvas['validate']>;
|
|
@@ -97,6 +101,7 @@ export interface CanvasAccess {
|
|
|
97
101
|
openMcpApp(input: OpenMcpAppInput): Promise<OpenMcpAppResult>;
|
|
98
102
|
addDiagram(input: AddDiagramInput): Promise<OpenMcpAppResult>;
|
|
99
103
|
addJsonRenderNode(input: AddJsonRenderNodeInput): Promise<AddJsonRenderNodeResult>;
|
|
104
|
+
addHtmlNode(input: AddHtmlNodeInput): Promise<string>;
|
|
100
105
|
addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult>;
|
|
101
106
|
buildWebArtifact(input: WebArtifactInput): Promise<WebArtifactResult>;
|
|
102
107
|
updateNode(id: string, patch: UpdateNodePatch): Promise<void>;
|
|
@@ -117,10 +122,11 @@ export interface CanvasAccess {
|
|
|
117
122
|
setContextPins(nodeIds: string[], mode?: 'set' | 'add' | 'remove'): Promise<SetContextPinsResult>;
|
|
118
123
|
getPinnedNodeIds(): Promise<string[]>;
|
|
119
124
|
runBatch(operations: RunBatchInput): Promise<RunBatchResult>;
|
|
120
|
-
listSnapshots(): Promise<SnapshotList>;
|
|
125
|
+
listSnapshots(options?: SnapshotListOptions): Promise<SnapshotList>;
|
|
121
126
|
saveSnapshot(name: string): Promise<CanvasSnapshot | null>;
|
|
122
127
|
restoreSnapshot(id: string): Promise<{ ok: boolean }>;
|
|
123
128
|
deleteSnapshot(id: string): Promise<DeleteSnapshotResult>;
|
|
129
|
+
gcSnapshots(options?: GcSnapshotsOptions): Promise<GcSnapshotsResult>;
|
|
124
130
|
diffSnapshot(idOrName: string): Promise<DiffSnapshotResult>;
|
|
125
131
|
getCodeGraph(): Promise<CodeGraphResult>;
|
|
126
132
|
validate(): Promise<ValidationResult>;
|
|
@@ -177,6 +183,10 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
177
183
|
return this.canvas.addJsonRenderNode(input);
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
async addHtmlNode(input: AddHtmlNodeInput): Promise<string> {
|
|
187
|
+
return this.canvas.addHtmlNode(input);
|
|
188
|
+
}
|
|
189
|
+
|
|
180
190
|
async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {
|
|
181
191
|
return this.canvas.addGraphNode(input);
|
|
182
192
|
}
|
|
@@ -257,8 +267,8 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
257
267
|
return await this.canvas.runBatch(operations);
|
|
258
268
|
}
|
|
259
269
|
|
|
260
|
-
async listSnapshots(): Promise<SnapshotList> {
|
|
261
|
-
return this.canvas.listSnapshots();
|
|
270
|
+
async listSnapshots(options?: SnapshotListOptions): Promise<SnapshotList> {
|
|
271
|
+
return this.canvas.listSnapshots(options);
|
|
262
272
|
}
|
|
263
273
|
|
|
264
274
|
async saveSnapshot(name: string): Promise<CanvasSnapshot | null> {
|
|
@@ -273,6 +283,10 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
273
283
|
return this.canvas.deleteSnapshot(id);
|
|
274
284
|
}
|
|
275
285
|
|
|
286
|
+
async gcSnapshots(options?: GcSnapshotsOptions): Promise<GcSnapshotsResult> {
|
|
287
|
+
return this.canvas.gcSnapshots(options);
|
|
288
|
+
}
|
|
289
|
+
|
|
276
290
|
async diffSnapshot(idOrName: string): Promise<DiffSnapshotResult> {
|
|
277
291
|
return this.canvas.diffSnapshot(idOrName);
|
|
278
292
|
}
|
|
@@ -359,11 +373,11 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
359
373
|
}
|
|
360
374
|
|
|
361
375
|
async getLayout(): Promise<CanvasLayout> {
|
|
362
|
-
return await this.requestJson<CanvasLayout>('GET', '/api/canvas/state');
|
|
376
|
+
return await this.requestJson<CanvasLayout>('GET', '/api/canvas/state?includeBlobs=true');
|
|
363
377
|
}
|
|
364
378
|
|
|
365
379
|
async getNode(id: string): Promise<CanvasNodeState | undefined> {
|
|
366
|
-
const response = await fetch(`${this.remoteBaseUrl}/api/canvas/node/${encodeURIComponent(id)}`);
|
|
380
|
+
const response = await fetch(`${this.remoteBaseUrl}/api/canvas/node/${encodeURIComponent(id)}?includeBlobs=true`);
|
|
367
381
|
if (response.status === 404) return undefined;
|
|
368
382
|
const text = await response.text();
|
|
369
383
|
let parsed: unknown = undefined;
|
|
@@ -415,6 +429,10 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
415
429
|
return { id, url: response.url, spec: response.spec };
|
|
416
430
|
}
|
|
417
431
|
|
|
432
|
+
async addHtmlNode(input: AddHtmlNodeInput): Promise<string> {
|
|
433
|
+
return await this.requestNodeId('POST', '/api/canvas/node', { type: 'html', ...input });
|
|
434
|
+
}
|
|
435
|
+
|
|
418
436
|
async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {
|
|
419
437
|
const response = await this.requestJson<GraphNodeResponse>('POST', '/api/canvas/graph', {
|
|
420
438
|
...input,
|
|
@@ -527,8 +545,13 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
527
545
|
return await this.requestJson<RunBatchResult>('POST', '/api/canvas/batch', { operations });
|
|
528
546
|
}
|
|
529
547
|
|
|
530
|
-
async listSnapshots(): Promise<SnapshotList> {
|
|
531
|
-
|
|
548
|
+
async listSnapshots(options?: SnapshotListOptions): Promise<SnapshotList> {
|
|
549
|
+
const params = new URLSearchParams();
|
|
550
|
+
if (typeof options?.limit === 'number') params.set('limit', String(options.limit));
|
|
551
|
+
if (options?.query) params.set('q', options.query);
|
|
552
|
+
if (options?.all) params.set('all', 'true');
|
|
553
|
+
const query = params.size > 0 ? `?${params.toString()}` : '';
|
|
554
|
+
return await this.requestJson<SnapshotList>('GET', `/api/canvas/snapshots${query}`);
|
|
532
555
|
}
|
|
533
556
|
|
|
534
557
|
async saveSnapshot(name: string): Promise<CanvasSnapshot | null> {
|
|
@@ -544,6 +567,10 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
544
567
|
return await this.requestJson<DeleteSnapshotResult>('DELETE', `/api/canvas/snapshots/${encodeURIComponent(id)}`);
|
|
545
568
|
}
|
|
546
569
|
|
|
570
|
+
async gcSnapshots(options?: GcSnapshotsOptions): Promise<GcSnapshotsResult> {
|
|
571
|
+
return await this.requestJson<GcSnapshotsResult>('POST', '/api/canvas/snapshots/gc', options ?? {});
|
|
572
|
+
}
|
|
573
|
+
|
|
547
574
|
async diffSnapshot(idOrName: string): Promise<DiffSnapshotResult> {
|
|
548
575
|
return await this.requestJson<DiffSnapshotResult>('GET', `/api/canvas/snapshots/${encodeURIComponent(idOrName)}/diff`);
|
|
549
576
|
}
|
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)'),
|
|
@@ -267,6 +342,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
267
342
|
duration: z.string().optional().describe('Trace node duration badge text'),
|
|
268
343
|
resultSummary: z.string().optional().describe('Trace node result summary'),
|
|
269
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.'),
|
|
270
347
|
},
|
|
271
348
|
async (input) => {
|
|
272
349
|
const c = await ensureCanvas();
|
|
@@ -297,7 +374,39 @@ export async function startMcpServer(): Promise<void> {
|
|
|
297
374
|
: input;
|
|
298
375
|
const id = await c.addNode(nodeInput);
|
|
299
376
|
return {
|
|
300
|
-
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) }],
|
|
301
410
|
};
|
|
302
411
|
},
|
|
303
412
|
);
|
|
@@ -554,7 +663,10 @@ export async function startMcpServer(): Promise<void> {
|
|
|
554
663
|
bytes: result.fileSize,
|
|
555
664
|
projectPath: result.projectPath,
|
|
556
665
|
openedInCanvas: result.openedInCanvas,
|
|
666
|
+
startedAt: result.startedAt,
|
|
557
667
|
completedAt: result.completedAt,
|
|
668
|
+
durationMs: result.durationMs,
|
|
669
|
+
timeoutMs: result.timeoutMs,
|
|
558
670
|
// `id` only present when a canvas node was actually created.
|
|
559
671
|
// See the matching block in src/server/server.ts handleCanvasBuildWebArtifact.
|
|
560
672
|
...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
|
|
@@ -720,10 +832,19 @@ export async function startMcpServer(): Promise<void> {
|
|
|
720
832
|
xKey: z.string().optional().describe('Graph x/category key'),
|
|
721
833
|
yKey: z.string().optional().describe('Graph y/value key'),
|
|
722
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'),
|
|
723
841
|
collapsed: z.boolean().optional().describe('Collapse or expand the node'),
|
|
724
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.'),
|
|
725
845
|
},
|
|
726
|
-
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;
|
|
727
848
|
const c = await ensureCanvas();
|
|
728
849
|
const node = await c.getNode(id);
|
|
729
850
|
if (!node) {
|
|
@@ -750,13 +871,19 @@ export async function startMcpServer(): Promise<void> {
|
|
|
750
871
|
if (xKey !== undefined) patch.xKey = xKey;
|
|
751
872
|
if (yKey !== undefined) patch.yKey = yKey;
|
|
752
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;
|
|
753
880
|
if (arrangeLocked !== undefined) {
|
|
754
881
|
patch.arrangeLocked = arrangeLocked;
|
|
755
882
|
}
|
|
756
883
|
await c.updateNode(id, patch);
|
|
757
884
|
const updated = await c.getNode(id);
|
|
758
885
|
return {
|
|
759
|
-
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) }],
|
|
760
887
|
};
|
|
761
888
|
},
|
|
762
889
|
);
|
|
@@ -1463,12 +1590,14 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1463
1590
|
width: z.number().optional().describe('Width (auto-computed from children if omitted)'),
|
|
1464
1591
|
height: z.number().optional().describe('Height (auto-computed from children if omitted)'),
|
|
1465
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.'),
|
|
1466
1595
|
},
|
|
1467
1596
|
async (input) => {
|
|
1468
1597
|
const c = await ensureCanvas();
|
|
1469
1598
|
const id = await c.createGroup(input);
|
|
1470
1599
|
return {
|
|
1471
|
-
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) }],
|
|
1472
1601
|
};
|
|
1473
1602
|
},
|
|
1474
1603
|
);
|
|
@@ -1501,12 +1630,15 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1501
1630
|
assign: z.string().optional().describe('Optional reference name for later operations'),
|
|
1502
1631
|
args: z.record(z.string(), z.unknown()).optional().describe('Operation arguments'),
|
|
1503
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.'),
|
|
1504
1635
|
},
|
|
1505
|
-
async (
|
|
1636
|
+
async (input) => {
|
|
1506
1637
|
const c = await ensureCanvas();
|
|
1507
|
-
const result = await c.runBatch(operations);
|
|
1638
|
+
const result = await c.runBatch(input.operations);
|
|
1639
|
+
const payload = wantsFullPayload(input) ? result : compactBatchResult(result);
|
|
1508
1640
|
return {
|
|
1509
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
1641
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1510
1642
|
...(result.ok ? {} : { isError: true }),
|
|
1511
1643
|
};
|
|
1512
1644
|
},
|
|
@@ -1586,12 +1718,33 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1586
1718
|
// ── canvas_list_snapshots ───────────────────────────────────
|
|
1587
1719
|
server.tool(
|
|
1588
1720
|
'canvas_list_snapshots',
|
|
1589
|
-
'List
|
|
1590
|
-
{
|
|
1591
|
-
|
|
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) => {
|
|
1592
1728
|
const c = await ensureCanvas();
|
|
1593
1729
|
return {
|
|
1594
|
-
content: [{ type: 'text', text: JSON.stringify({ snapshots: await c.listSnapshots() }, null, 2) }],
|
|
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) => {
|
|
1744
|
+
const c = await ensureCanvas();
|
|
1745
|
+
const result = await c.gcSnapshots(input);
|
|
1746
|
+
return {
|
|
1747
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1595
1748
|
};
|
|
1596
1749
|
},
|
|
1597
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;
|
|
@@ -1149,8 +1159,8 @@ export function setCanvasContextPins(
|
|
|
1149
1159
|
};
|
|
1150
1160
|
}
|
|
1151
1161
|
|
|
1152
|
-
export function listCanvasSnapshots(): CanvasSnapshot[] {
|
|
1153
|
-
return canvasState.listSnapshots();
|
|
1162
|
+
export function listCanvasSnapshots(options?: Parameters<typeof canvasState.listSnapshots>[0]): CanvasSnapshot[] {
|
|
1163
|
+
return canvasState.listSnapshots(options);
|
|
1154
1164
|
}
|
|
1155
1165
|
|
|
1156
1166
|
export function saveCanvasSnapshot(name: string): CanvasSnapshot | null {
|
|
@@ -1160,7 +1170,10 @@ export function saveCanvasSnapshot(name: string): CanvasSnapshot | null {
|
|
|
1160
1170
|
export async function restoreCanvasSnapshot(idOrName: string): Promise<{ ok: boolean }> {
|
|
1161
1171
|
const ok = canvasState.restoreSnapshot(idOrName);
|
|
1162
1172
|
if (ok) {
|
|
1163
|
-
|
|
1173
|
+
primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
|
|
1174
|
+
void syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true }).finally(() => {
|
|
1175
|
+
emitCanvasLayoutUpdate();
|
|
1176
|
+
});
|
|
1164
1177
|
canvasState.flushToDisk();
|
|
1165
1178
|
}
|
|
1166
1179
|
return { ok };
|
|
@@ -1170,6 +1183,10 @@ export function deleteCanvasSnapshot(id: string): { ok: boolean } {
|
|
|
1170
1183
|
return { ok: canvasState.deleteSnapshot(id) };
|
|
1171
1184
|
}
|
|
1172
1185
|
|
|
1186
|
+
export function gcCanvasSnapshots(options?: Parameters<typeof canvasState.gcSnapshots>[0]): ReturnType<typeof canvasState.gcSnapshots> {
|
|
1187
|
+
return canvasState.gcSnapshots(options);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1173
1190
|
export function addCanvasEdge(input: {
|
|
1174
1191
|
from?: string;
|
|
1175
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',
|