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
|
@@ -15,16 +15,11 @@ import type { PmxAxSource } from '../../ax-state.js';
|
|
|
15
15
|
export const AX_SOURCES = ['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'] as const;
|
|
16
16
|
|
|
17
17
|
/** Zod schema for the optional `source` field shared by AX MCP tool shapes. */
|
|
18
|
-
export const AX_SOURCE_SHAPE = z
|
|
19
|
-
.enum(AX_SOURCES)
|
|
20
|
-
.optional()
|
|
21
|
-
.describe('Optional host/source label. Defaults to mcp.');
|
|
18
|
+
export const AX_SOURCE_SHAPE = z.enum(AX_SOURCES).optional().describe('Optional host/source label. Defaults to mcp.');
|
|
22
19
|
|
|
23
20
|
/** An absent or unrecognized source falls back to the per-surface default. */
|
|
24
21
|
export function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource {
|
|
25
|
-
return (AX_SOURCES as readonly string[]).includes(value as string)
|
|
26
|
-
? (value as PmxAxSource)
|
|
27
|
-
: fallback;
|
|
22
|
+
return (AX_SOURCES as readonly string[]).includes(value as string) ? (value as PmxAxSource) : fallback;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
25
|
export function normalizeAxNodeIds(value: unknown): string[] {
|
|
@@ -52,7 +52,8 @@ const axGetOperation = defineOperation<z.infer<typeof axGetSchema>, Record<strin
|
|
|
52
52
|
},
|
|
53
53
|
mcp: {
|
|
54
54
|
toolName: 'canvas_get_ax',
|
|
55
|
-
description:
|
|
55
|
+
description:
|
|
56
|
+
'Read the host-agnostic PMX AX state and agent-ready AX context. Use this when you need pinned context plus the current focus field.',
|
|
56
57
|
extraShape: {
|
|
57
58
|
includeContext: z.boolean().optional().describe('Include serialized agent-ready AX context. Default true.'),
|
|
58
59
|
},
|
|
@@ -85,7 +86,12 @@ const axGetOperation = defineOperation<z.infer<typeof axGetSchema>, Record<strin
|
|
|
85
86
|
|
|
86
87
|
const axFocusSetShape = {
|
|
87
88
|
nodeIds: z.unknown().optional().describe('Node IDs to place in the AX focus field. Missing nodes are ignored.'),
|
|
88
|
-
source: z
|
|
89
|
+
source: z
|
|
90
|
+
.unknown()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe(
|
|
93
|
+
'Optional host/source label for adapter-originated focus. Defaults to mcp. Use codex from the Codex app adapter.',
|
|
94
|
+
),
|
|
89
95
|
};
|
|
90
96
|
|
|
91
97
|
const axFocusSetSchema = z.looseObject(axFocusSetShape);
|
|
@@ -101,12 +107,16 @@ const axFocusSetOperation = defineOperation<z.infer<typeof axFocusSetSchema>, Re
|
|
|
101
107
|
},
|
|
102
108
|
mcp: {
|
|
103
109
|
toolName: 'canvas_set_ax_focus',
|
|
104
|
-
description:
|
|
110
|
+
description:
|
|
111
|
+
'Set the PMX AX focus field without requiring viewport movement. Focus is persisted and available through canvas://ax-context.',
|
|
105
112
|
extraShape: {
|
|
106
113
|
nodeIds: z.array(z.string()).describe('Node IDs to place in the AX focus field. Missing nodes are ignored.'),
|
|
107
|
-
source: z
|
|
114
|
+
source: z
|
|
115
|
+
.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
|
|
108
116
|
.optional()
|
|
109
|
-
.describe(
|
|
117
|
+
.describe(
|
|
118
|
+
'Optional host/source label for adapter-originated focus. Defaults to mcp. Use codex from the Codex app adapter.',
|
|
119
|
+
),
|
|
110
120
|
},
|
|
111
121
|
buildInput: (input) => ({ ...input, source: normalizeAxSource(input.source, 'mcp') }),
|
|
112
122
|
formatResult: (result) => {
|
|
@@ -146,13 +156,16 @@ const axPolicySetOperation = defineOperation<z.infer<typeof axPolicySetSchema>,
|
|
|
146
156
|
},
|
|
147
157
|
mcp: {
|
|
148
158
|
toolName: 'canvas_set_ax_policy',
|
|
149
|
-
description:
|
|
159
|
+
description:
|
|
160
|
+
'Set the declarative AX policy (allowed/excluded/approval-required tools; prompt mode/append). PMX stores it and exposes it via canvas://ax-context; host adapters READ and enforce it. Merges with the existing policy.',
|
|
150
161
|
extraShape: {
|
|
151
|
-
tools: z
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
162
|
+
tools: z
|
|
163
|
+
.object({
|
|
164
|
+
allowed: z.array(z.string()).optional(),
|
|
165
|
+
excluded: z.array(z.string()).optional(),
|
|
166
|
+
approvalRequired: z.array(z.string()).optional(),
|
|
167
|
+
})
|
|
168
|
+
.optional(),
|
|
156
169
|
prompt: z.object({ systemAppend: z.string().optional(), mode: z.string().optional() }).optional(),
|
|
157
170
|
source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
|
|
158
171
|
},
|
|
@@ -196,7 +209,10 @@ const axHostCapabilityReportShape = {
|
|
|
196
209
|
|
|
197
210
|
const axHostCapabilityReportSchema = z.looseObject(axHostCapabilityReportShape);
|
|
198
211
|
|
|
199
|
-
const axHostCapabilityReportOperation = defineOperation<
|
|
212
|
+
const axHostCapabilityReportOperation = defineOperation<
|
|
213
|
+
z.infer<typeof axHostCapabilityReportSchema>,
|
|
214
|
+
Record<string, unknown>
|
|
215
|
+
>({
|
|
200
216
|
name: 'ax.host-capability.report',
|
|
201
217
|
mutates: false,
|
|
202
218
|
input: axHostCapabilityReportSchema,
|
|
@@ -208,7 +224,8 @@ const axHostCapabilityReportOperation = defineOperation<z.infer<typeof axHostCap
|
|
|
208
224
|
},
|
|
209
225
|
mcp: {
|
|
210
226
|
toolName: 'canvas_report_host_capability',
|
|
211
|
-
description:
|
|
227
|
+
description:
|
|
228
|
+
'Report host/session capability from an adapter: what the host can do (canvas/hooks/tools/sessionMessaging/permissions/files/uiPrompts). Stored for diagnostics; core does not depend on a host.',
|
|
212
229
|
extraShape: {
|
|
213
230
|
host: z.string().optional().describe('Host identifier (e.g. copilot, codex).'),
|
|
214
231
|
canvas: z.boolean().optional(),
|
|
@@ -219,7 +236,8 @@ const axHostCapabilityReportOperation = defineOperation<z.infer<typeof axHostCap
|
|
|
219
236
|
files: z.boolean().optional(),
|
|
220
237
|
uiPrompts: z.boolean().optional(),
|
|
221
238
|
raw: z.record(z.string(), z.unknown()).optional().describe('Optional raw capability payload for diagnostics.'),
|
|
222
|
-
source: z
|
|
239
|
+
source: z
|
|
240
|
+
.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
|
|
223
241
|
.optional()
|
|
224
242
|
.describe('Optional host/source label. Defaults to mcp.'),
|
|
225
243
|
},
|
|
@@ -53,7 +53,15 @@ import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
|
53
53
|
import { isRecord } from './nodes.js';
|
|
54
54
|
import { AX_SOURCE_SHAPE, axJsonResult, normalizeAxNodeIds, normalizeAxSource } from './ax-shared.js';
|
|
55
55
|
|
|
56
|
-
const AX_EVENT_KINDS = [
|
|
56
|
+
const AX_EVENT_KINDS = [
|
|
57
|
+
'prompt',
|
|
58
|
+
'assistant-message',
|
|
59
|
+
'tool-start',
|
|
60
|
+
'tool-result',
|
|
61
|
+
'failure',
|
|
62
|
+
'approval',
|
|
63
|
+
'steering',
|
|
64
|
+
] as const;
|
|
57
65
|
const AX_EVIDENCE_KINDS = ['logs', 'tool-result', 'screenshot', 'file', 'diff', 'test-output'] as const;
|
|
58
66
|
|
|
59
67
|
// ── ax.event.record (canvas_record_ax_event) ──────────────────
|
|
@@ -80,7 +88,8 @@ const axEventRecordOperation = defineOperation<z.infer<typeof axEventRecordSchem
|
|
|
80
88
|
},
|
|
81
89
|
mcp: {
|
|
82
90
|
toolName: 'canvas_record_ax_event',
|
|
83
|
-
description:
|
|
91
|
+
description:
|
|
92
|
+
'Record a normalized AX timeline event (prompt/assistant-message/tool-start/tool-result/failure/approval/steering). Timeline events persist for diagnostics and continuity but are not restored by snapshots.',
|
|
84
93
|
extraShape: {
|
|
85
94
|
kind: z.enum(AX_EVENT_KINDS).describe('Normalized event kind.'),
|
|
86
95
|
summary: z.string().describe('Short human-readable summary of the event.'),
|
|
@@ -136,7 +145,8 @@ const axEvidenceAddOperation = defineOperation<z.infer<typeof axEvidenceAddSchem
|
|
|
136
145
|
},
|
|
137
146
|
mcp: {
|
|
138
147
|
toolName: 'canvas_add_evidence',
|
|
139
|
-
description:
|
|
148
|
+
description:
|
|
149
|
+
'Record an AX evidence item (logs/tool-result/screenshot/file/diff/test-output) on the timeline. Evidence persists for diagnostics and continuity but is not restored by snapshots; exposed via canvas://ax-timeline.',
|
|
140
150
|
extraShape: {
|
|
141
151
|
kind: z.enum(AX_EVIDENCE_KINDS).describe('Evidence kind.'),
|
|
142
152
|
title: z.string().describe('Short human-readable title for the evidence.'),
|
|
@@ -189,7 +199,8 @@ const axSteerOperation = defineOperation<z.infer<typeof axSteerSchema>, Record<s
|
|
|
189
199
|
},
|
|
190
200
|
mcp: {
|
|
191
201
|
toolName: 'canvas_send_steering',
|
|
192
|
-
description:
|
|
202
|
+
description:
|
|
203
|
+
'Record a steering message: a user instruction from the surface to the active agent session. Persisted on the AX timeline and exposed via canvas://ax-timeline.',
|
|
193
204
|
extraShape: {
|
|
194
205
|
message: z.string().describe('The steering instruction to deliver to the active agent session.'),
|
|
195
206
|
source: AX_SOURCE_SHAPE,
|
|
@@ -228,7 +239,8 @@ const axTimelineGetOperation = defineOperation<z.infer<typeof axTimelineGetSchem
|
|
|
228
239
|
},
|
|
229
240
|
mcp: {
|
|
230
241
|
toolName: 'canvas_get_ax_timeline',
|
|
231
|
-
description:
|
|
242
|
+
description:
|
|
243
|
+
'Read the bounded AX timeline: recent agent-events, evidence, and steering messages plus counts. Use this for diagnostics and session continuity.',
|
|
232
244
|
extraShape: {
|
|
233
245
|
limit: z.number().optional().describe('Max rows per timeline table (default 50, max 200).'),
|
|
234
246
|
},
|
|
@@ -238,9 +250,7 @@ const axTimelineGetOperation = defineOperation<z.infer<typeof axTimelineGetSchem
|
|
|
238
250
|
// `limit` arrives as a number over MCP or a string over the HTTP query;
|
|
239
251
|
// Number() normalizes both. Only a finite positive limit is forwarded.
|
|
240
252
|
const limit = Number(input.limit ?? '');
|
|
241
|
-
const timeline = canvasState.getAxTimeline(
|
|
242
|
-
Number.isFinite(limit) && limit > 0 ? { limit } : {},
|
|
243
|
-
);
|
|
253
|
+
const timeline = canvasState.getAxTimeline(Number.isFinite(limit) && limit > 0 ? { limit } : {});
|
|
244
254
|
return { ok: true, ...timeline } as unknown as Record<string, unknown>;
|
|
245
255
|
},
|
|
246
256
|
});
|
|
@@ -266,11 +276,17 @@ const axDeliveryPendingOperation = defineOperation<z.infer<typeof axDeliveryPend
|
|
|
266
276
|
},
|
|
267
277
|
mcp: {
|
|
268
278
|
toolName: 'canvas_claim_ax_delivery',
|
|
269
|
-
description:
|
|
279
|
+
description:
|
|
280
|
+
'Claim pending PMX AX deliveries for a consumer (adapterless delivery). Returns `pending` undelivered steering (mark each with canvas_mark_ax_delivery after acting) AND `pendingActivity`: open canvas-bound AX items awaiting the agent (open work items, pending approval gates / elicitations / mode requests) — typically created by the human in the browser. Both exclude items the consumer itself originated (loop prevention). `pending` defaults to oldest-first (FIFO, for ordered processing); pass `order:"newest"` to surface the human\'s LATEST in-canvas steering first when a small `limit` would otherwise bury it behind a stale backlog (report #68). pendingActivity is read-only here: resolve each via its own tool (canvas_resolve_approval / canvas_respond_elicitation / canvas_resolve_mode / canvas_update_work_item), not canvas_mark_ax_delivery.',
|
|
270
281
|
extraShape: {
|
|
271
282
|
consumer: z.string().optional().describe('Consumer/source label to exclude from results (e.g. copilot, mcp).'),
|
|
272
283
|
limit: z.number().optional().describe('Max steering messages to return.'),
|
|
273
|
-
order: z
|
|
284
|
+
order: z
|
|
285
|
+
.enum(['newest', 'oldest'])
|
|
286
|
+
.optional()
|
|
287
|
+
.describe(
|
|
288
|
+
'Order of returned steering: "oldest" (FIFO, default) for ordered processing, or "newest" first to see the latest browser action when limited.',
|
|
289
|
+
),
|
|
274
290
|
},
|
|
275
291
|
// `consumer` is a loop-safety scope, not a source label — never defaulted.
|
|
276
292
|
formatResult: axJsonResult,
|
|
@@ -285,9 +301,7 @@ const axDeliveryPendingOperation = defineOperation<z.infer<typeof axDeliveryPend
|
|
|
285
301
|
// queries apply the same loop-safe consumer filter before the limit.
|
|
286
302
|
const newest = input.order === 'newest';
|
|
287
303
|
const scope = { ...(consumer ? { consumer } : {}), ...(limit ? { limit } : {}) };
|
|
288
|
-
const pending = newest
|
|
289
|
-
? canvasState.getPendingSteeringForContext(scope)
|
|
290
|
-
: canvasState.getPendingSteering(scope);
|
|
304
|
+
const pending = newest ? canvasState.getPendingSteeringForContext(scope) : canvasState.getPendingSteering(scope);
|
|
291
305
|
// The MCP tool aggregated pendingActivity; one wire body now serves it over
|
|
292
306
|
// HTTP too (documented broadening). Loop-safe: consumer scopes both queries.
|
|
293
307
|
const pendingActivity = buildPendingAxActivity(canvasState.getAxState(), consumer);
|
|
@@ -352,7 +366,8 @@ const axCommandInvokeOperation = defineOperation<z.infer<typeof axCommandInvokeS
|
|
|
352
366
|
},
|
|
353
367
|
mcp: {
|
|
354
368
|
toolName: 'canvas_invoke_command',
|
|
355
|
-
description:
|
|
369
|
+
description:
|
|
370
|
+
'Invoke a registry-gated PMX command intent (pmx.plan | pmx.execute | pmx.promote-context | pmx.summarize | pmx.review). Records a timeline event a host/agent can observe — NOT arbitrary execution; unknown names are rejected.',
|
|
356
371
|
extraShape: {
|
|
357
372
|
name: z.string().describe('A command name from the PMX command registry.'),
|
|
358
373
|
args: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -365,11 +380,9 @@ const axCommandInvokeOperation = defineOperation<z.infer<typeof axCommandInvokeS
|
|
|
365
380
|
if (typeof input.name !== 'string') {
|
|
366
381
|
throw new OperationError('command requires a name.');
|
|
367
382
|
}
|
|
368
|
-
const event = canvasState.invokeCommand(
|
|
369
|
-
input.
|
|
370
|
-
|
|
371
|
-
{ source: normalizeAxSource(input.source, 'api') },
|
|
372
|
-
);
|
|
383
|
+
const event = canvasState.invokeCommand(input.name, isRecord(input.args) ? input.args : null, {
|
|
384
|
+
source: normalizeAxSource(input.source, 'api'),
|
|
385
|
+
});
|
|
373
386
|
// Allowlist gate: invokeCommand returns null for an unknown command name.
|
|
374
387
|
if (!event) throw new OperationError(`Unknown command "${input.name}".`, 400);
|
|
375
388
|
ctx.emit('ax-event-created', { event });
|
|
@@ -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';
|
|
@@ -54,6 +51,7 @@ const SUPPORTED_BATCH_OPS = new Set([
|
|
|
54
51
|
'node.remove',
|
|
55
52
|
'graph.add',
|
|
56
53
|
'edge.add',
|
|
54
|
+
'edge.remove',
|
|
57
55
|
'group.create',
|
|
58
56
|
'group.add',
|
|
59
57
|
'group.remove',
|
|
@@ -207,7 +205,7 @@ function shapeBatchEntry(op: string, result: unknown): Record<string, unknown> {
|
|
|
207
205
|
return { ok: true, arranged: body.arranged, layout: body.layout };
|
|
208
206
|
}
|
|
209
207
|
|
|
210
|
-
// edge.add / group.remove: wire shape
|
|
208
|
+
// edge.add / edge.remove / group.remove: wire shape matches the push verbatim.
|
|
211
209
|
return body;
|
|
212
210
|
}
|
|
213
211
|
|
|
@@ -261,12 +259,19 @@ async function runBatch(operations: BatchEntry[]): Promise<BatchEnvelope> {
|
|
|
261
259
|
}
|
|
262
260
|
|
|
263
261
|
const batchShape = {
|
|
264
|
-
operations: z
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
262
|
+
operations: z
|
|
263
|
+
.array(
|
|
264
|
+
z.object({
|
|
265
|
+
op: z.string().describe('Operation name, e.g. "node.add" or "edge.add"'),
|
|
266
|
+
assign: z.string().optional().describe('Optional reference name for later operations'),
|
|
267
|
+
args: z.record(z.string(), z.unknown()).optional().describe('Operation arguments'),
|
|
268
|
+
}),
|
|
269
|
+
)
|
|
270
|
+
.describe('Ordered array of batch operations'),
|
|
271
|
+
full: z
|
|
272
|
+
.boolean()
|
|
273
|
+
.optional()
|
|
274
|
+
.describe('Return full batch operation results. Default false compacts node-like payloads.'),
|
|
270
275
|
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
271
276
|
};
|
|
272
277
|
|
|
@@ -287,7 +292,25 @@ function compactBatchValue(value: unknown): unknown {
|
|
|
287
292
|
const record = value as Record<string, unknown>;
|
|
288
293
|
const nodeLike = typeof record.id === 'string' && typeof record.type === 'string';
|
|
289
294
|
const compact: Record<string, unknown> = {};
|
|
290
|
-
for (const key of [
|
|
295
|
+
for (const key of [
|
|
296
|
+
'ok',
|
|
297
|
+
'id',
|
|
298
|
+
'type',
|
|
299
|
+
'kind',
|
|
300
|
+
'title',
|
|
301
|
+
'content',
|
|
302
|
+
'position',
|
|
303
|
+
'size',
|
|
304
|
+
'fetch',
|
|
305
|
+
'error',
|
|
306
|
+
'from',
|
|
307
|
+
'to',
|
|
308
|
+
'groupId',
|
|
309
|
+
'nodeIds',
|
|
310
|
+
'snapshot',
|
|
311
|
+
'arranged',
|
|
312
|
+
'layout',
|
|
313
|
+
]) {
|
|
291
314
|
if (record[key] !== undefined) compact[key] = record[key];
|
|
292
315
|
}
|
|
293
316
|
if (nodeLike) return compact;
|
|
@@ -325,13 +348,16 @@ const batchOperation = defineOperation<z.infer<typeof batchSchema>, BatchEnvelop
|
|
|
325
348
|
const body = await readJsonValue(req);
|
|
326
349
|
const operations = Array.isArray(body)
|
|
327
350
|
? body
|
|
328
|
-
: isRecord(body) && Array.isArray(body.operations)
|
|
351
|
+
: isRecord(body) && Array.isArray(body.operations)
|
|
352
|
+
? body.operations
|
|
353
|
+
: [];
|
|
329
354
|
return { operations };
|
|
330
355
|
},
|
|
331
356
|
},
|
|
332
357
|
mcp: {
|
|
333
358
|
toolName: 'canvas_batch',
|
|
334
|
-
description:
|
|
359
|
+
description:
|
|
360
|
+
'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, edge.remove, group.create, group.add, group.remove, pin.set/add/remove, snapshot.save, and arrange.',
|
|
335
361
|
formatResult: (result, input) => {
|
|
336
362
|
const envelope = result as BatchEnvelope;
|
|
337
363
|
const payload = wantsFullBatch(input) ? envelope : compactBatchResult(envelope);
|
|
@@ -361,5 +387,5 @@ export const batchOperations: Operation[] = [batchOperation];
|
|
|
361
387
|
export async function runCanvasBatchOperation(
|
|
362
388
|
operations: Array<{ op: string; assign?: string; args?: Record<string, unknown> }>,
|
|
363
389
|
): Promise<BatchEnvelope> {
|
|
364
|
-
return await executeOperation('canvas.batch', { operations }) as BatchEnvelope;
|
|
390
|
+
return (await executeOperation('canvas.batch', { operations })) as BatchEnvelope;
|
|
365
391
|
}
|