pmx-canvas 0.3.0 → 0.3.2
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 +73 -0
- package/dist/canvas/index.js +65 -65
- package/dist/types/cli/agent.d.ts +9 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/server/server.d.ts +8 -0
- package/docs/cli.md +10 -1
- package/docs/http-api.md +28 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-07.md +2 -2
- package/package.json +5 -2
- package/skills/pmx-canvas/SKILL.md +15 -10
- package/skills/pmx-canvas/references/full-reference.md +17 -3
- package/src/cli/agent.ts +1951 -1571
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +80 -323
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +194 -80
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +35 -21
- package/src/mcp/server.ts +132 -70
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +182 -39
- package/src/server/ax-state.ts +142 -47
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +180 -123
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +454 -73
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +150 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +19 -6
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +3 -12
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +320 -217
- package/src/server/intent-registry.ts +2 -5
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +1 -3
- package/src/server/operations/http.ts +2 -3
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +22 -30
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +98 -73
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +41 -15
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +298 -109
- package/src/server/operations/ops/query.ts +46 -28
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +122 -1028
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -19,10 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { z } from 'zod';
|
|
21
21
|
import { canvasState } from '../../canvas-state.js';
|
|
22
|
-
import {
|
|
23
|
-
setCanvasContextPins,
|
|
24
|
-
syncCanvasRuntimeBackends,
|
|
25
|
-
} from '../../canvas-operations.js';
|
|
22
|
+
import { setCanvasContextPins, syncCanvasRuntimeBackends } from '../../canvas-operations.js';
|
|
26
23
|
import { buildCanvasSummary } from '../../canvas-serialization.js';
|
|
27
24
|
import { mutationHistory } from '../../mutation-history.js';
|
|
28
25
|
import { buildSpatialContext, searchNodes } from '../../spatial-analysis.js';
|
|
@@ -36,7 +33,10 @@ const MAX_PINS = 20;
|
|
|
36
33
|
|
|
37
34
|
const pinShape = {
|
|
38
35
|
nodeIds: z.unknown().optional().describe('Array of node IDs to pin'),
|
|
39
|
-
mode: z
|
|
36
|
+
mode: z
|
|
37
|
+
.unknown()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('set: replace all pins, add: add to existing pins, remove: unpin these nodes (default: set)'),
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
const pinSchema = z.looseObject(pinShape);
|
|
@@ -52,20 +52,25 @@ const pinOperation = defineOperation<z.infer<typeof pinSchema>, Record<string, u
|
|
|
52
52
|
},
|
|
53
53
|
mcp: {
|
|
54
54
|
toolName: 'canvas_pin_nodes',
|
|
55
|
-
description:
|
|
55
|
+
description:
|
|
56
|
+
'Pin nodes to include them in the agent context. Pinned nodes appear in the canvas://pinned-context resource. The human can also pin nodes by clicking in the browser.',
|
|
56
57
|
extraShape: {
|
|
57
58
|
nodeIds: z.array(z.string()).describe('Array of node IDs to pin'),
|
|
58
|
-
mode: z
|
|
59
|
+
mode: z
|
|
60
|
+
.enum(['set', 'add', 'remove'])
|
|
61
|
+
.optional()
|
|
59
62
|
.describe('set: replace all pins, add: add to existing pins, remove: unpin these nodes (default: set)'),
|
|
60
63
|
},
|
|
61
64
|
// The wire body is { ok, count } (legacy HTTP shape); the tool reports the
|
|
62
65
|
// resulting pin list, so re-read it from the host. (Legacy RemoteCanvasAccess
|
|
63
66
|
// computed the list client-side; the server state is authoritative now.)
|
|
64
67
|
formatResult: async (_result, _input, host) => ({
|
|
65
|
-
content: [
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: 'text' as const,
|
|
71
|
+
text: JSON.stringify({ ok: true, pinnedNodeIds: await host.getPinnedNodeIds() }),
|
|
72
|
+
},
|
|
73
|
+
],
|
|
69
74
|
}),
|
|
70
75
|
},
|
|
71
76
|
handler: (input, ctx) => {
|
|
@@ -103,7 +108,8 @@ const searchOperation = defineOperation<z.infer<typeof searchSchema>, Record<str
|
|
|
103
108
|
},
|
|
104
109
|
mcp: {
|
|
105
110
|
toolName: 'canvas_search',
|
|
106
|
-
description:
|
|
111
|
+
description:
|
|
112
|
+
'Search for nodes by title or content keywords. Returns matching nodes ranked by relevance with snippets. Much faster than reading the full layout when you need to find specific nodes.',
|
|
107
113
|
extraShape: {
|
|
108
114
|
query: z.string().describe('Search query — matches against node titles, content, and file paths'),
|
|
109
115
|
limit: z.number().optional().describe('Max results to return (default: 10)'),
|
|
@@ -119,14 +125,20 @@ const searchOperation = defineOperation<z.infer<typeof searchSchema>, Record<str
|
|
|
119
125
|
const results = Array.isArray(body.results) ? body.results : [];
|
|
120
126
|
const limit = typeof input.limit === 'number' ? input.limit : 10;
|
|
121
127
|
return {
|
|
122
|
-
content: [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
type: 'text' as const,
|
|
131
|
+
text: JSON.stringify(
|
|
132
|
+
{
|
|
133
|
+
query: input.query,
|
|
134
|
+
resultCount: results.length,
|
|
135
|
+
results: results.slice(0, limit),
|
|
136
|
+
},
|
|
137
|
+
null,
|
|
138
|
+
2,
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
],
|
|
130
142
|
};
|
|
131
143
|
},
|
|
132
144
|
},
|
|
@@ -136,9 +148,12 @@ const searchOperation = defineOperation<z.infer<typeof searchSchema>, Record<str
|
|
|
136
148
|
return { results: [], query: q };
|
|
137
149
|
}
|
|
138
150
|
const rawLimit = input.limit;
|
|
139
|
-
const limit =
|
|
140
|
-
|
|
141
|
-
|
|
151
|
+
const limit =
|
|
152
|
+
typeof rawLimit === 'number'
|
|
153
|
+
? rawLimit
|
|
154
|
+
: typeof rawLimit === 'string' && rawLimit.trim() !== ''
|
|
155
|
+
? Number(rawLimit)
|
|
156
|
+
: Number.NaN;
|
|
142
157
|
const results = searchNodes(canvasState.getLayout().nodes, q);
|
|
143
158
|
return {
|
|
144
159
|
results: Number.isFinite(limit) && limit > 0 ? results.slice(0, Math.floor(limit)) : results,
|
|
@@ -178,10 +193,12 @@ async function formatUndoRedoResult(result: unknown, host: OperationMcpToolHost)
|
|
|
178
193
|
const historyBody = isRecord(history) ? history : {};
|
|
179
194
|
const body = isRecord(result) ? result : {};
|
|
180
195
|
return {
|
|
181
|
-
content: [
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: 'text' as const,
|
|
199
|
+
text: JSON.stringify({ ...body, canUndo: historyBody.canUndo, canRedo: historyBody.canRedo }),
|
|
200
|
+
},
|
|
201
|
+
],
|
|
185
202
|
};
|
|
186
203
|
}
|
|
187
204
|
|
|
@@ -200,7 +217,8 @@ const undoOperation = defineOperation<z.infer<typeof undoRedoSchema>, Record<str
|
|
|
200
217
|
},
|
|
201
218
|
mcp: {
|
|
202
219
|
toolName: 'canvas_undo',
|
|
203
|
-
description:
|
|
220
|
+
description:
|
|
221
|
+
'Undo the last canvas mutation. Returns a description of what was undone. Use this to backtrack when an approach is wrong — explore without fear.',
|
|
204
222
|
formatResult: (result, _input, host) => formatUndoRedoResult(result, host),
|
|
205
223
|
},
|
|
206
224
|
handler: async (_input, ctx) => {
|
|
@@ -69,7 +69,8 @@ const snapshotListOperation = defineOperation<z.infer<typeof snapshotListSchema>
|
|
|
69
69
|
},
|
|
70
70
|
mcp: {
|
|
71
71
|
toolName: 'canvas_list_snapshots',
|
|
72
|
-
description:
|
|
72
|
+
description:
|
|
73
|
+
'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). 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.',
|
|
73
74
|
extraShape: {
|
|
74
75
|
limit: z.number().optional().describe('Maximum snapshots to return (default: 20)'),
|
|
75
76
|
query: z.string().optional().describe('Optional case-insensitive ID/name filter'),
|
|
@@ -84,9 +85,7 @@ const snapshotListOperation = defineOperation<z.infer<typeof snapshotListSchema>
|
|
|
84
85
|
handler: (input) => {
|
|
85
86
|
const body: Record<string, unknown> = input;
|
|
86
87
|
// Legacy precedence: ?q= wins over ?query= even when empty.
|
|
87
|
-
const query = typeof body.q === 'string'
|
|
88
|
-
? body.q
|
|
89
|
-
: typeof body.query === 'string' ? body.query : undefined;
|
|
88
|
+
const query = typeof body.q === 'string' ? body.q : typeof body.query === 'string' ? body.query : undefined;
|
|
90
89
|
const limit = parsePositiveIntegerParam(body.limit);
|
|
91
90
|
return listCanvasSnapshots({
|
|
92
91
|
...(limit !== undefined ? { limit } : {}),
|
|
@@ -117,7 +116,8 @@ const snapshotSaveOperation = defineOperation<z.infer<typeof snapshotSaveSchema>
|
|
|
117
116
|
},
|
|
118
117
|
mcp: {
|
|
119
118
|
toolName: 'canvas_snapshot',
|
|
120
|
-
description:
|
|
119
|
+
description:
|
|
120
|
+
'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Save the current canvas state as a named snapshot. Snapshots persist to disk and can be restored later.',
|
|
121
121
|
extraShape: {
|
|
122
122
|
name: z.string().describe('Name for this snapshot (e.g., "before refactor", "investigation v2")'),
|
|
123
123
|
},
|
|
@@ -154,7 +154,8 @@ const snapshotGcOperation = defineOperation<z.infer<typeof snapshotGcSchema>, Re
|
|
|
154
154
|
},
|
|
155
155
|
mcp: {
|
|
156
156
|
toolName: 'canvas_gc_snapshots',
|
|
157
|
-
description:
|
|
157
|
+
description:
|
|
158
|
+
'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Delete old saved canvas snapshots, keeping the newest N snapshots. Use dryRun=true to preview deletions.',
|
|
158
159
|
extraShape: {
|
|
159
160
|
keep: z.number().optional().describe('Number of newest snapshots to keep (default: 20)'),
|
|
160
161
|
dryRun: z.boolean().optional().describe('Preview deletions without removing snapshot files'),
|
|
@@ -168,11 +169,8 @@ const snapshotGcOperation = defineOperation<z.infer<typeof snapshotGcSchema>, Re
|
|
|
168
169
|
const keepValue = body.keep;
|
|
169
170
|
// Legacy coercion replicated as-is, including the Number('junk') → NaN
|
|
170
171
|
// pass-through (gcCanvasSnapshots falls back to its own default).
|
|
171
|
-
const keep =
|
|
172
|
-
? keepValue
|
|
173
|
-
: typeof keepValue === 'string'
|
|
174
|
-
? Number(keepValue)
|
|
175
|
-
: undefined;
|
|
172
|
+
const keep =
|
|
173
|
+
typeof keepValue === 'number' ? keepValue : typeof keepValue === 'string' ? Number(keepValue) : undefined;
|
|
176
174
|
const dryRun = body.dryRun === true || body['dry-run'] === true;
|
|
177
175
|
return gcCanvasSnapshots({
|
|
178
176
|
...(keep !== undefined ? { keep } : {}),
|
|
@@ -207,7 +205,8 @@ const snapshotDiffOperation = defineOperation<z.infer<typeof snapshotDiffSchema>
|
|
|
207
205
|
},
|
|
208
206
|
mcp: {
|
|
209
207
|
toolName: 'canvas_diff',
|
|
210
|
-
description:
|
|
208
|
+
description:
|
|
209
|
+
'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Compare the current canvas state against a saved snapshot. Shows added/removed/modified nodes and edges. Pass either a snapshot name or ID.',
|
|
211
210
|
extraShape: {
|
|
212
211
|
snapshot: z.string().describe('Snapshot name or ID to compare against'),
|
|
213
212
|
},
|
|
@@ -242,9 +241,7 @@ const snapshotDiffQueryOperation = defineOperation<z.infer<typeof snapshotDiffQu
|
|
|
242
241
|
},
|
|
243
242
|
handler: (input) => {
|
|
244
243
|
// Legacy precedence: ?name= wins over ?id= even when empty.
|
|
245
|
-
const name = typeof input.name === 'string'
|
|
246
|
-
? input.name
|
|
247
|
-
: typeof input.id === 'string' ? input.id : '';
|
|
244
|
+
const name = typeof input.name === 'string' ? input.name : typeof input.id === 'string' ? input.id : '';
|
|
248
245
|
if (!name.trim()) throw new OperationError('Missing snapshot name or id.');
|
|
249
246
|
return diffSnapshotCore(name);
|
|
250
247
|
},
|
|
@@ -273,7 +270,11 @@ function buildSnapshotRestoreSummary(layout: unknown): Record<string, unknown> {
|
|
|
273
270
|
}
|
|
274
271
|
|
|
275
272
|
const snapshotRestoreShape = {
|
|
276
|
-
id: z
|
|
273
|
+
id: z
|
|
274
|
+
.string()
|
|
275
|
+
.optional()
|
|
276
|
+
.catch(undefined)
|
|
277
|
+
.describe('Snapshot ID or name to restore (from canvas_snapshot or snapshot list)'),
|
|
277
278
|
};
|
|
278
279
|
|
|
279
280
|
const snapshotRestoreSchema = z.looseObject(snapshotRestoreShape);
|
|
@@ -289,21 +290,28 @@ const snapshotRestoreOperation = defineOperation<z.infer<typeof snapshotRestoreS
|
|
|
289
290
|
},
|
|
290
291
|
mcp: {
|
|
291
292
|
toolName: 'canvas_restore',
|
|
292
|
-
description:
|
|
293
|
+
description:
|
|
294
|
+
'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Restore the canvas to a previously saved snapshot. Use canvas_snapshot to save first. Pass either the snapshot ID or name to restore.',
|
|
293
295
|
extraShape: {
|
|
294
296
|
id: z.string().describe('Snapshot ID or name to restore (from canvas_snapshot or snapshot list)'),
|
|
295
297
|
},
|
|
296
298
|
formatResult: async (_result, input, host) => {
|
|
297
299
|
const layout = await host.invoker().invoke('layout.get', { includeBlobs: true });
|
|
298
300
|
return {
|
|
299
|
-
content: [
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: 'text' as const,
|
|
304
|
+
text: JSON.stringify(
|
|
305
|
+
{
|
|
306
|
+
ok: true,
|
|
307
|
+
restored: input.id,
|
|
308
|
+
summary: buildSnapshotRestoreSummary(layout),
|
|
309
|
+
},
|
|
310
|
+
null,
|
|
311
|
+
2,
|
|
312
|
+
),
|
|
313
|
+
},
|
|
314
|
+
],
|
|
307
315
|
};
|
|
308
316
|
},
|
|
309
317
|
},
|
|
@@ -338,7 +346,8 @@ const snapshotDeleteOperation = defineOperation<z.infer<typeof snapshotDeleteSch
|
|
|
338
346
|
},
|
|
339
347
|
mcp: {
|
|
340
348
|
toolName: 'canvas_delete_snapshot',
|
|
341
|
-
description:
|
|
349
|
+
description:
|
|
350
|
+
'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Delete a saved snapshot by ID.',
|
|
342
351
|
extraShape: {
|
|
343
352
|
id: z.string().describe('Snapshot ID to delete'),
|
|
344
353
|
},
|
|
@@ -26,7 +26,8 @@ const validateGetOperation = defineOperation<z.infer<typeof validateGetSchema>,
|
|
|
26
26
|
},
|
|
27
27
|
mcp: {
|
|
28
28
|
toolName: 'canvas_validate',
|
|
29
|
-
description:
|
|
29
|
+
description:
|
|
30
|
+
'Validate the current canvas layout. Distinguishes true node collisions from expected group-child containment and reports missing edge endpoints.',
|
|
30
31
|
formatResult: (result) => ({
|
|
31
32
|
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
32
33
|
}),
|
|
@@ -14,11 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import { canvasState } from '../../canvas-state.js';
|
|
17
|
-
import {
|
|
18
|
-
arrangeCanvasNodes,
|
|
19
|
-
clearCanvas,
|
|
20
|
-
fitCanvasView,
|
|
21
|
-
} from '../../canvas-operations.js';
|
|
17
|
+
import { arrangeCanvasNodes, clearCanvas, fitCanvasView } from '../../canvas-operations.js';
|
|
22
18
|
import { validateCanvasLayout } from '../../canvas-validation.js';
|
|
23
19
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
24
20
|
import { closeNodeAppSession, isRecord } from './nodes.js';
|
|
@@ -42,17 +38,20 @@ const arrangeOperation = defineOperation<z.infer<typeof arrangeSchema>, Record<s
|
|
|
42
38
|
},
|
|
43
39
|
mcp: {
|
|
44
40
|
toolName: 'canvas_arrange',
|
|
45
|
-
description:
|
|
41
|
+
description:
|
|
42
|
+
'Auto-arrange all nodes on the canvas. Layouts: grid (default), column (vertical stack), flow (horizontal row).',
|
|
46
43
|
extraShape: {
|
|
47
44
|
layout: z.enum(['grid', 'column', 'flow']).optional().describe('Arrangement layout (default: grid)'),
|
|
48
45
|
},
|
|
49
46
|
// Legacy tool reported { ok: true, layout } regardless of the arrange
|
|
50
47
|
// result (it ignored the arranged count and validation outcome).
|
|
51
48
|
formatResult: (result, input) => ({
|
|
52
|
-
content: [
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text' as const,
|
|
52
|
+
text: JSON.stringify({ ok: true, layout: typeof input.layout === 'string' ? input.layout : 'grid' }),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
56
55
|
}),
|
|
57
56
|
},
|
|
58
57
|
handler: (input) => {
|
|
@@ -93,7 +92,8 @@ const focusOperation = defineOperation<z.infer<typeof focusSchema>, Record<strin
|
|
|
93
92
|
},
|
|
94
93
|
mcp: {
|
|
95
94
|
toolName: 'canvas_focus_node',
|
|
96
|
-
description:
|
|
95
|
+
description:
|
|
96
|
+
"Bring a node into focus. By default the viewport pans so the node is centered. Pass noPan=true to raise/select the node without moving the human's camera (useful when reacting to background events without disrupting the human's current view).",
|
|
97
97
|
extraShape: {
|
|
98
98
|
id: z.string().describe('Node ID to focus on'),
|
|
99
99
|
noPan: z
|
|
@@ -104,10 +104,12 @@ const focusOperation = defineOperation<z.infer<typeof focusSchema>, Record<strin
|
|
|
104
104
|
formatResult: (result) => {
|
|
105
105
|
const body = isRecord(result) ? result : {};
|
|
106
106
|
return {
|
|
107
|
-
content: [
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: 'text' as const,
|
|
110
|
+
text: JSON.stringify({ ok: true, focused: body.focused, panned: body.panned }),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
111
113
|
};
|
|
112
114
|
},
|
|
113
115
|
},
|
|
@@ -155,7 +157,8 @@ const fitOperation = defineOperation<z.infer<typeof fitSchema>, Record<string, u
|
|
|
155
157
|
},
|
|
156
158
|
mcp: {
|
|
157
159
|
toolName: 'canvas_fit_view',
|
|
158
|
-
description:
|
|
160
|
+
description:
|
|
161
|
+
'Fit the canvas viewport to all nodes or a selected subset. Useful before screenshots and whole-board review.',
|
|
159
162
|
extraShape: {
|
|
160
163
|
nodeIds: z.array(z.string()).optional().describe('Optional node IDs to fit instead of the whole canvas'),
|
|
161
164
|
},
|
|
@@ -211,9 +214,50 @@ const clearOperation = defineOperation<z.infer<typeof clearSchema>, Record<strin
|
|
|
211
214
|
},
|
|
212
215
|
});
|
|
213
216
|
|
|
217
|
+
// ── viewport.set (plan-009 C1 slice 3) ────────────────────────
|
|
218
|
+
|
|
219
|
+
const viewportSetShape = {
|
|
220
|
+
x: z.unknown().optional().describe('Viewport x offset'),
|
|
221
|
+
y: z.unknown().optional().describe('Viewport y offset'),
|
|
222
|
+
scale: z.unknown().optional().describe('Viewport zoom scale'),
|
|
223
|
+
recordHistory: z.unknown().optional().describe('Pass false to skip the undo-history entry'),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const viewportSetSchema = z.looseObject(viewportSetShape);
|
|
227
|
+
|
|
228
|
+
const viewportSetOperation = defineOperation<z.infer<typeof viewportSetSchema>, Record<string, unknown>>({
|
|
229
|
+
name: 'viewport.set',
|
|
230
|
+
mutates: false,
|
|
231
|
+
input: viewportSetSchema,
|
|
232
|
+
inputShape: viewportSetShape,
|
|
233
|
+
http: {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
path: '/api/canvas/viewport',
|
|
236
|
+
},
|
|
237
|
+
// Legacy wire: emits canvas-viewport-update (never a layout update).
|
|
238
|
+
handler: (input, ctx) => {
|
|
239
|
+
const body: Record<string, unknown> = input;
|
|
240
|
+
const next = {
|
|
241
|
+
x: typeof body.x === 'number' ? body.x : canvasState.viewport.x,
|
|
242
|
+
y: typeof body.y === 'number' ? body.y : canvasState.viewport.y,
|
|
243
|
+
scale: typeof body.scale === 'number' ? body.scale : canvasState.viewport.scale,
|
|
244
|
+
};
|
|
245
|
+
if (body.recordHistory === false) {
|
|
246
|
+
canvasState.withSuppressedRecording(() => {
|
|
247
|
+
canvasState.setViewport(next);
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
canvasState.setViewport(next);
|
|
251
|
+
}
|
|
252
|
+
ctx.emit('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
253
|
+
return { ok: true };
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
214
257
|
export const viewportOperations: Operation[] = [
|
|
215
258
|
arrangeOperation,
|
|
216
259
|
focusOperation,
|
|
217
260
|
fitOperation,
|
|
218
261
|
clearOperation,
|
|
262
|
+
viewportSetOperation,
|
|
219
263
|
];
|
|
@@ -72,7 +72,11 @@ async function runWebviewTask<T>(task: () => Promise<T> | T): Promise<T> {
|
|
|
72
72
|
} catch (error) {
|
|
73
73
|
const message = error instanceof Error ? error.message : String(error);
|
|
74
74
|
let webview: WebviewStatus | undefined;
|
|
75
|
-
try {
|
|
75
|
+
try {
|
|
76
|
+
webview = getWebviewRunner().status();
|
|
77
|
+
} catch {
|
|
78
|
+
/* runner not wired */
|
|
79
|
+
}
|
|
76
80
|
throw new OperationError(message, 400, webview ? { webview } : undefined);
|
|
77
81
|
}
|
|
78
82
|
}
|
|
@@ -164,7 +168,9 @@ const startOperation = defineOperation<z.infer<typeof startSchema>, WebviewStart
|
|
|
164
168
|
description:
|
|
165
169
|
'Start or replace the headless Bun.WebView automation session for the current PMX Canvas workbench. Use this before screenshot, evaluate, or resize when no automation session is active.',
|
|
166
170
|
extraShape: {
|
|
167
|
-
backend: z
|
|
171
|
+
backend: z
|
|
172
|
+
.enum(['chrome', 'webkit'])
|
|
173
|
+
.optional()
|
|
168
174
|
.describe('Automation backend. Default: webkit on macOS, chrome elsewhere.'),
|
|
169
175
|
width: z.number().optional().describe('Viewport width in pixels (default: 1280)'),
|
|
170
176
|
height: z.number().optional().describe('Viewport height in pixels (default: 800)'),
|
|
@@ -184,14 +190,20 @@ const startOperation = defineOperation<z.infer<typeof startSchema>, WebviewStart
|
|
|
184
190
|
const body = result as { ok?: boolean; webview?: WebviewStatus; error?: string };
|
|
185
191
|
if (body.ok && body.webview) return statusText(body.webview);
|
|
186
192
|
return {
|
|
187
|
-
content: [
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
content: [
|
|
194
|
+
{
|
|
195
|
+
type: 'text' as const,
|
|
196
|
+
text: JSON.stringify(
|
|
197
|
+
{
|
|
198
|
+
ok: false,
|
|
199
|
+
error: body.error ?? 'WebView start failed.',
|
|
200
|
+
...(body.webview ? { webview: body.webview } : {}),
|
|
201
|
+
},
|
|
202
|
+
null,
|
|
203
|
+
2,
|
|
204
|
+
),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
195
207
|
isError: true,
|
|
196
208
|
};
|
|
197
209
|
},
|
|
@@ -228,10 +240,12 @@ const stopOperation = defineOperation<z.infer<typeof stopSchema>, { stopped: boo
|
|
|
228
240
|
formatResult: (result) => {
|
|
229
241
|
const body = result as { ok: boolean; stopped: boolean; webview: WebviewStatus };
|
|
230
242
|
return {
|
|
231
|
-
content: [
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: 'text' as const,
|
|
246
|
+
text: JSON.stringify({ ok: true, stopped: body.stopped, webview: body.webview }, null, 2),
|
|
247
|
+
},
|
|
248
|
+
],
|
|
235
249
|
};
|
|
236
250
|
},
|
|
237
251
|
},
|
|
@@ -262,7 +276,8 @@ const resizeOperation = defineOperation<z.infer<typeof resizeSchema>, WebviewSta
|
|
|
262
276
|
},
|
|
263
277
|
mcp: {
|
|
264
278
|
toolName: 'canvas_resize',
|
|
265
|
-
description:
|
|
279
|
+
description:
|
|
280
|
+
'Resize the active Bun.WebView automation viewport. Requires an active automation session started via canvas_webview_start.',
|
|
266
281
|
extraShape: {
|
|
267
282
|
width: z.number().describe('Viewport width in pixels'),
|
|
268
283
|
height: z.number().describe('Viewport height in pixels'),
|
|
@@ -287,7 +302,12 @@ const resizeOperation = defineOperation<z.infer<typeof resizeSchema>, WebviewSta
|
|
|
287
302
|
|
|
288
303
|
const evaluateShape = {
|
|
289
304
|
expression: z.unknown().optional().describe('JavaScript expression to evaluate in the page context'),
|
|
290
|
-
script: z
|
|
305
|
+
script: z
|
|
306
|
+
.unknown()
|
|
307
|
+
.optional()
|
|
308
|
+
.describe(
|
|
309
|
+
'Multi-statement JavaScript body. The MCP server wraps it in an async IIFE and evaluates the resolved return value.',
|
|
310
|
+
),
|
|
291
311
|
};
|
|
292
312
|
const evaluateSchema = z.looseObject(evaluateShape);
|
|
293
313
|
|
|
@@ -302,10 +322,16 @@ const evaluateOperation = defineOperation<z.infer<typeof evaluateSchema>, { valu
|
|
|
302
322
|
},
|
|
303
323
|
mcp: {
|
|
304
324
|
toolName: 'canvas_evaluate',
|
|
305
|
-
description:
|
|
325
|
+
description:
|
|
326
|
+
'Evaluate JavaScript in the active Bun.WebView automation session for the workbench page. Use this to inspect rendered browser state. Requires an active automation session started via canvas_webview_start.',
|
|
306
327
|
extraShape: {
|
|
307
328
|
expression: z.string().optional().describe('JavaScript expression to evaluate in the page context'),
|
|
308
|
-
script: z
|
|
329
|
+
script: z
|
|
330
|
+
.string()
|
|
331
|
+
.optional()
|
|
332
|
+
.describe(
|
|
333
|
+
'Multi-statement JavaScript body. The MCP server wraps it in an async IIFE and evaluates the resolved return value.',
|
|
334
|
+
),
|
|
309
335
|
},
|
|
310
336
|
// Legacy canvas_evaluate validation: exactly one of expression/script (its
|
|
311
337
|
// own message). Validate here so the MCP tool throws the legacy message
|
|
@@ -95,9 +95,8 @@ function linkedIntentId(rawInput: unknown): string | undefined {
|
|
|
95
95
|
|
|
96
96
|
function allowedIntentKinds(name: string, rawInput: unknown): readonly PmxAxIntentKind[] | undefined {
|
|
97
97
|
if (name === 'jsonrender.stream') {
|
|
98
|
-
const input =
|
|
99
|
-
? rawInput as Record<string, unknown>
|
|
100
|
-
: {};
|
|
98
|
+
const input =
|
|
99
|
+
rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? (rawInput as Record<string, unknown>) : {};
|
|
101
100
|
return typeof input.nodeId === 'string' && input.nodeId.length > 0 ? ['edit'] : ['create'];
|
|
102
101
|
}
|
|
103
102
|
return INTENT_KINDS_BY_OPERATION[name];
|
|
@@ -107,7 +107,9 @@ export interface Operation {
|
|
|
107
107
|
* Any `z.looseObject(...)` satisfies this.
|
|
108
108
|
*/
|
|
109
109
|
export interface OperationInputSchema<I> {
|
|
110
|
-
safeParse(
|
|
110
|
+
safeParse(
|
|
111
|
+
value: unknown,
|
|
112
|
+
):
|
|
111
113
|
| { success: true; data: I }
|
|
112
114
|
| { success: false; error: { issues: Array<{ path: PropertyKey[]; message: string }> } };
|
|
113
115
|
}
|
|
@@ -126,9 +128,7 @@ export interface OperationDefinition<I extends Record<string, unknown>, O> {
|
|
|
126
128
|
serialize?: (output: O) => unknown;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
export function defineOperation<I extends Record<string, unknown>, O>(
|
|
130
|
-
def: OperationDefinition<I, O>,
|
|
131
|
-
): Operation {
|
|
131
|
+
export function defineOperation<I extends Record<string, unknown>, O>(def: OperationDefinition<I, O>): Operation {
|
|
132
132
|
return {
|
|
133
133
|
name: def.name,
|
|
134
134
|
mutates: def.mutates,
|
|
@@ -139,7 +139,9 @@ export function defineOperation<I extends Record<string, unknown>, O>(
|
|
|
139
139
|
const parsed = def.input.safeParse(rawInput ?? {});
|
|
140
140
|
if (!parsed.success) {
|
|
141
141
|
const detail = parsed.error.issues
|
|
142
|
-
.map((issue) =>
|
|
142
|
+
.map((issue) =>
|
|
143
|
+
issue.path.length > 0 ? `${issue.path.map(String).join('.')}: ${issue.message}` : issue.message,
|
|
144
|
+
)
|
|
143
145
|
.join('; ');
|
|
144
146
|
throw new OperationError(`Invalid input for ${def.name}: ${detail}`, 400);
|
|
145
147
|
}
|
|
@@ -69,9 +69,7 @@ export function setWebviewRunner(runner: WebviewRunner | null): void {
|
|
|
69
69
|
|
|
70
70
|
export function getWebviewRunner(): WebviewRunner {
|
|
71
71
|
if (!injectedRunner) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
'Webview runner is not injected. server.ts must call setWebviewRunner(...) at module load.',
|
|
74
|
-
);
|
|
72
|
+
throw new Error('Webview runner is not injected. server.ts must call setWebviewRunner(...) at module load.');
|
|
75
73
|
}
|
|
76
74
|
return injectedRunner;
|
|
77
75
|
}
|
package/src/server/placement.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
findBlocker,
|
|
3
|
-
findOpenCanvasPosition,
|
|
4
|
-
overlapsAny,
|
|
5
|
-
type CanvasPlacementRect,
|
|
6
|
-
} from '../shared/placement.js';
|
|
1
|
+
import { findBlocker, findOpenCanvasPosition, overlapsAny, type CanvasPlacementRect } from '../shared/placement.js';
|
|
7
2
|
|
|
8
3
|
export { findOpenCanvasPosition, type CanvasPlacementRect } from '../shared/placement.js';
|
|
9
4
|
|
|
@@ -30,7 +25,10 @@ export function computeGroupBounds(
|
|
|
30
25
|
): { x: number; y: number; width: number; height: number } | null {
|
|
31
26
|
if (children.length === 0) return null;
|
|
32
27
|
|
|
33
|
-
let minX = Infinity,
|
|
28
|
+
let minX = Infinity,
|
|
29
|
+
minY = Infinity,
|
|
30
|
+
maxX = -Infinity,
|
|
31
|
+
maxY = -Infinity;
|
|
34
32
|
for (const child of children) {
|
|
35
33
|
minX = Math.min(minX, child.position.x);
|
|
36
34
|
minY = Math.min(minY, child.position.y);
|
|
@@ -56,17 +54,12 @@ export function computePackedGroupLayout<T extends CanvasPlacementRect & { id: s
|
|
|
56
54
|
const positions = new Map<string, { x: number; y: number }>();
|
|
57
55
|
if (children.length === 0) return { positions, bounds: null };
|
|
58
56
|
|
|
59
|
-
const sorted = [...children].sort(
|
|
60
|
-
(a, b) => a.position.y - b.position.y || a.position.x - b.position.x,
|
|
61
|
-
);
|
|
57
|
+
const sorted = [...children].sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x);
|
|
62
58
|
const totalArea = sorted.reduce((sum, child) => sum + child.size.width * child.size.height, 0);
|
|
63
59
|
const widestChild = sorted.reduce((max, child) => Math.max(max, child.size.width), 0);
|
|
64
60
|
const targetRowWidth = Math.max(
|
|
65
61
|
widestChild,
|
|
66
|
-
Math.min(
|
|
67
|
-
GROUP_LAYOUT_MAX_ROW_WIDTH,
|
|
68
|
-
Math.max(GROUP_LAYOUT_MIN_ROW_WIDTH, Math.ceil(Math.sqrt(totalArea))),
|
|
69
|
-
),
|
|
62
|
+
Math.min(GROUP_LAYOUT_MAX_ROW_WIDTH, Math.max(GROUP_LAYOUT_MIN_ROW_WIDTH, Math.ceil(Math.sqrt(totalArea)))),
|
|
70
63
|
);
|
|
71
64
|
|
|
72
65
|
const startX = Math.min(...sorted.map((child) => child.position.x));
|
|
@@ -76,10 +69,7 @@ export function computePackedGroupLayout<T extends CanvasPlacementRect & { id: s
|
|
|
76
69
|
let rowHeight = 0;
|
|
77
70
|
|
|
78
71
|
for (const child of sorted) {
|
|
79
|
-
if (
|
|
80
|
-
cursorX > startX &&
|
|
81
|
-
cursorX + child.size.width > startX + targetRowWidth
|
|
82
|
-
) {
|
|
72
|
+
if (cursorX > startX && cursorX + child.size.width > startX + targetRowWidth) {
|
|
83
73
|
cursorX = startX;
|
|
84
74
|
cursorY += rowHeight + GROUP_LAYOUT_GAP_Y;
|
|
85
75
|
rowHeight = 0;
|