pmx-canvas 0.2.7 → 0.3.1
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 +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -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/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/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- 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/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- 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 +60 -39
- 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 +40 -121
- package/src/mcp/server.ts +206 -271
- 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 +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- 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 +321 -218
- package/src/server/intent-registry.ts +3 -6
- 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 +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- 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 +39 -14
- 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 +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- 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 +121 -1012
- 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/ax-intent.ts +1 -1
- 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
|
@@ -61,9 +61,7 @@ import { AX_SOURCE_SHAPE, AX_SOURCES, axJsonResult, normalizeAxNodeIds, normaliz
|
|
|
61
61
|
|
|
62
62
|
const AX_WORK_ITEM_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
|
|
63
63
|
function normalizeAxWorkItemStatus(value: unknown): PmxAxWorkItemStatus | undefined {
|
|
64
|
-
return typeof value === 'string' && AX_WORK_ITEM_STATUSES.has(value)
|
|
65
|
-
? value as PmxAxWorkItemStatus
|
|
66
|
-
: undefined;
|
|
64
|
+
return typeof value === 'string' && AX_WORK_ITEM_STATUSES.has(value) ? (value as PmxAxWorkItemStatus) : undefined;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
const AX_REVIEW_KINDS = new Set(['comment', 'finding']);
|
|
@@ -72,16 +70,16 @@ const AX_REVIEW_STATUSES = new Set(['open', 'resolved', 'dismissed']);
|
|
|
72
70
|
const AX_REVIEW_ANCHORS = new Set(['node', 'file', 'region']);
|
|
73
71
|
|
|
74
72
|
function normalizeAxReviewKind(value: unknown): PmxAxReviewKind | undefined {
|
|
75
|
-
return typeof value === 'string' && AX_REVIEW_KINDS.has(value) ? value as PmxAxReviewKind : undefined;
|
|
73
|
+
return typeof value === 'string' && AX_REVIEW_KINDS.has(value) ? (value as PmxAxReviewKind) : undefined;
|
|
76
74
|
}
|
|
77
75
|
function normalizeAxReviewSeverity(value: unknown): PmxAxReviewSeverity | undefined {
|
|
78
|
-
return typeof value === 'string' && AX_REVIEW_SEVERITIES.has(value) ? value as PmxAxReviewSeverity : undefined;
|
|
76
|
+
return typeof value === 'string' && AX_REVIEW_SEVERITIES.has(value) ? (value as PmxAxReviewSeverity) : undefined;
|
|
79
77
|
}
|
|
80
78
|
function normalizeAxReviewStatus(value: unknown): PmxAxReviewStatus | undefined {
|
|
81
|
-
return typeof value === 'string' && AX_REVIEW_STATUSES.has(value) ? value as PmxAxReviewStatus : undefined;
|
|
79
|
+
return typeof value === 'string' && AX_REVIEW_STATUSES.has(value) ? (value as PmxAxReviewStatus) : undefined;
|
|
82
80
|
}
|
|
83
81
|
function normalizeAxReviewAnchor(value: unknown): PmxAxReviewAnchorType | undefined {
|
|
84
|
-
return typeof value === 'string' && AX_REVIEW_ANCHORS.has(value) ? value as PmxAxReviewAnchorType : undefined;
|
|
82
|
+
return typeof value === 'string' && AX_REVIEW_ANCHORS.has(value) ? (value as PmxAxReviewAnchorType) : undefined;
|
|
85
83
|
}
|
|
86
84
|
function normalizeAxReviewRegion(value: unknown): PmxAxReviewRegion | undefined {
|
|
87
85
|
if (!isRecord(value)) return undefined;
|
|
@@ -115,10 +113,12 @@ const axWorkCreateOperation = defineOperation<z.infer<typeof axWorkCreateSchema>
|
|
|
115
113
|
},
|
|
116
114
|
mcp: {
|
|
117
115
|
toolName: 'canvas_add_work_item',
|
|
118
|
-
description:
|
|
116
|
+
description:
|
|
117
|
+
'Add a canvas-bound AX work item: a visible task/plan/status tied to nodes and agent work. Work items participate in snapshots and are exposed via canvas://ax-work.',
|
|
119
118
|
extraShape: {
|
|
120
119
|
title: z.string().describe('Short title of the work item.'),
|
|
121
|
-
status: z
|
|
120
|
+
status: z
|
|
121
|
+
.enum(['todo', 'in-progress', 'blocked', 'done', 'cancelled'])
|
|
122
122
|
.optional()
|
|
123
123
|
.describe('Work item status. Defaults to todo.'),
|
|
124
124
|
detail: z.string().optional().describe('Optional longer description.'),
|
|
@@ -135,7 +135,9 @@ const axWorkCreateOperation = defineOperation<z.infer<typeof axWorkCreateSchema>
|
|
|
135
135
|
// Report #56: reject an unknown status (e.g. "in_progress") instead of
|
|
136
136
|
// silently dropping it — the accepted tokens use hyphens.
|
|
137
137
|
if (input.status !== undefined && !normalizeAxWorkItemStatus(input.status)) {
|
|
138
|
-
throw new OperationError(
|
|
138
|
+
throw new OperationError(
|
|
139
|
+
`invalid work item status "${String(input.status)}"; expected one of: todo, in-progress, blocked, done, cancelled.`,
|
|
140
|
+
);
|
|
139
141
|
}
|
|
140
142
|
const status = normalizeAxWorkItemStatus(input.status);
|
|
141
143
|
const workItem = canvasState.addWorkItem(
|
|
@@ -176,13 +178,12 @@ const axWorkUpdateOperation = defineOperation<z.infer<typeof axWorkUpdateSchema>
|
|
|
176
178
|
},
|
|
177
179
|
mcp: {
|
|
178
180
|
toolName: 'canvas_update_work_item',
|
|
179
|
-
description:
|
|
181
|
+
description:
|
|
182
|
+
'Update a canvas-bound AX work item by ID (title/status/detail/nodeIds). Returns null if the work item does not exist.',
|
|
180
183
|
extraShape: {
|
|
181
184
|
id: z.string().describe('Work item ID to update.'),
|
|
182
185
|
title: z.string().optional().describe('New title.'),
|
|
183
|
-
status: z.enum(['todo', 'in-progress', 'blocked', 'done', 'cancelled'])
|
|
184
|
-
.optional()
|
|
185
|
-
.describe('New status.'),
|
|
186
|
+
status: z.enum(['todo', 'in-progress', 'blocked', 'done', 'cancelled']).optional().describe('New status.'),
|
|
186
187
|
detail: z.string().optional().describe('New detail text.'),
|
|
187
188
|
nodeIds: z.array(z.string()).optional().describe('Replacement node IDs.'),
|
|
188
189
|
source: AX_SOURCE_SHAPE,
|
|
@@ -194,7 +195,9 @@ const axWorkUpdateOperation = defineOperation<z.infer<typeof axWorkUpdateSchema>
|
|
|
194
195
|
const id = typeof input.id === 'string' ? input.id : '';
|
|
195
196
|
// Report #56: reject an unknown status instead of returning ok:true + no-op.
|
|
196
197
|
if (input.status !== undefined && !normalizeAxWorkItemStatus(input.status)) {
|
|
197
|
-
throw new OperationError(
|
|
198
|
+
throw new OperationError(
|
|
199
|
+
`invalid work item status "${String(input.status)}"; expected one of: todo, in-progress, blocked, done, cancelled.`,
|
|
200
|
+
);
|
|
198
201
|
}
|
|
199
202
|
const status = normalizeAxWorkItemStatus(input.status);
|
|
200
203
|
const workItem = canvasState.updateWorkItem(
|
|
@@ -240,7 +243,8 @@ const axReviewAddOperation = defineOperation<z.infer<typeof axReviewAddSchema>,
|
|
|
240
243
|
},
|
|
241
244
|
mcp: {
|
|
242
245
|
toolName: 'canvas_add_review_annotation',
|
|
243
|
-
description:
|
|
246
|
+
description:
|
|
247
|
+
'Add a canvas-bound review annotation: a comment or finding anchored to a node, file, or region. Review annotations participate in snapshots and are exposed via canvas://ax-work.',
|
|
244
248
|
extraShape: {
|
|
245
249
|
body: z.string().describe('Annotation body text.'),
|
|
246
250
|
kind: z.enum(['comment', 'finding']).optional().describe('Annotation kind. Default comment.'),
|
|
@@ -248,11 +252,14 @@ const axReviewAddOperation = defineOperation<z.infer<typeof axReviewAddSchema>,
|
|
|
248
252
|
anchorType: z.enum(['node', 'file', 'region']).optional().describe('Anchor type. Default node.'),
|
|
249
253
|
nodeId: z.string().optional().describe('Node ID when anchorType is node.'),
|
|
250
254
|
file: z.string().optional().describe('File path when anchorType is file.'),
|
|
251
|
-
region: z
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
region: z
|
|
256
|
+
.object({
|
|
257
|
+
line: z.number().optional(),
|
|
258
|
+
endLine: z.number().optional(),
|
|
259
|
+
label: z.string().optional(),
|
|
260
|
+
})
|
|
261
|
+
.optional()
|
|
262
|
+
.describe('Region descriptor when anchorType is region.'),
|
|
256
263
|
author: z.string().optional().describe('Optional author label.'),
|
|
257
264
|
source: AX_SOURCE_SHAPE,
|
|
258
265
|
},
|
|
@@ -356,7 +363,8 @@ const axApprovalRequestOperation = defineOperation<z.infer<typeof axApprovalRequ
|
|
|
356
363
|
},
|
|
357
364
|
mcp: {
|
|
358
365
|
toolName: 'canvas_request_approval',
|
|
359
|
-
description:
|
|
366
|
+
description:
|
|
367
|
+
'Request human approval before a high-impact AX action: creates a pending approval gate tied to nodes. Canvas-bound and snapshotted; exposed via canvas://ax-work.',
|
|
360
368
|
extraShape: {
|
|
361
369
|
title: z.string().describe('Short title of what needs approval.'),
|
|
362
370
|
detail: z.string().optional().describe('Optional explanation of the action and its impact.'),
|
|
@@ -407,7 +415,8 @@ const axApprovalResolveOperation = defineOperation<z.infer<typeof axApprovalReso
|
|
|
407
415
|
},
|
|
408
416
|
mcp: {
|
|
409
417
|
toolName: 'canvas_resolve_approval',
|
|
410
|
-
description:
|
|
418
|
+
description:
|
|
419
|
+
'Resolve a pending approval gate by ID with approved or rejected. Returns null if the gate does not exist or is already resolved.',
|
|
411
420
|
extraShape: {
|
|
412
421
|
id: z.string().describe('Approval gate ID to resolve.'),
|
|
413
422
|
decision: z.enum(['approved', 'rejected']).describe('Approval decision.'),
|
|
@@ -422,14 +431,10 @@ const axApprovalResolveOperation = defineOperation<z.infer<typeof axApprovalReso
|
|
|
422
431
|
if (input.decision !== 'approved' && input.decision !== 'rejected') {
|
|
423
432
|
throw new OperationError('resolve requires decision approved or rejected.');
|
|
424
433
|
}
|
|
425
|
-
const approvalGate = canvasState.resolveApproval(
|
|
426
|
-
|
|
427
|
-
input.
|
|
428
|
-
|
|
429
|
-
...(typeof input.resolution === 'string' ? { resolution: input.resolution } : {}),
|
|
430
|
-
source: normalizeAxSource(input.source, 'api'),
|
|
431
|
-
},
|
|
432
|
-
);
|
|
434
|
+
const approvalGate = canvasState.resolveApproval(id, input.decision, {
|
|
435
|
+
...(typeof input.resolution === 'string' ? { resolution: input.resolution } : {}),
|
|
436
|
+
source: normalizeAxSource(input.source, 'api'),
|
|
437
|
+
});
|
|
433
438
|
if (!approvalGate) throw new OperationError('approval gate not found or already resolved.', 404);
|
|
434
439
|
ctx.emit('ax-state-changed', { approvalGate });
|
|
435
440
|
return { ok: true, approvalGate } as unknown as Record<string, unknown>;
|
|
@@ -447,7 +452,10 @@ const axElicitationRequestShape = {
|
|
|
447
452
|
|
|
448
453
|
const axElicitationRequestSchema = z.looseObject(axElicitationRequestShape);
|
|
449
454
|
|
|
450
|
-
const axElicitationRequestOperation = defineOperation<
|
|
455
|
+
const axElicitationRequestOperation = defineOperation<
|
|
456
|
+
z.infer<typeof axElicitationRequestSchema>,
|
|
457
|
+
Record<string, unknown>
|
|
458
|
+
>({
|
|
451
459
|
name: 'ax.elicitation.request',
|
|
452
460
|
mutates: false,
|
|
453
461
|
input: axElicitationRequestSchema,
|
|
@@ -458,7 +466,8 @@ const axElicitationRequestOperation = defineOperation<z.infer<typeof axElicitati
|
|
|
458
466
|
},
|
|
459
467
|
mcp: {
|
|
460
468
|
toolName: 'canvas_request_elicitation',
|
|
461
|
-
description:
|
|
469
|
+
description:
|
|
470
|
+
'Request structured human input (an elicitation): a pending question/form tied to nodes. Canvas-bound and snapshotted; exposed via canvas://ax-work. Answer it with canvas_respond_elicitation.',
|
|
462
471
|
extraShape: {
|
|
463
472
|
prompt: z.string().describe('The question or instruction for the human.'),
|
|
464
473
|
fields: z.array(z.string()).optional().describe('Optional field names to request (a simple structured form).'),
|
|
@@ -475,7 +484,9 @@ const axElicitationRequestOperation = defineOperation<z.infer<typeof axElicitati
|
|
|
475
484
|
const elicitation = canvasState.requestElicitation(
|
|
476
485
|
{
|
|
477
486
|
prompt: input.prompt,
|
|
478
|
-
...(Array.isArray(input.fields)
|
|
487
|
+
...(Array.isArray(input.fields)
|
|
488
|
+
? { fields: input.fields.filter((f): f is string => typeof f === 'string') }
|
|
489
|
+
: {}),
|
|
479
490
|
...(Array.isArray(input.nodeIds) ? { nodeIds: normalizeAxNodeIds(input.nodeIds) } : {}),
|
|
480
491
|
},
|
|
481
492
|
{ source: normalizeAxSource(input.source, 'api') },
|
|
@@ -495,7 +506,10 @@ const axElicitationRespondShape = {
|
|
|
495
506
|
|
|
496
507
|
const axElicitationRespondSchema = z.looseObject(axElicitationRespondShape);
|
|
497
508
|
|
|
498
|
-
const axElicitationRespondOperation = defineOperation<
|
|
509
|
+
const axElicitationRespondOperation = defineOperation<
|
|
510
|
+
z.infer<typeof axElicitationRespondSchema>,
|
|
511
|
+
Record<string, unknown>
|
|
512
|
+
>({
|
|
499
513
|
name: 'ax.elicitation.respond',
|
|
500
514
|
mutates: false,
|
|
501
515
|
input: axElicitationRespondSchema,
|
|
@@ -518,7 +532,9 @@ const axElicitationRespondOperation = defineOperation<z.infer<typeof axElicitati
|
|
|
518
532
|
handler: (input, ctx) => {
|
|
519
533
|
const id = typeof input.id === 'string' ? input.id : '';
|
|
520
534
|
const response = isRecord(input.response) ? input.response : {};
|
|
521
|
-
const elicitation = canvasState.respondElicitation(id, response, {
|
|
535
|
+
const elicitation = canvasState.respondElicitation(id, response, {
|
|
536
|
+
source: normalizeAxSource(input.source, 'api'),
|
|
537
|
+
});
|
|
522
538
|
if (!elicitation) throw new OperationError('elicitation not found or already answered.', 404);
|
|
523
539
|
ctx.emit('ax-state-changed', { elicitation });
|
|
524
540
|
return { ok: true, elicitation } as unknown as Record<string, unknown>;
|
|
@@ -547,7 +563,8 @@ const axModeRequestOperation = defineOperation<z.infer<typeof axModeRequestSchem
|
|
|
547
563
|
},
|
|
548
564
|
mcp: {
|
|
549
565
|
toolName: 'canvas_request_mode',
|
|
550
|
-
description:
|
|
566
|
+
description:
|
|
567
|
+
'Request a workflow mode transition (plan/execute/autonomous): a pending mode request tied to nodes. Canvas-bound and snapshotted; exposed via canvas://ax-work. Resolve with canvas_resolve_mode.',
|
|
551
568
|
extraShape: {
|
|
552
569
|
mode: z.enum(['plan', 'execute', 'autonomous']).describe('Requested target mode.'),
|
|
553
570
|
reason: z.string().optional(),
|
|
@@ -24,10 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { z } from 'zod';
|
|
26
26
|
import { canvasState } from '../../canvas-state.js';
|
|
27
|
-
import {
|
|
28
|
-
serializeCanvasNode,
|
|
29
|
-
serializeCanvasNodeCompact,
|
|
30
|
-
} from '../../canvas-serialization.js';
|
|
27
|
+
import { serializeCanvasNode, serializeCanvasNodeCompact } from '../../canvas-serialization.js';
|
|
31
28
|
import { addCanvasNode } from '../../canvas-operations.js';
|
|
32
29
|
import { executeOperation, runWithSuppressedEmits } from '../registry.js';
|
|
33
30
|
import { defineOperation, type Operation, type OperationContext } from '../types.js';
|
|
@@ -261,12 +258,19 @@ async function runBatch(operations: BatchEntry[]): Promise<BatchEnvelope> {
|
|
|
261
258
|
}
|
|
262
259
|
|
|
263
260
|
const batchShape = {
|
|
264
|
-
operations: z
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
261
|
+
operations: z
|
|
262
|
+
.array(
|
|
263
|
+
z.object({
|
|
264
|
+
op: z.string().describe('Operation name, e.g. "node.add" or "edge.add"'),
|
|
265
|
+
assign: z.string().optional().describe('Optional reference name for later operations'),
|
|
266
|
+
args: z.record(z.string(), z.unknown()).optional().describe('Operation arguments'),
|
|
267
|
+
}),
|
|
268
|
+
)
|
|
269
|
+
.describe('Ordered array of batch operations'),
|
|
270
|
+
full: z
|
|
271
|
+
.boolean()
|
|
272
|
+
.optional()
|
|
273
|
+
.describe('Return full batch operation results. Default false compacts node-like payloads.'),
|
|
270
274
|
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
271
275
|
};
|
|
272
276
|
|
|
@@ -287,7 +291,25 @@ function compactBatchValue(value: unknown): unknown {
|
|
|
287
291
|
const record = value as Record<string, unknown>;
|
|
288
292
|
const nodeLike = typeof record.id === 'string' && typeof record.type === 'string';
|
|
289
293
|
const compact: Record<string, unknown> = {};
|
|
290
|
-
for (const key of [
|
|
294
|
+
for (const key of [
|
|
295
|
+
'ok',
|
|
296
|
+
'id',
|
|
297
|
+
'type',
|
|
298
|
+
'kind',
|
|
299
|
+
'title',
|
|
300
|
+
'content',
|
|
301
|
+
'position',
|
|
302
|
+
'size',
|
|
303
|
+
'fetch',
|
|
304
|
+
'error',
|
|
305
|
+
'from',
|
|
306
|
+
'to',
|
|
307
|
+
'groupId',
|
|
308
|
+
'nodeIds',
|
|
309
|
+
'snapshot',
|
|
310
|
+
'arranged',
|
|
311
|
+
'layout',
|
|
312
|
+
]) {
|
|
291
313
|
if (record[key] !== undefined) compact[key] = record[key];
|
|
292
314
|
}
|
|
293
315
|
if (nodeLike) return compact;
|
|
@@ -325,13 +347,16 @@ const batchOperation = defineOperation<z.infer<typeof batchSchema>, BatchEnvelop
|
|
|
325
347
|
const body = await readJsonValue(req);
|
|
326
348
|
const operations = Array.isArray(body)
|
|
327
349
|
? body
|
|
328
|
-
: isRecord(body) && Array.isArray(body.operations)
|
|
350
|
+
: isRecord(body) && Array.isArray(body.operations)
|
|
351
|
+
? body.operations
|
|
352
|
+
: [];
|
|
329
353
|
return { operations };
|
|
330
354
|
},
|
|
331
355
|
},
|
|
332
356
|
mcp: {
|
|
333
357
|
toolName: 'canvas_batch',
|
|
334
|
-
description:
|
|
358
|
+
description:
|
|
359
|
+
'Run a non-atomic batch of canvas operations with optional assigned references. Use assign to name a result, then reference it later as "$name" for the created node id or "$name.id" for a specific result field. On failure, earlier successful operations remain applied and the response includes ok:false, failedIndex, error, results, and refs. Supports node.add, node.update, node.remove, graph.add, edge.add, group.create, group.add, group.remove, pin.set/add/remove, snapshot.save, and arrange.',
|
|
335
360
|
formatResult: (result, input) => {
|
|
336
361
|
const envelope = result as BatchEnvelope;
|
|
337
362
|
const payload = wantsFullBatch(input) ? envelope : compactBatchResult(envelope);
|
|
@@ -361,5 +386,5 @@ export const batchOperations: Operation[] = [batchOperation];
|
|
|
361
386
|
export async function runCanvasBatchOperation(
|
|
362
387
|
operations: Array<{ op: string; assign?: string; args?: Record<string, unknown> }>,
|
|
363
388
|
): Promise<BatchEnvelope> {
|
|
364
|
-
return await executeOperation('canvas.batch', { operations }) as BatchEnvelope;
|
|
389
|
+
return (await executeOperation('canvas.batch', { operations })) as BatchEnvelope;
|
|
365
390
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas wire ops (plan-009 C1 slice 3): theme.get / theme.set /
|
|
3
|
+
* canvas.apply-updates — the remaining simple browser-client routes
|
|
4
|
+
* (GET+POST /api/canvas/theme, POST /api/canvas/update), wire-identical to
|
|
5
|
+
* the legacy server.ts handlers they replace. HTTP-only: no MCP tools.
|
|
6
|
+
*
|
|
7
|
+
* This module must never import server.ts or index.ts.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { normalizeCanvasTheme } from '../../canvas-db.js';
|
|
11
|
+
import { applyCanvasNodeUpdates } from '../../canvas-operations.js';
|
|
12
|
+
import { canvasState } from '../../canvas-state.js';
|
|
13
|
+
import { defineOperation, type Operation } from '../types.js';
|
|
14
|
+
|
|
15
|
+
const emptyShape = {};
|
|
16
|
+
const emptySchema = z.looseObject(emptyShape);
|
|
17
|
+
|
|
18
|
+
const themeGetOperation = defineOperation<z.infer<typeof emptySchema>, Record<string, unknown>>({
|
|
19
|
+
name: 'theme.get',
|
|
20
|
+
mutates: false,
|
|
21
|
+
input: emptySchema,
|
|
22
|
+
inputShape: emptyShape,
|
|
23
|
+
http: {
|
|
24
|
+
method: 'GET',
|
|
25
|
+
path: '/api/canvas/theme',
|
|
26
|
+
},
|
|
27
|
+
handler: () => ({ ok: true, theme: canvasState.theme }),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const themeSetShape = {
|
|
31
|
+
theme: z.unknown().optional().describe('Theme: dark, light, or high-contrast'),
|
|
32
|
+
};
|
|
33
|
+
const themeSetSchema = z.looseObject(themeSetShape);
|
|
34
|
+
|
|
35
|
+
const themeSetOperation = defineOperation<z.infer<typeof themeSetSchema>, Record<string, unknown>>({
|
|
36
|
+
name: 'theme.set',
|
|
37
|
+
mutates: false,
|
|
38
|
+
input: themeSetSchema,
|
|
39
|
+
inputShape: themeSetShape,
|
|
40
|
+
http: {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
path: '/api/canvas/theme',
|
|
43
|
+
},
|
|
44
|
+
handler: (input, ctx) => {
|
|
45
|
+
const theme = normalizeCanvasTheme(input.theme, canvasState.theme);
|
|
46
|
+
const next = canvasState.setTheme(theme);
|
|
47
|
+
ctx.emit('theme-changed', { theme: next });
|
|
48
|
+
return { ok: true, theme: next };
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const applyUpdatesShape = {
|
|
53
|
+
updates: z.unknown().optional().describe('Array of node updates: { id, position?, size?, … }'),
|
|
54
|
+
recordHistory: z.unknown().optional().describe('Pass false to skip the undo-history entries'),
|
|
55
|
+
};
|
|
56
|
+
const applyUpdatesSchema = z.looseObject(applyUpdatesShape);
|
|
57
|
+
|
|
58
|
+
const applyUpdatesOperation = defineOperation<z.infer<typeof applyUpdatesSchema>, Record<string, unknown>>({
|
|
59
|
+
name: 'canvas.apply-updates',
|
|
60
|
+
mutates: false,
|
|
61
|
+
input: applyUpdatesSchema,
|
|
62
|
+
inputShape: applyUpdatesShape,
|
|
63
|
+
http: {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
path: '/api/canvas/update',
|
|
66
|
+
},
|
|
67
|
+
// Legacy wire: one layout update only when something actually applied.
|
|
68
|
+
handler: (input, ctx) => {
|
|
69
|
+
const body: Record<string, unknown> = input;
|
|
70
|
+
const updates = Array.isArray(body.updates) ? body.updates : [];
|
|
71
|
+
const result =
|
|
72
|
+
body.recordHistory === false
|
|
73
|
+
? (() => {
|
|
74
|
+
let suppressedResult: ReturnType<typeof applyCanvasNodeUpdates> = {
|
|
75
|
+
applied: 0,
|
|
76
|
+
skipped: updates.length,
|
|
77
|
+
};
|
|
78
|
+
canvasState.withSuppressedRecording(() => {
|
|
79
|
+
suppressedResult = applyCanvasNodeUpdates(updates);
|
|
80
|
+
});
|
|
81
|
+
return suppressedResult;
|
|
82
|
+
})()
|
|
83
|
+
: applyCanvasNodeUpdates(updates);
|
|
84
|
+
if (result.applied > 0) {
|
|
85
|
+
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, ...result };
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const canvasWireOperations: Operation[] = [themeGetOperation, themeSetOperation, applyUpdatesOperation];
|
|
@@ -16,11 +16,23 @@ const VALID_EDGE_STYLES = new Set(['solid', 'dashed', 'dotted']);
|
|
|
16
16
|
// ── edge.add ──────────────────────────────────────────────────
|
|
17
17
|
|
|
18
18
|
const edgeAddShape = {
|
|
19
|
-
intentId: z
|
|
19
|
+
intentId: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.catch(undefined)
|
|
23
|
+
.describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
|
|
20
24
|
from: z.string().optional().catch(undefined).describe('Source node ID'),
|
|
21
25
|
to: z.string().optional().catch(undefined).describe('Target node ID'),
|
|
22
|
-
fromSearch: z
|
|
23
|
-
|
|
26
|
+
fromSearch: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.catch(undefined)
|
|
30
|
+
.describe('Resolve the source node by exact or fuzzy title/content search'),
|
|
31
|
+
toSearch: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.catch(undefined)
|
|
35
|
+
.describe('Resolve the target node by exact or fuzzy title/content search'),
|
|
24
36
|
type: z.unknown().optional().describe('Edge type: flow, depends-on, relation, or references'),
|
|
25
37
|
label: z.unknown().optional().describe('Edge label text'),
|
|
26
38
|
style: z.unknown().optional().describe('Optional edge stroke style: solid, dashed, or dotted'),
|
|
@@ -40,7 +52,8 @@ const edgeAddOperation = defineOperation<z.infer<typeof edgeAddSchema>, CanvasEd
|
|
|
40
52
|
},
|
|
41
53
|
mcp: {
|
|
42
54
|
toolName: 'canvas_add_edge',
|
|
43
|
-
description:
|
|
55
|
+
description:
|
|
56
|
+
'Add an edge (connection) between two nodes. Edge types: flow (sequential), depends-on (dependency), relation (general), references (cross-reference).',
|
|
44
57
|
extraShape: {
|
|
45
58
|
type: z.enum(['flow', 'depends-on', 'relation', 'references']).describe('Edge type'),
|
|
46
59
|
label: z.string().optional().describe('Edge label text'),
|
|
@@ -59,18 +72,24 @@ const edgeAddOperation = defineOperation<z.infer<typeof edgeAddSchema>, CanvasEd
|
|
|
59
72
|
formatResult: (result) => {
|
|
60
73
|
const body = isRecord(result) ? result : {};
|
|
61
74
|
return {
|
|
62
|
-
content: [
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text' as const,
|
|
78
|
+
text: JSON.stringify(
|
|
79
|
+
{
|
|
80
|
+
id: body.id,
|
|
81
|
+
from: body.from,
|
|
82
|
+
to: body.to,
|
|
83
|
+
type: body.type,
|
|
84
|
+
label: body.label,
|
|
85
|
+
style: body.style,
|
|
86
|
+
animated: body.animated,
|
|
87
|
+
},
|
|
88
|
+
null,
|
|
89
|
+
2,
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
],
|
|
74
93
|
};
|
|
75
94
|
},
|
|
76
95
|
},
|
|
@@ -78,11 +97,7 @@ const edgeAddOperation = defineOperation<z.infer<typeof edgeAddSchema>, CanvasEd
|
|
|
78
97
|
const body: Record<string, unknown> = input;
|
|
79
98
|
const rawType = body.type;
|
|
80
99
|
const style = typeof body.style === 'string' ? body.style : undefined;
|
|
81
|
-
if (
|
|
82
|
-
!rawType ||
|
|
83
|
-
(!body.from && !body.fromSearch) ||
|
|
84
|
-
(!body.to && !body.toSearch)
|
|
85
|
-
) {
|
|
100
|
+
if (!rawType || (!body.from && !body.fromSearch) || (!body.to && !body.toSearch)) {
|
|
86
101
|
throw new OperationError('Missing required fields: type plus from/fromSearch and to/toSearch.');
|
|
87
102
|
}
|
|
88
103
|
if (typeof rawType !== 'string' || !VALID_EDGE_TYPES.has(rawType)) {
|
|
@@ -161,7 +176,4 @@ const edgeRemoveOperation = defineOperation<z.infer<typeof edgeRemoveSchema>, Re
|
|
|
161
176
|
},
|
|
162
177
|
});
|
|
163
178
|
|
|
164
|
-
export const edgeOperations: Operation[] = [
|
|
165
|
-
edgeAddOperation,
|
|
166
|
-
edgeRemoveOperation,
|
|
167
|
-
];
|
|
179
|
+
export const edgeOperations: Operation[] = [edgeAddOperation, edgeRemoveOperation];
|