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.
Files changed (189) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/Readme.md +13 -11
  3. package/dist/canvas/index.js +2 -2
  4. package/dist/json-render/index.css +1 -1
  5. package/dist/types/cli/daemon.d.ts +74 -0
  6. package/dist/types/cli/watch.d.ts +2 -2
  7. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  8. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  9. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/mcp/canvas-access.d.ts +0 -7
  17. package/dist/types/server/ax-state-manager.d.ts +3 -2
  18. package/dist/types/server/bundled-skills.d.ts +3 -3
  19. package/dist/types/server/canvas-schema.d.ts +1 -1
  20. package/dist/types/server/canvas-state.d.ts +2 -0
  21. package/dist/types/server/ext-app-lookup.d.ts +1 -3
  22. package/dist/types/server/html-primitives.d.ts +1 -1
  23. package/dist/types/server/index.d.ts +4 -4
  24. package/dist/types/server/operations/composites.d.ts +29 -23
  25. package/dist/types/server/operations/http.d.ts +2 -1
  26. package/dist/types/server/operations/index.d.ts +1 -1
  27. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  28. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  29. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  30. package/dist/types/shared/ax-intent.d.ts +1 -1
  31. package/docs/RELEASE.md +23 -8
  32. package/docs/ax-host-adapter-contract.md +7 -7
  33. package/docs/ax-state-contract.md +7 -6
  34. package/docs/bun-webview-integration.md +5 -5
  35. package/docs/cli.md +1 -1
  36. package/docs/http-api.md +35 -3
  37. package/docs/mcp.md +121 -100
  38. package/docs/node-types.md +45 -35
  39. package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
  40. package/docs/plans/plan-008-registry-finish.md +2 -0
  41. package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
  42. package/docs/screenshot.png +0 -0
  43. package/docs/tech-debt-assessment-2026-06.md +1 -1
  44. package/docs/tech-debt-assessment-2026-07.md +135 -0
  45. package/package.json +5 -2
  46. package/skills/data-analysis/SKILL.md +3 -3
  47. package/skills/frontend-design/SKILL.md +3 -2
  48. package/skills/json-render-mcp/SKILL.md +4 -3
  49. package/skills/playwright-cli/SKILL.md +1 -1
  50. package/skills/pmx-canvas/SKILL.md +12 -7
  51. package/skills/pmx-canvas/evals/evals.json +2 -2
  52. package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
  53. package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
  54. package/skills/pmx-canvas/references/full-reference.md +58 -56
  55. package/skills/pmx-canvas/references/html-primitives.md +3 -3
  56. package/skills/tufte-viz/SKILL.md +5 -4
  57. package/skills/web-artifacts-builder/SKILL.md +4 -4
  58. package/src/cli/agent.ts +1861 -1548
  59. package/src/cli/daemon.ts +460 -0
  60. package/src/cli/index.ts +63 -326
  61. package/src/cli/watch.ts +2 -10
  62. package/src/client/App.tsx +48 -46
  63. package/src/client/canvas/AttentionHistory.tsx +11 -1
  64. package/src/client/canvas/CanvasNode.tsx +41 -29
  65. package/src/client/canvas/CanvasViewport.tsx +101 -66
  66. package/src/client/canvas/CommandPalette.tsx +61 -27
  67. package/src/client/canvas/ContextMenu.tsx +13 -20
  68. package/src/client/canvas/ContextPinBar.tsx +1 -5
  69. package/src/client/canvas/ContextPinHud.tsx +1 -6
  70. package/src/client/canvas/DockedNode.tsx +4 -4
  71. package/src/client/canvas/EdgeLayer.tsx +37 -36
  72. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  73. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  74. package/src/client/canvas/IntentLayer.tsx +31 -14
  75. package/src/client/canvas/Minimap.tsx +11 -16
  76. package/src/client/canvas/SelectionBar.tsx +4 -11
  77. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  78. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  79. package/src/client/canvas/auto-fit.ts +15 -14
  80. package/src/client/canvas/snap-guides.ts +12 -12
  81. package/src/client/canvas/use-node-resize.ts +1 -5
  82. package/src/client/canvas/use-pan-zoom.ts +25 -26
  83. package/src/client/ext-app/bridge.ts +3 -12
  84. package/src/client/icons.tsx +63 -20
  85. package/src/client/nodes/ContextNode.tsx +14 -25
  86. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  87. package/src/client/nodes/FileNode.tsx +74 -62
  88. package/src/client/nodes/GroupNode.tsx +4 -6
  89. package/src/client/nodes/HtmlNode.tsx +76 -46
  90. package/src/client/nodes/ImageNode.tsx +18 -27
  91. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  92. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  93. package/src/client/nodes/LedgerNode.tsx +10 -4
  94. package/src/client/nodes/MarkdownNode.tsx +3 -10
  95. package/src/client/nodes/McpAppNode.tsx +26 -22
  96. package/src/client/nodes/MdFormatBar.tsx +10 -7
  97. package/src/client/nodes/PromptNode.tsx +23 -51
  98. package/src/client/nodes/ResponseNode.tsx +3 -13
  99. package/src/client/nodes/StatusNode.tsx +5 -9
  100. package/src/client/nodes/StatusSummary.tsx +2 -8
  101. package/src/client/nodes/WebpageNode.tsx +20 -14
  102. package/src/client/nodes/iframe-document-url.ts +25 -16
  103. package/src/client/nodes/image-warnings.ts +1 -7
  104. package/src/client/nodes/md-format.ts +20 -5
  105. package/src/client/state/attention-bridge.ts +4 -9
  106. package/src/client/state/attention-store.ts +1 -7
  107. package/src/client/state/canvas-store.ts +52 -36
  108. package/src/client/state/intent-bridge.ts +176 -112
  109. package/src/client/state/intent-store.ts +4 -1
  110. package/src/client/state/sse-bridge.ts +53 -70
  111. package/src/json-render/catalog.ts +12 -16
  112. package/src/json-render/charts/components.tsx +16 -20
  113. package/src/json-render/charts/extra-components.tsx +8 -16
  114. package/src/json-render/charts/extra-definitions.ts +1 -2
  115. package/src/json-render/charts/tufte-components.tsx +37 -20
  116. package/src/json-render/charts/tufte-definitions.ts +8 -2
  117. package/src/json-render/renderer/index.tsx +42 -22
  118. package/src/json-render/schema.ts +6 -3
  119. package/src/json-render/server.ts +33 -39
  120. package/src/mcp/canvas-access.ts +40 -121
  121. package/src/mcp/server.ts +206 -271
  122. package/src/server/agent-context.ts +63 -36
  123. package/src/server/ax-context.ts +7 -5
  124. package/src/server/ax-interaction.ts +176 -43
  125. package/src/server/ax-state-manager.ts +185 -41
  126. package/src/server/ax-state.ts +142 -47
  127. package/src/server/bundled-skills.ts +3 -3
  128. package/src/server/canvas-db.ts +213 -95
  129. package/src/server/canvas-operations.ts +177 -120
  130. package/src/server/canvas-provenance.ts +1 -4
  131. package/src/server/canvas-schema.ts +500 -102
  132. package/src/server/canvas-serialization.ts +27 -35
  133. package/src/server/canvas-state.ts +155 -58
  134. package/src/server/chart-template.ts +4 -5
  135. package/src/server/code-graph.ts +20 -7
  136. package/src/server/diagram-presets.ts +28 -29
  137. package/src/server/ext-app-lookup.ts +4 -20
  138. package/src/server/html-node-summary.ts +19 -10
  139. package/src/server/html-primitives.ts +326 -97
  140. package/src/server/html-surface.ts +6 -9
  141. package/src/server/image-source.ts +6 -4
  142. package/src/server/index.ts +321 -218
  143. package/src/server/intent-registry.ts +3 -6
  144. package/src/server/mcp-app-candidate.ts +5 -10
  145. package/src/server/mcp-app-host.ts +14 -38
  146. package/src/server/mcp-app-runtime.ts +12 -20
  147. package/src/server/mutation-history.ts +15 -5
  148. package/src/server/operations/composites.ts +38 -40
  149. package/src/server/operations/http.ts +5 -5
  150. package/src/server/operations/index.ts +7 -1
  151. package/src/server/operations/invoker.ts +4 -3
  152. package/src/server/operations/mcp.ts +29 -36
  153. package/src/server/operations/ops/annotation.ts +122 -10
  154. package/src/server/operations/ops/app.ts +102 -77
  155. package/src/server/operations/ops/ax-await.ts +17 -10
  156. package/src/server/operations/ops/ax-read.ts +347 -0
  157. package/src/server/operations/ops/ax-shared.ts +2 -7
  158. package/src/server/operations/ops/ax-state.ts +32 -14
  159. package/src/server/operations/ops/ax-timeline.ts +32 -19
  160. package/src/server/operations/ops/ax-work.ts +54 -37
  161. package/src/server/operations/ops/batch.ts +39 -14
  162. package/src/server/operations/ops/canvas-wire.ts +91 -0
  163. package/src/server/operations/ops/edges.ts +37 -25
  164. package/src/server/operations/ops/ext-app.ts +346 -0
  165. package/src/server/operations/ops/groups.ts +49 -20
  166. package/src/server/operations/ops/intent.ts +18 -12
  167. package/src/server/operations/ops/json-render.ts +239 -98
  168. package/src/server/operations/ops/nodes.ts +300 -111
  169. package/src/server/operations/ops/query.ts +62 -33
  170. package/src/server/operations/ops/snapshots.ts +35 -26
  171. package/src/server/operations/ops/validate.ts +2 -1
  172. package/src/server/operations/ops/viewport.ts +60 -16
  173. package/src/server/operations/ops/webview.ts +44 -18
  174. package/src/server/operations/registry.ts +2 -3
  175. package/src/server/operations/types.ts +7 -5
  176. package/src/server/operations/webview-runner.ts +1 -3
  177. package/src/server/placement.ts +8 -18
  178. package/src/server/server.ts +121 -1012
  179. package/src/server/spatial-analysis.ts +39 -25
  180. package/src/server/trace-manager.ts +3 -8
  181. package/src/server/web-artifacts.ts +23 -27
  182. package/src/server/webpage-node.ts +5 -13
  183. package/src/shared/auto-arrange.ts +12 -5
  184. package/src/shared/ax-intent.ts +1 -1
  185. package/src/shared/content-height-reporter.ts +8 -6
  186. package/src/shared/ext-app-tool-result.ts +2 -6
  187. package/src/shared/placement.ts +1 -4
  188. package/src/shared/semantic-attention.ts +39 -37
  189. 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: '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.',
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.enum(['todo', 'in-progress', 'blocked', 'done', 'cancelled'])
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(`invalid work item status "${String(input.status)}"; expected one of: todo, in-progress, blocked, done, cancelled.`);
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: 'Update a canvas-bound AX work item by ID (title/status/detail/nodeIds). Returns null if the work item does not exist.',
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(`invalid work item status "${String(input.status)}"; expected one of: todo, in-progress, blocked, done, cancelled.`);
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: '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.',
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.object({
252
- line: z.number().optional(),
253
- endLine: z.number().optional(),
254
- label: z.string().optional(),
255
- }).optional().describe('Region descriptor when anchorType is region.'),
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: '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.',
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: 'Resolve a pending approval gate by ID with approved or rejected. Returns null if the gate does not exist or is already resolved.',
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
- id,
427
- input.decision,
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<z.infer<typeof axElicitationRequestSchema>, Record<string, unknown>>({
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: '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.',
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) ? { fields: input.fields.filter((f): f is string => typeof f === 'string') } : {}),
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<z.infer<typeof axElicitationRespondSchema>, Record<string, unknown>>({
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, { source: normalizeAxSource(input.source, 'api') });
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: '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.',
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.array(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
- })).describe('Ordered array of batch operations'),
269
- full: z.boolean().optional().describe('Return full batch operation results. Default false compacts node-like payloads.'),
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 ['ok', 'id', 'type', 'kind', 'title', 'content', 'position', 'size', 'fetch', 'error', 'from', 'to', 'groupId', 'nodeIds', 'snapshot', 'arranged', 'layout']) {
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) ? 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: '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.',
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.string().optional().catch(undefined).describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
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.string().optional().catch(undefined).describe('Resolve the source node by exact or fuzzy title/content search'),
23
- toSearch: z.string().optional().catch(undefined).describe('Resolve the target node by exact or fuzzy title/content search'),
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: 'Add an edge (connection) between two nodes. Edge types: flow (sequential), depends-on (dependency), relation (general), references (cross-reference).',
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
- type: 'text' as const,
64
- text: JSON.stringify({
65
- id: body.id,
66
- from: body.from,
67
- to: body.to,
68
- type: body.type,
69
- label: body.label,
70
- style: body.style,
71
- animated: body.animated,
72
- }, null, 2),
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];