pmx-canvas 0.6.4 → 0.6.5
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 +19 -0
- package/Readme.md +7 -1
- package/dist/canvas/global.css +58 -4
- package/dist/canvas/index.js +92 -92
- package/dist/canvas/mermaid-entry.js +258 -258
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/record.d.ts +12 -0
- package/dist/types/client/canvas/Presentation.d.ts +13 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
- package/dist/types/client/state/presentation.d.ts +3 -0
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/json-render/catalog.d.ts +2 -2
- package/dist/types/json-render/server.d.ts +2 -0
- package/dist/types/server/canvas-db.d.ts +2 -0
- package/dist/types/server/canvas-operations.d.ts +22 -0
- package/dist/types/server/canvas-schema.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +6 -0
- package/dist/types/server/canvas-validation.d.ts +3 -0
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/operations/ops/app.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +3 -1
- package/dist/types/server/web-artifacts.d.ts +3 -0
- package/dist/types/shared/tour.d.ts +72 -0
- package/docs/cli.md +89 -0
- package/docs/http-api.md +79 -4
- package/docs/mcp.md +25 -1
- package/docs/node-types.md +80 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +10 -2
- package/skills/pmx-canvas/references/full-reference.md +1 -1
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/src/cli/agent.ts +5 -0
- package/src/cli/commands/nodes.ts +10 -0
- package/src/cli/commands/record.ts +218 -0
- package/src/cli/index.ts +2 -0
- package/src/client/App.tsx +7 -1
- package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
- package/src/client/canvas/CanvasNode.tsx +8 -0
- package/src/client/canvas/IntentLayer.tsx +9 -2
- package/src/client/canvas/Presentation.tsx +108 -0
- package/src/client/canvas/TopBar.tsx +28 -2
- package/src/client/canvas/use-node-drag.ts +8 -3
- package/src/client/mermaid-entry.ts +39 -0
- package/src/client/nodes/HtmlNode.tsx +12 -13
- package/src/client/nodes/McpAppNode.tsx +105 -13
- package/src/client/nodes/MermaidNode.tsx +3 -2
- package/src/client/state/intent-store.ts +5 -1
- package/src/client/state/presentation.ts +7 -0
- package/src/client/state/session-store.ts +3 -0
- package/src/client/state/sse-bridge.ts +3 -0
- package/src/client/theme/global.css +58 -4
- package/src/json-render/server.ts +29 -0
- package/src/server/agent-presence.ts +15 -9
- package/src/server/canvas-db.ts +15 -0
- package/src/server/canvas-operations.ts +115 -22
- package/src/server/canvas-schema.ts +10 -0
- package/src/server/canvas-state.ts +39 -4
- package/src/server/canvas-validation.ts +9 -1
- package/src/server/demo-state.json +11 -7
- package/src/server/html-primitives.ts +199 -91
- package/src/server/html-surface.ts +8 -3
- package/src/server/index.ts +12 -0
- package/src/server/operations/composites.ts +3 -1
- package/src/server/operations/ops/app.ts +10 -0
- package/src/server/operations/ops/batch.ts +12 -3
- package/src/server/operations/ops/groups.ts +14 -7
- package/src/server/operations/ops/json-render.ts +37 -5
- package/src/server/operations/ops/nodes.ts +42 -16
- package/src/server/operations/ops/viewport.ts +32 -0
- package/src/server/operations/registry.ts +6 -0
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +9 -6
- package/src/server/web-artifacts.ts +9 -2
- package/src/server/workboard.ts +3 -3
- package/src/shared/tour.ts +87 -0
package/src/server/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { derivedTour, type Tour } from '../shared/tour.js';
|
|
3
|
+
export type { Tour, TourStop } from '../shared/tour.js';
|
|
2
4
|
import { agentPresence } from './agent-presence.js';
|
|
3
5
|
import { checkFenceTarget, type FenceTarget } from './scope-fence.js';
|
|
4
6
|
import type { AgentPhase, AgentPresence, AgentPresenceSnapshot } from '../shared/agent-presence.js';
|
|
@@ -547,6 +549,16 @@ export class PmxCanvas extends EventEmitter {
|
|
|
547
549
|
);
|
|
548
550
|
}
|
|
549
551
|
|
|
552
|
+
getTour(): Tour {
|
|
553
|
+
return canvasState.getTour() ?? derivedTour(canvasState.getLayout().nodes);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
setTour(tour: Tour | null): void {
|
|
557
|
+
assertInsideFence('tour.set', { boardWide: true });
|
|
558
|
+
canvasState.setTour(tour);
|
|
559
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
560
|
+
}
|
|
561
|
+
|
|
550
562
|
clear(): void {
|
|
551
563
|
assertInsideFence('canvas.clear', { boardWide: true });
|
|
552
564
|
for (const node of canvasState.getLayout().nodes) {
|
|
@@ -203,8 +203,10 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
203
203
|
toolName: 'canvas_view',
|
|
204
204
|
description:
|
|
205
205
|
'Canvas viewport and layout control. Action "arrange" auto-lays-out nodes (grid/columns/etc.); "focus" pans/zooms the viewport to a node; "fit" zooms to fit all nodes in view; "clear" removes every node and edge from the canvas; "remove-annotation" deletes a human-drawn annotation by id.',
|
|
206
|
-
actionSummary: 'arrange | focus | fit | clear | remove-annotation',
|
|
206
|
+
actionSummary: 'arrange | focus | fit | clear | remove-annotation | get-tour | set-tour',
|
|
207
207
|
actions: {
|
|
208
|
+
'get-tour': 'tour.get',
|
|
209
|
+
'set-tour': 'tour.set',
|
|
208
210
|
arrange: 'arrange',
|
|
209
211
|
focus: 'node.focus',
|
|
210
212
|
fit: 'view.fit',
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { z } from 'zod';
|
|
34
34
|
import { canvasState } from '../../canvas-state.js';
|
|
35
|
+
import { describeCreateSize, type CanvasSizeAdjustment } from '../../canvas-operations.js';
|
|
35
36
|
import {
|
|
36
37
|
closeMcpAppSession,
|
|
37
38
|
openMcpApp as openExternalMcpApp,
|
|
@@ -75,6 +76,7 @@ export interface OpenMcpAppCoreResult {
|
|
|
75
76
|
toolCallId: string;
|
|
76
77
|
sessionId: string;
|
|
77
78
|
resourceUri: string;
|
|
79
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
/**
|
|
@@ -121,6 +123,7 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
121
123
|
const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
|
|
122
124
|
? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
|
|
123
125
|
: opened.toolResult;
|
|
126
|
+
const existingNodeIds = new Set(canvasState.getLayout().nodes.map((node) => node.id));
|
|
124
127
|
ctx.emit('ext-app-open', {
|
|
125
128
|
toolCallId,
|
|
126
129
|
nodeId: nodeIdSeed,
|
|
@@ -158,6 +161,11 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
158
161
|
getNode: (id) => canvasState.getNode(id),
|
|
159
162
|
listNodes: () => canvasState.getLayout().nodes,
|
|
160
163
|
});
|
|
164
|
+
const node = nodeId ? canvasState.getNode(nodeId) : undefined;
|
|
165
|
+
const sizeAdjustment =
|
|
166
|
+
node && !existingNodeIds.has(node.id)
|
|
167
|
+
? describeCreateSize(input.width, input.height, node.size.width, node.size.height, node.size)
|
|
168
|
+
: undefined;
|
|
161
169
|
return {
|
|
162
170
|
ok: true,
|
|
163
171
|
...(nodeId ? { id: nodeId } : {}),
|
|
@@ -165,6 +173,7 @@ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationC
|
|
|
165
173
|
toolCallId,
|
|
166
174
|
sessionId: opened.sessionId,
|
|
167
175
|
resourceUri: opened.resourceUri,
|
|
176
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
168
177
|
};
|
|
169
178
|
}
|
|
170
179
|
|
|
@@ -388,6 +397,7 @@ function webArtifactEnvelope(result: WebArtifactCanvasBuildResult, includeLogs:
|
|
|
388
397
|
...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
|
|
389
398
|
nodeId: result.nodeId,
|
|
390
399
|
url: result.url,
|
|
400
|
+
...(result.sizeAdjustment ? { sizeAdjustment: result.sizeAdjustment } : {}),
|
|
391
401
|
metadata: result.metadata,
|
|
392
402
|
logs: result.logs,
|
|
393
403
|
...(includeLogs ? { stdout: result.stdout, stderr: result.stderr } : {}),
|
|
@@ -145,7 +145,11 @@ function createInternalBatchNode(args: Record<string, unknown>): Record<string,
|
|
|
145
145
|
defaultHeight: 200,
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
...serializeCanvasNodeCompact(created.node),
|
|
151
|
+
...(created.sizeAdjustment ? { sizeAdjustment: created.sizeAdjustment } : {}),
|
|
152
|
+
};
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|
|
@@ -165,7 +169,7 @@ function shapeBatchEntry(op: string, result: unknown): Record<string, unknown> {
|
|
|
165
169
|
const id = typeof body.id === 'string' ? body.id : '';
|
|
166
170
|
const node = id ? canvasState.getNode(id) : undefined;
|
|
167
171
|
const extras: Record<string, unknown> = {};
|
|
168
|
-
for (const key of ['fetch', 'error', 'url', 'spec']) {
|
|
172
|
+
for (const key of ['fetch', 'error', 'url', 'spec', 'sizeAdjustment']) {
|
|
169
173
|
if (body[key] !== undefined) extras[key] = body[key];
|
|
170
174
|
}
|
|
171
175
|
if (node) return { ok: true, ...serializeCanvasNodeCompact(node), ...extras };
|
|
@@ -183,7 +187,11 @@ function shapeBatchEntry(op: string, result: unknown): Record<string, unknown> {
|
|
|
183
187
|
if (op === 'group.add') {
|
|
184
188
|
const groupId = typeof body.groupId === 'string' ? body.groupId : '';
|
|
185
189
|
const group = groupId ? canvasState.getNode(groupId) : undefined;
|
|
186
|
-
return {
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
...(group ? serializeCanvasNode(group) : { id: groupId }),
|
|
193
|
+
...(body.sizeAdjustment ? { sizeAdjustment: body.sizeAdjustment } : {}),
|
|
194
|
+
};
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
// pin.set/add/remove: legacy pushed { ok:true, ...{ count, nodeIds } }; wire shape drops nodeIds.
|
|
@@ -305,6 +313,7 @@ function compactBatchValue(value: unknown): unknown {
|
|
|
305
313
|
'content',
|
|
306
314
|
'position',
|
|
307
315
|
'size',
|
|
316
|
+
'sizeAdjustment',
|
|
308
317
|
'fetch',
|
|
309
318
|
'error',
|
|
310
319
|
'from',
|
|
@@ -42,7 +42,7 @@ const groupCreateShape = {
|
|
|
42
42
|
|
|
43
43
|
const groupCreateSchema = z.looseObject(groupCreateShape);
|
|
44
44
|
|
|
45
|
-
const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
45
|
+
const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>, ReturnType<typeof createCanvasGroup>>({
|
|
46
46
|
name: 'group.create',
|
|
47
47
|
mutates: true,
|
|
48
48
|
input: groupCreateSchema,
|
|
@@ -69,7 +69,12 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
69
69
|
formatResult: (result, input) => {
|
|
70
70
|
const body = isRecord(result) ? result : {};
|
|
71
71
|
const node = body.node as CanvasNodeState | undefined;
|
|
72
|
-
const payload = node
|
|
72
|
+
const payload = node
|
|
73
|
+
? {
|
|
74
|
+
...createdNodePayloadFromNode(node, input),
|
|
75
|
+
...(body.sizeAdjustment ? { sizeAdjustment: body.sizeAdjustment } : {}),
|
|
76
|
+
}
|
|
77
|
+
: { ok: true };
|
|
73
78
|
return { content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }] };
|
|
74
79
|
},
|
|
75
80
|
},
|
|
@@ -93,7 +98,7 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
93
98
|
);
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
|
-
|
|
101
|
+
return createCanvasGroup({
|
|
97
102
|
title,
|
|
98
103
|
childIds,
|
|
99
104
|
color,
|
|
@@ -103,9 +108,11 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
103
108
|
height,
|
|
104
109
|
...(childLayout ? { childLayout } : {}),
|
|
105
110
|
});
|
|
106
|
-
return node;
|
|
107
111
|
},
|
|
108
|
-
serialize: (node) =>
|
|
112
|
+
serialize: ({ node, sizeAdjustment }) => ({
|
|
113
|
+
...buildNodeResponse(node),
|
|
114
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
115
|
+
}),
|
|
109
116
|
});
|
|
110
117
|
|
|
111
118
|
// ── group.add ─────────────────────────────────────────────────
|
|
@@ -157,9 +164,9 @@ const groupAddOperation = defineOperation<z.infer<typeof groupAddSchema>, Record
|
|
|
157
164
|
if (!groupId || childIds.length === 0) {
|
|
158
165
|
throw new OperationError('Missing groupId or childIds.');
|
|
159
166
|
}
|
|
160
|
-
const { ok } = groupCanvasNodes(groupId, childIds, childLayout ? { childLayout } : {});
|
|
167
|
+
const { ok, sizeAdjustment } = groupCanvasNodes(groupId, childIds, childLayout ? { childLayout } : {});
|
|
161
168
|
if (!ok) throw new OperationError('Group not found or no valid children.');
|
|
162
|
-
return { ok: true, groupId };
|
|
169
|
+
return { ok: true, groupId, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
|
|
163
170
|
},
|
|
164
171
|
});
|
|
165
172
|
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
createCanvasGraphNode,
|
|
37
37
|
createCanvasJsonRenderNode,
|
|
38
38
|
createCanvasStreamingJsonRenderNode,
|
|
39
|
+
type CanvasSizeAdjustment,
|
|
39
40
|
} from '../../canvas-operations.js';
|
|
40
41
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from '../../canvas-schema.js';
|
|
41
42
|
import { isHtmlPrimitiveKind } from '../../html-primitives.js';
|
|
@@ -84,9 +85,13 @@ function structuredNodeToolResult(result: unknown): { content: Array<{ type: 'te
|
|
|
84
85
|
const body = isRecord(result) ? result : {};
|
|
85
86
|
const node = body.node as CanvasNodeState | undefined;
|
|
86
87
|
const payload = {
|
|
87
|
-
...(node
|
|
88
|
+
...(node
|
|
89
|
+
? createdNodePayloadFromNode(node, {}, body.sizeAdjustment ? { sizeAdjustment: body.sizeAdjustment } : {})
|
|
90
|
+
: { ok: true }),
|
|
88
91
|
url: body.url,
|
|
89
92
|
spec: body.spec,
|
|
93
|
+
...(Array.isArray(body.warnings) ? { warnings: body.warnings } : {}),
|
|
94
|
+
...(body.warning ? { warning: body.warning } : {}),
|
|
90
95
|
};
|
|
91
96
|
return { content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }] };
|
|
92
97
|
}
|
|
@@ -168,7 +173,13 @@ const jsonRenderAddOperation = defineOperation<
|
|
|
168
173
|
throw new OperationError(error instanceof Error ? error.message : String(error));
|
|
169
174
|
}
|
|
170
175
|
},
|
|
171
|
-
serialize: (result) => ({
|
|
176
|
+
serialize: (result) => ({
|
|
177
|
+
...buildNodeResponse(result.node),
|
|
178
|
+
url: result.url,
|
|
179
|
+
spec: result.spec,
|
|
180
|
+
warnings: result.warnings,
|
|
181
|
+
...(result.sizeAdjustment ? { sizeAdjustment: result.sizeAdjustment } : {}),
|
|
182
|
+
}),
|
|
172
183
|
});
|
|
173
184
|
|
|
174
185
|
// ── jsonrender.stream ─────────────────────────────────────────
|
|
@@ -194,6 +205,7 @@ export interface StreamJsonRenderResult {
|
|
|
194
205
|
specVersion: number;
|
|
195
206
|
elementCount: number;
|
|
196
207
|
streamStatus: 'open' | 'closed';
|
|
208
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
197
209
|
}
|
|
198
210
|
|
|
199
211
|
/**
|
|
@@ -206,6 +218,7 @@ export function streamJsonRenderCore(input: StreamJsonRenderInput): StreamJsonRe
|
|
|
206
218
|
const done = input.done === true;
|
|
207
219
|
let nodeId = typeof input.nodeId === 'string' && input.nodeId ? input.nodeId : undefined;
|
|
208
220
|
let url = '';
|
|
221
|
+
let sizeAdjustment: CanvasSizeAdjustment | undefined;
|
|
209
222
|
if (!nodeId) {
|
|
210
223
|
const created = createCanvasStreamingJsonRenderNode({
|
|
211
224
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
@@ -217,11 +230,17 @@ export function streamJsonRenderCore(input: StreamJsonRenderInput): StreamJsonRe
|
|
|
217
230
|
});
|
|
218
231
|
nodeId = created.id;
|
|
219
232
|
url = created.url;
|
|
233
|
+
sizeAdjustment = created.sizeAdjustment;
|
|
220
234
|
}
|
|
221
235
|
const result = appendCanvasJsonRenderStream(nodeId, patches, done);
|
|
222
236
|
if (!result.ok) throw new OperationError(result.error);
|
|
223
237
|
const node = canvasState.getNode(nodeId);
|
|
224
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
id: nodeId,
|
|
240
|
+
url: url || String(node?.data.url ?? ''),
|
|
241
|
+
...result,
|
|
242
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
243
|
+
};
|
|
225
244
|
}
|
|
226
245
|
|
|
227
246
|
const jsonRenderStreamShape = {
|
|
@@ -303,6 +322,7 @@ const jsonRenderStreamOperation = defineOperation<z.infer<typeof jsonRenderStrea
|
|
|
303
322
|
specVersion: body.specVersion,
|
|
304
323
|
elementCount: body.elementCount,
|
|
305
324
|
streamStatus: body.streamStatus,
|
|
325
|
+
...(body.sizeAdjustment ? { sizeAdjustment: body.sizeAdjustment } : {}),
|
|
306
326
|
};
|
|
307
327
|
return { content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }] };
|
|
308
328
|
},
|
|
@@ -558,7 +578,7 @@ const graphAddOperation = defineOperation<z.infer<typeof graphAddSchema>, Return
|
|
|
558
578
|
? body.highlight
|
|
559
579
|
: undefined;
|
|
560
580
|
const sort = body.sort === 'asc' || body.sort === 'desc' || body.sort === 'none' ? body.sort : undefined;
|
|
561
|
-
|
|
581
|
+
const created = createCanvasGraphNode({
|
|
562
582
|
title,
|
|
563
583
|
graphType,
|
|
564
584
|
data,
|
|
@@ -600,12 +620,24 @@ const graphAddOperation = defineOperation<z.infer<typeof graphAddSchema>, Return
|
|
|
600
620
|
...(width !== undefined ? { width } : {}),
|
|
601
621
|
...(nodeHeight !== undefined ? { heightPx: nodeHeight } : {}),
|
|
602
622
|
});
|
|
623
|
+
return {
|
|
624
|
+
...created,
|
|
625
|
+
...(typeof body.height === 'number' && nodeHeight === undefined
|
|
626
|
+
? { warning: '`height` controls graph plot height; use `nodeHeight` to set the node frame height.' }
|
|
627
|
+
: {}),
|
|
628
|
+
};
|
|
603
629
|
} catch (error) {
|
|
604
630
|
if (error instanceof OperationError) throw error;
|
|
605
631
|
throw new OperationError(error instanceof Error ? error.message : String(error));
|
|
606
632
|
}
|
|
607
633
|
},
|
|
608
|
-
serialize: (result) => ({
|
|
634
|
+
serialize: (result) => ({
|
|
635
|
+
...buildNodeResponse(result.node),
|
|
636
|
+
url: result.url,
|
|
637
|
+
spec: result.spec,
|
|
638
|
+
...(result.sizeAdjustment ? { sizeAdjustment: result.sizeAdjustment } : {}),
|
|
639
|
+
...('warning' in result ? { warning: result.warning } : {}),
|
|
640
|
+
}),
|
|
609
641
|
});
|
|
610
642
|
|
|
611
643
|
// ── schema.describe ───────────────────────────────────────────
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
MCP_APP_NODE_DEFAULT_SIZE,
|
|
28
28
|
IMAGE_NODE_DEFAULT_SIZE,
|
|
29
29
|
LEDGER_NODE_DEFAULT_SIZE,
|
|
30
|
+
type CanvasSizeAdjustment,
|
|
30
31
|
} from '../../canvas-operations.js';
|
|
31
32
|
import { normalizeNodeAxCapabilities } from '../../ax-interaction.js';
|
|
32
33
|
import { applyFileContentToNodeData, readFileNodeContent } from '../../file-content.js';
|
|
@@ -377,12 +378,13 @@ export function agentSafeFullLayoutPayload(layout: CanvasLayout): Record<string,
|
|
|
377
378
|
export function createdNodePayloadFromNode(
|
|
378
379
|
node: CanvasNodeState,
|
|
379
380
|
options: Record<string, unknown> = {},
|
|
381
|
+
extras: Record<string, unknown> = {},
|
|
380
382
|
): Record<string, unknown> {
|
|
381
383
|
if (!wantsFullPayload(options)) {
|
|
382
|
-
return { ok: true, node: compactNodePayload(node), id: node.id, nodeId: node.id };
|
|
384
|
+
return { ok: true, node: compactNodePayload(node), id: node.id, nodeId: node.id, ...extras };
|
|
383
385
|
}
|
|
384
386
|
const serialized = serializeCanvasNodeForAgent(node);
|
|
385
|
-
return { ok: true, node: serialized, ...serialized, nodeId: node.id };
|
|
387
|
+
return { ok: true, node: serialized, ...serialized, nodeId: node.id, ...extras };
|
|
386
388
|
}
|
|
387
389
|
|
|
388
390
|
// ── Operation cores (the SDK wraps these directly) ────────────
|
|
@@ -395,7 +397,7 @@ export function createdNodePayloadFromNode(
|
|
|
395
397
|
export function createBasicCanvasNode(
|
|
396
398
|
body: Record<string, unknown>,
|
|
397
399
|
options: { fileMode: 'auto' | 'path' },
|
|
398
|
-
): { node: CanvasNodeState; needsCodeGraphRecompute: boolean } {
|
|
400
|
+
): { node: CanvasNodeState; needsCodeGraphRecompute: boolean; sizeAdjustment?: CanvasSizeAdjustment } {
|
|
399
401
|
const type = typeof body.type === 'string' ? body.type : '';
|
|
400
402
|
const extraData = isRecord(body.data) ? body.data : undefined;
|
|
401
403
|
if (type === 'html') {
|
|
@@ -455,7 +457,7 @@ export function createBasicCanvasNode(
|
|
|
455
457
|
const geometry = resolveCreateGeometry(body);
|
|
456
458
|
const defaults = defaultNodeSize(type);
|
|
457
459
|
try {
|
|
458
|
-
const { node, needsCodeGraphRecompute } = addCanvasNode({
|
|
460
|
+
const { node, needsCodeGraphRecompute, sizeAdjustment } = addCanvasNode({
|
|
459
461
|
type: type as CanvasNodeState['type'],
|
|
460
462
|
...(typeof body.title === 'string'
|
|
461
463
|
? { title: body.title }
|
|
@@ -481,7 +483,7 @@ export function createBasicCanvasNode(
|
|
|
481
483
|
defaultHeight: defaults.height,
|
|
482
484
|
fileMode: options.fileMode,
|
|
483
485
|
});
|
|
484
|
-
return { node, needsCodeGraphRecompute };
|
|
486
|
+
return { node, needsCodeGraphRecompute, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
|
|
485
487
|
} catch (error) {
|
|
486
488
|
if (error instanceof OperationError) throw error;
|
|
487
489
|
throw new OperationError(error instanceof Error ? error.message : String(error));
|
|
@@ -641,15 +643,15 @@ async function createWebpageNode(body: Record<string, unknown>, ctx: OperationCo
|
|
|
641
643
|
|
|
642
644
|
const extraData = isRecord(body.data) ? body.data : undefined;
|
|
643
645
|
const geometry = resolveCreateGeometry(body);
|
|
644
|
-
const { id, node } = addCanvasNode({
|
|
646
|
+
const { id, node, sizeAdjustment } = addCanvasNode({
|
|
645
647
|
type: 'webpage',
|
|
646
648
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
647
649
|
content: normalizedUrl,
|
|
648
650
|
...(extraData ? { data: extraData } : {}),
|
|
649
651
|
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
650
652
|
...geometry,
|
|
651
|
-
|
|
652
|
-
|
|
653
|
+
defaultWidth: WEBPAGE_NODE_DEFAULT_SIZE.width,
|
|
654
|
+
defaultHeight: WEBPAGE_NODE_DEFAULT_SIZE.height,
|
|
653
655
|
});
|
|
654
656
|
|
|
655
657
|
// The node should appear before the (slow) page fetch completes; the
|
|
@@ -662,6 +664,7 @@ async function createWebpageNode(body: Record<string, unknown>, ctx: OperationCo
|
|
|
662
664
|
extras: {
|
|
663
665
|
fetch: refreshed.ok ? { ok: true } : { ok: false, error: refreshed.error ?? 'Failed to fetch webpage content.' },
|
|
664
666
|
...(refreshed.ok ? {} : { error: refreshed.error }),
|
|
667
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
665
668
|
},
|
|
666
669
|
};
|
|
667
670
|
}
|
|
@@ -691,7 +694,7 @@ function createHtmlPrimitiveNode(body: Record<string, unknown>): NodeAddResult {
|
|
|
691
694
|
const axCapabilities =
|
|
692
695
|
normalizeNodeAxCapabilities(body.axCapabilities) ??
|
|
693
696
|
(declared ? { enabled: declared.enabled, allowed: [...declared.allowed] } : undefined);
|
|
694
|
-
const { node } = addCanvasNode({
|
|
697
|
+
const { node, sizeAdjustment } = addCanvasNode({
|
|
695
698
|
type: 'html',
|
|
696
699
|
title: built.title,
|
|
697
700
|
data: {
|
|
@@ -718,6 +721,7 @@ function createHtmlPrimitiveNode(body: Record<string, unknown>): NodeAddResult {
|
|
|
718
721
|
htmlBytes: Buffer.byteLength(built.html, 'utf-8'),
|
|
719
722
|
defaultSize: built.defaultSize,
|
|
720
723
|
},
|
|
724
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
721
725
|
},
|
|
722
726
|
};
|
|
723
727
|
}
|
|
@@ -733,14 +737,14 @@ function createGroupNode(body: Record<string, unknown>): NodeAddResult {
|
|
|
733
737
|
body.childLayout === 'grid' || body.childLayout === 'column' || body.childLayout === 'flow'
|
|
734
738
|
? body.childLayout
|
|
735
739
|
: undefined;
|
|
736
|
-
const { node } = createCanvasGroup({
|
|
740
|
+
const { node, sizeAdjustment } = createCanvasGroup({
|
|
737
741
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
738
742
|
childIds,
|
|
739
743
|
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
740
744
|
...(childLayout ? { childLayout } : {}),
|
|
741
745
|
...geometry,
|
|
742
746
|
});
|
|
743
|
-
return { node };
|
|
747
|
+
return { node, ...(sizeAdjustment ? { extras: { sizeAdjustment } } : {}) };
|
|
744
748
|
}
|
|
745
749
|
|
|
746
750
|
const nodeAddShape = {
|
|
@@ -910,7 +914,8 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
|
|
|
910
914
|
};
|
|
911
915
|
}
|
|
912
916
|
const node = body.node as CanvasNodeState | undefined;
|
|
913
|
-
const
|
|
917
|
+
const extras = body.sizeAdjustment ? { sizeAdjustment: body.sizeAdjustment } : {};
|
|
918
|
+
const payload = node ? createdNodePayloadFromNode(node, input, extras) : { ok: true };
|
|
914
919
|
return { content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }] };
|
|
915
920
|
},
|
|
916
921
|
},
|
|
@@ -954,13 +959,13 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
|
|
|
954
959
|
if (type === 'group') {
|
|
955
960
|
return createGroupNode(body);
|
|
956
961
|
}
|
|
957
|
-
const { node, needsCodeGraphRecompute } = createBasicCanvasNode(body, { fileMode: 'auto' });
|
|
962
|
+
const { node, needsCodeGraphRecompute, sizeAdjustment } = createBasicCanvasNode(body, { fileMode: 'auto' });
|
|
958
963
|
if (needsCodeGraphRecompute) {
|
|
959
964
|
scheduleCodeGraphRecompute(() => {
|
|
960
965
|
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
961
966
|
});
|
|
962
967
|
}
|
|
963
|
-
return { node };
|
|
968
|
+
return { node, ...(sizeAdjustment ? { extras: { sizeAdjustment } } : {}) };
|
|
964
969
|
},
|
|
965
970
|
serialize: ({ node, extras }) => ({ ...buildNodeResponse(node), ...(extras ?? {}) }),
|
|
966
971
|
});
|
|
@@ -1129,7 +1134,10 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
|
|
|
1129
1134
|
const body = isRecord(result) ? result : {};
|
|
1130
1135
|
const node = body.node as CanvasNodeState | undefined;
|
|
1131
1136
|
if (node) {
|
|
1132
|
-
const payload =
|
|
1137
|
+
const payload = {
|
|
1138
|
+
...createdNodePayloadFromNode(node, input),
|
|
1139
|
+
...(body.sizeAdjustment ? { sizeAdjustment: body.sizeAdjustment } : {}),
|
|
1140
|
+
};
|
|
1133
1141
|
return { content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }] };
|
|
1134
1142
|
}
|
|
1135
1143
|
// No `node` field: a webpage-refresh result ({ ok, id, fetch?, error? }) or
|
|
@@ -1224,7 +1232,25 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
|
|
|
1224
1232
|
}
|
|
1225
1233
|
}
|
|
1226
1234
|
const updated = canvasState.getNode(id);
|
|
1227
|
-
|
|
1235
|
+
const fitted =
|
|
1236
|
+
updated &&
|
|
1237
|
+
existing.type === 'group' &&
|
|
1238
|
+
(updated.size.width !== (patch.size?.width ?? existing.size.width) ||
|
|
1239
|
+
updated.size.height !== (patch.size?.height ?? existing.size.height));
|
|
1240
|
+
return updated
|
|
1241
|
+
? {
|
|
1242
|
+
...buildNodeResponse(updated),
|
|
1243
|
+
...(fitted
|
|
1244
|
+
? {
|
|
1245
|
+
sizeAdjustment: {
|
|
1246
|
+
requested: { width: patch.size?.width ?? null, height: patch.size?.height ?? null },
|
|
1247
|
+
applied: updated.size,
|
|
1248
|
+
reason: 'fit-to-children',
|
|
1249
|
+
},
|
|
1250
|
+
}
|
|
1251
|
+
: {}),
|
|
1252
|
+
}
|
|
1253
|
+
: { ok: true, id };
|
|
1228
1254
|
},
|
|
1229
1255
|
});
|
|
1230
1256
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* This module must never import server.ts or index.ts.
|
|
14
14
|
*/
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
+
import { tourSchema, derivedTour } from '../../../shared/tour.js';
|
|
16
17
|
import { canvasState } from '../../canvas-state.js';
|
|
17
18
|
import { arrangeCanvasNodes, clearCanvas, fitCanvasView, setClientViewportSize } from '../../canvas-operations.js';
|
|
18
19
|
import { validateCanvasLayout } from '../../canvas-validation.js';
|
|
@@ -272,7 +273,38 @@ const viewportSetOperation = defineOperation<z.infer<typeof viewportSetSchema>,
|
|
|
272
273
|
},
|
|
273
274
|
});
|
|
274
275
|
|
|
276
|
+
const tourSetShape = { tour: tourSchema.nullable().describe('Ordered camera stops, or null to derive from groups') };
|
|
277
|
+
const tourSetOperation = defineOperation({
|
|
278
|
+
name: 'tour.set',
|
|
279
|
+
mutates: true,
|
|
280
|
+
input: z.object(tourSetShape),
|
|
281
|
+
inputShape: tourSetShape,
|
|
282
|
+
http: { method: 'POST', path: '/api/canvas/tour' },
|
|
283
|
+
mcp: { toolName: 'canvas_set_tour', description: 'Persist an ordered board presentation tour.' },
|
|
284
|
+
handler: (input) => {
|
|
285
|
+
canvasState.setTour(input.tour);
|
|
286
|
+
return { ok: true };
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
const tourGetOperation = defineOperation({
|
|
290
|
+
name: 'tour.get',
|
|
291
|
+
mutates: false,
|
|
292
|
+
input: z.object({}),
|
|
293
|
+
inputShape: {},
|
|
294
|
+
http: { method: 'GET', path: '/api/canvas/tour' },
|
|
295
|
+
mcp: {
|
|
296
|
+
toolName: 'canvas_get_tour',
|
|
297
|
+
description: 'Read the board tour, deriving groups in reading order when absent.',
|
|
298
|
+
},
|
|
299
|
+
handler: () => ({
|
|
300
|
+
tour: canvasState.getTour() ?? derivedTour(canvasState.getLayout().nodes),
|
|
301
|
+
derived: !canvasState.getTour(),
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
|
|
275
305
|
export const viewportOperations: Operation[] = [
|
|
306
|
+
tourSetOperation,
|
|
307
|
+
tourGetOperation,
|
|
276
308
|
arrangeOperation,
|
|
277
309
|
focusOperation,
|
|
278
310
|
fitOperation,
|
|
@@ -296,6 +296,12 @@ const PRESENCE_EXEMPT_OPS = new Set([
|
|
|
296
296
|
'ax.activity.ingest',
|
|
297
297
|
'human.presence.set',
|
|
298
298
|
'human.presence.get',
|
|
299
|
+
// Camera and attention controls are observation/navigation, not edits.
|
|
300
|
+
// They must not create a writer, bump opCount, or refresh an existing one.
|
|
301
|
+
'viewport.set',
|
|
302
|
+
'view.fit',
|
|
303
|
+
'node.focus',
|
|
304
|
+
'ax.focus.set',
|
|
299
305
|
]);
|
|
300
306
|
|
|
301
307
|
/** Layout mutations AND non-GET AX writes (work items, gates, evidence, steering) are agent activity. */
|
|
@@ -111,7 +111,7 @@ function createTarget(input: Record<string, unknown>, extraNodeIds: string[] = [
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** Ops that rewrite the whole board — never allowed under a fence. */
|
|
114
|
-
const BOARD_WIDE_OPS = new Set(['arrange', 'canvas.clear', 'snapshot.restore', 'render.workboard']);
|
|
114
|
+
const BOARD_WIDE_OPS = new Set(['arrange', 'canvas.clear', 'snapshot.restore', 'render.workboard', 'tour.set']);
|
|
115
115
|
|
|
116
116
|
/** Describe what a registry op would write, in fence terms. */
|
|
117
117
|
export function describeOpTarget(op: Operation, rawInput: unknown): FenceTarget {
|
package/src/server/server.ts
CHANGED
|
@@ -143,7 +143,7 @@ agentPresence.setSessionEndListener((presence, startSnapshotId, endedBy) => {
|
|
|
143
143
|
let snapshot = startSnapshotId
|
|
144
144
|
? (canvasState.listSnapshots({ all: true }).find((entry) => entry.id === startSnapshotId) ?? null)
|
|
145
145
|
: null;
|
|
146
|
-
let unchanged =
|
|
146
|
+
let unchanged = !startSnapshotId && canvasState.getLayout().nodes.length === 0;
|
|
147
147
|
// A session that changed NOTHING on the board leaves no snapshot behind —
|
|
148
148
|
// session churn (idle timeouts, receipt tests, attach/detach cycles) was
|
|
149
149
|
// flooding the History drawer with identical boards.
|
|
@@ -184,6 +184,7 @@ agentPresence.setSessionEndListener((presence, startSnapshotId, endedBy) => {
|
|
|
184
184
|
// workbench session and would overwrite it. The label is what the receipt shows.
|
|
185
185
|
emitPrimaryWorkbenchEvent('agent-session-ended', {
|
|
186
186
|
label: presence.label,
|
|
187
|
+
parentAgentId: presence.parentAgentId ?? null,
|
|
187
188
|
endedAt,
|
|
188
189
|
endedBy,
|
|
189
190
|
counts,
|
|
@@ -1431,15 +1432,17 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1431
1432
|
// escaped diagram source, rendered by /canvas/mermaid-entry.js.
|
|
1432
1433
|
const inlineAssets = url.searchParams.get('inline-assets') === '1';
|
|
1433
1434
|
const doc = buildHtmlSurfaceDocument(
|
|
1434
|
-
buildMermaidSurfaceHtml(
|
|
1435
|
+
buildMermaidSurfaceHtml(
|
|
1436
|
+
source,
|
|
1437
|
+
inlineAssets ? mermaidEntryJsInline() : null,
|
|
1438
|
+
node.data.fit === 'none' ? 'none' : 'contain',
|
|
1439
|
+
url.searchParams.get('frameToken') ?? '',
|
|
1440
|
+
),
|
|
1435
1441
|
{
|
|
1436
1442
|
theme,
|
|
1437
1443
|
title: surfaceTitle,
|
|
1438
1444
|
themeToken: url.searchParams.get('themeToken') ?? undefined,
|
|
1439
|
-
//
|
|
1440
|
-
...(url.searchParams.get('frameToken')
|
|
1441
|
-
? { contentHeightToken: url.searchParams.get('frameToken') as string }
|
|
1442
|
-
: {}),
|
|
1445
|
+
// Mermaid reports its natural height itself, before contain scaling.
|
|
1443
1446
|
// Inline the theme tokens so srcdoc-rendered surfaces (Amp orb portals)
|
|
1444
1447
|
// are styled without depending on the <link> subresource.
|
|
1445
1448
|
...(surfaceThemeCssInline() ? { inlineThemeCss: surfaceThemeCssInline() as string } : {}),
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
14
14
|
import { ensureArtifactsDir, getWorkspaceRoot } from './artifact-paths.js';
|
|
15
15
|
import { canvasState, type CanvasNodeState } from './canvas-state.js';
|
|
16
|
-
import { emitCanvasLayoutUpdate } from './canvas-operations.js';
|
|
16
|
+
import { emitCanvasLayoutUpdate, type CanvasSizeAdjustment } from './canvas-operations.js';
|
|
17
17
|
import { findOpenCanvasPosition } from './placement.js';
|
|
18
18
|
|
|
19
19
|
const BUNDLED_WEB_ARTIFACT_SCRIPTS_DIR = join(import.meta.dir, 'web-artifacts', 'scripts');
|
|
@@ -95,12 +95,14 @@ export interface WebArtifactSourceContext {
|
|
|
95
95
|
export interface WebArtifactCanvasOpenResult {
|
|
96
96
|
nodeId: string;
|
|
97
97
|
url: string;
|
|
98
|
+
sizeAdjustment: CanvasSizeAdjustment;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
export interface WebArtifactCanvasBuildResult extends WebArtifactBuildOutput {
|
|
101
102
|
openedInCanvas: boolean;
|
|
102
103
|
nodeId?: string;
|
|
103
104
|
url?: string;
|
|
105
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
104
106
|
startedAt: string;
|
|
105
107
|
completedAt: string;
|
|
106
108
|
durationMs: number;
|
|
@@ -740,7 +742,11 @@ export function openWebArtifactInCanvas(input: {
|
|
|
740
742
|
// This keeps web-artifacts.ts server-independent so the registry op
|
|
741
743
|
// (operations/ops/app.ts) can import it without pulling in server.ts.
|
|
742
744
|
emitCanvasLayoutUpdate();
|
|
743
|
-
return {
|
|
745
|
+
return {
|
|
746
|
+
nodeId: id,
|
|
747
|
+
url,
|
|
748
|
+
sizeAdjustment: { requested: { width: null, height: null }, applied: node.size, reason: 'defaulted' },
|
|
749
|
+
};
|
|
744
750
|
}
|
|
745
751
|
|
|
746
752
|
export async function buildWebArtifactOnCanvas(
|
|
@@ -778,6 +784,7 @@ export async function buildWebArtifactOnCanvas(
|
|
|
778
784
|
openedInCanvas: true,
|
|
779
785
|
nodeId: opened.nodeId,
|
|
780
786
|
url: opened.url,
|
|
787
|
+
sizeAdjustment: opened.sizeAdjustment,
|
|
781
788
|
...timing,
|
|
782
789
|
};
|
|
783
790
|
}
|
package/src/server/workboard.ts
CHANGED
|
@@ -82,14 +82,14 @@ export function buildWorkboardSpec(workItems: PmxAxWorkItem[]): JsonRenderSpec {
|
|
|
82
82
|
}
|
|
83
83
|
elements[columnId] = {
|
|
84
84
|
type: 'Stack',
|
|
85
|
-
props: { direction: 'vertical', gap: 'sm' },
|
|
85
|
+
props: { direction: 'vertical', gap: 'sm', align: 'stretch' },
|
|
86
86
|
children: columnChildren,
|
|
87
87
|
};
|
|
88
88
|
columnIds.push(columnId);
|
|
89
89
|
}
|
|
90
90
|
elements.board = {
|
|
91
|
-
type: '
|
|
92
|
-
props: {
|
|
91
|
+
type: 'Grid',
|
|
92
|
+
props: { columns: columnIds.length, gap: 'md' },
|
|
93
93
|
children: columnIds,
|
|
94
94
|
};
|
|
95
95
|
return { root: 'board', elements };
|