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.
Files changed (161) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/canvas/index.js +65 -65
  3. package/dist/types/cli/agent.d.ts +9 -1
  4. package/dist/types/cli/daemon.d.ts +74 -0
  5. package/dist/types/cli/watch.d.ts +2 -2
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  8. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  9. package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -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/server/canvas-schema.d.ts +1 -1
  17. package/dist/types/server/html-primitives.d.ts +1 -1
  18. package/dist/types/server/index.d.ts +4 -4
  19. package/dist/types/server/operations/index.d.ts +1 -1
  20. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  21. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  22. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  23. package/dist/types/server/server.d.ts +8 -0
  24. package/docs/cli.md +10 -1
  25. package/docs/http-api.md +28 -0
  26. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  27. package/docs/screenshot.png +0 -0
  28. package/docs/tech-debt-assessment-2026-07.md +2 -2
  29. package/package.json +5 -2
  30. package/skills/pmx-canvas/SKILL.md +15 -10
  31. package/skills/pmx-canvas/references/full-reference.md +17 -3
  32. package/src/cli/agent.ts +1951 -1571
  33. package/src/cli/daemon.ts +460 -0
  34. package/src/cli/index.ts +80 -323
  35. package/src/cli/watch.ts +2 -10
  36. package/src/client/App.tsx +48 -46
  37. package/src/client/canvas/AttentionHistory.tsx +11 -1
  38. package/src/client/canvas/CanvasNode.tsx +41 -29
  39. package/src/client/canvas/CanvasViewport.tsx +101 -66
  40. package/src/client/canvas/CommandPalette.tsx +61 -27
  41. package/src/client/canvas/ContextMenu.tsx +13 -20
  42. package/src/client/canvas/ContextPinBar.tsx +1 -5
  43. package/src/client/canvas/ContextPinHud.tsx +1 -6
  44. package/src/client/canvas/DockedNode.tsx +4 -4
  45. package/src/client/canvas/EdgeLayer.tsx +37 -36
  46. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  47. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  48. package/src/client/canvas/IntentLayer.tsx +31 -14
  49. package/src/client/canvas/Minimap.tsx +11 -16
  50. package/src/client/canvas/SelectionBar.tsx +4 -11
  51. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  52. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  53. package/src/client/canvas/auto-fit.ts +15 -14
  54. package/src/client/canvas/snap-guides.ts +12 -12
  55. package/src/client/canvas/use-node-resize.ts +1 -5
  56. package/src/client/canvas/use-pan-zoom.ts +25 -26
  57. package/src/client/ext-app/bridge.ts +3 -12
  58. package/src/client/icons.tsx +63 -20
  59. package/src/client/nodes/ContextNode.tsx +14 -25
  60. package/src/client/nodes/ExtAppFrame.tsx +194 -80
  61. package/src/client/nodes/FileNode.tsx +74 -62
  62. package/src/client/nodes/GroupNode.tsx +4 -6
  63. package/src/client/nodes/HtmlNode.tsx +76 -46
  64. package/src/client/nodes/ImageNode.tsx +18 -27
  65. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  66. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  67. package/src/client/nodes/LedgerNode.tsx +10 -4
  68. package/src/client/nodes/MarkdownNode.tsx +3 -10
  69. package/src/client/nodes/McpAppNode.tsx +26 -22
  70. package/src/client/nodes/MdFormatBar.tsx +10 -7
  71. package/src/client/nodes/PromptNode.tsx +23 -51
  72. package/src/client/nodes/ResponseNode.tsx +3 -13
  73. package/src/client/nodes/StatusNode.tsx +5 -9
  74. package/src/client/nodes/StatusSummary.tsx +2 -8
  75. package/src/client/nodes/WebpageNode.tsx +20 -14
  76. package/src/client/nodes/iframe-document-url.ts +25 -16
  77. package/src/client/nodes/image-warnings.ts +1 -7
  78. package/src/client/nodes/md-format.ts +20 -5
  79. package/src/client/state/attention-bridge.ts +4 -9
  80. package/src/client/state/attention-store.ts +1 -7
  81. package/src/client/state/canvas-store.ts +52 -36
  82. package/src/client/state/intent-bridge.ts +176 -112
  83. package/src/client/state/intent-store.ts +4 -1
  84. package/src/client/state/sse-bridge.ts +53 -70
  85. package/src/json-render/catalog.ts +12 -16
  86. package/src/json-render/charts/components.tsx +16 -20
  87. package/src/json-render/charts/extra-components.tsx +8 -16
  88. package/src/json-render/charts/extra-definitions.ts +1 -2
  89. package/src/json-render/charts/tufte-components.tsx +37 -20
  90. package/src/json-render/charts/tufte-definitions.ts +8 -2
  91. package/src/json-render/renderer/index.tsx +42 -22
  92. package/src/json-render/schema.ts +6 -3
  93. package/src/json-render/server.ts +33 -39
  94. package/src/mcp/canvas-access.ts +35 -21
  95. package/src/mcp/server.ts +132 -70
  96. package/src/server/agent-context.ts +63 -36
  97. package/src/server/ax-context.ts +7 -5
  98. package/src/server/ax-interaction.ts +176 -43
  99. package/src/server/ax-state-manager.ts +182 -39
  100. package/src/server/ax-state.ts +142 -47
  101. package/src/server/canvas-db.ts +213 -95
  102. package/src/server/canvas-operations.ts +180 -123
  103. package/src/server/canvas-provenance.ts +1 -4
  104. package/src/server/canvas-schema.ts +454 -73
  105. package/src/server/canvas-serialization.ts +27 -35
  106. package/src/server/canvas-state.ts +150 -58
  107. package/src/server/chart-template.ts +4 -5
  108. package/src/server/code-graph.ts +19 -6
  109. package/src/server/diagram-presets.ts +28 -29
  110. package/src/server/ext-app-lookup.ts +3 -12
  111. package/src/server/html-node-summary.ts +19 -10
  112. package/src/server/html-primitives.ts +326 -97
  113. package/src/server/html-surface.ts +6 -9
  114. package/src/server/image-source.ts +6 -4
  115. package/src/server/index.ts +320 -217
  116. package/src/server/intent-registry.ts +2 -5
  117. package/src/server/mcp-app-candidate.ts +5 -10
  118. package/src/server/mcp-app-host.ts +14 -38
  119. package/src/server/mcp-app-runtime.ts +12 -20
  120. package/src/server/mutation-history.ts +15 -5
  121. package/src/server/operations/composites.ts +1 -3
  122. package/src/server/operations/http.ts +2 -3
  123. package/src/server/operations/index.ts +7 -1
  124. package/src/server/operations/invoker.ts +4 -3
  125. package/src/server/operations/mcp.ts +22 -30
  126. package/src/server/operations/ops/annotation.ts +122 -10
  127. package/src/server/operations/ops/app.ts +98 -73
  128. package/src/server/operations/ops/ax-await.ts +17 -10
  129. package/src/server/operations/ops/ax-read.ts +347 -0
  130. package/src/server/operations/ops/ax-shared.ts +2 -7
  131. package/src/server/operations/ops/ax-state.ts +32 -14
  132. package/src/server/operations/ops/ax-timeline.ts +32 -19
  133. package/src/server/operations/ops/ax-work.ts +54 -37
  134. package/src/server/operations/ops/batch.ts +41 -15
  135. package/src/server/operations/ops/canvas-wire.ts +91 -0
  136. package/src/server/operations/ops/edges.ts +37 -25
  137. package/src/server/operations/ops/ext-app.ts +346 -0
  138. package/src/server/operations/ops/groups.ts +49 -20
  139. package/src/server/operations/ops/intent.ts +18 -12
  140. package/src/server/operations/ops/json-render.ts +239 -98
  141. package/src/server/operations/ops/nodes.ts +298 -109
  142. package/src/server/operations/ops/query.ts +46 -28
  143. package/src/server/operations/ops/snapshots.ts +35 -26
  144. package/src/server/operations/ops/validate.ts +2 -1
  145. package/src/server/operations/ops/viewport.ts +60 -16
  146. package/src/server/operations/ops/webview.ts +44 -18
  147. package/src/server/operations/registry.ts +2 -3
  148. package/src/server/operations/types.ts +7 -5
  149. package/src/server/operations/webview-runner.ts +1 -3
  150. package/src/server/placement.ts +8 -18
  151. package/src/server/server.ts +122 -1028
  152. package/src/server/spatial-analysis.ts +39 -25
  153. package/src/server/trace-manager.ts +3 -8
  154. package/src/server/web-artifacts.ts +23 -27
  155. package/src/server/webpage-node.ts +5 -13
  156. package/src/shared/auto-arrange.ts +12 -5
  157. package/src/shared/content-height-reporter.ts +8 -6
  158. package/src/shared/ext-app-tool-result.ts +2 -6
  159. package/src/shared/placement.ts +1 -4
  160. package/src/shared/semantic-attention.ts +39 -37
  161. package/src/shared/surface.ts +8 -4
@@ -85,10 +85,7 @@ export interface OpenMcpAppCoreResult {
85
85
  * call it. The diagram.open op delegates here after building the Excalidraw input
86
86
  * (the SSE pair fires ONCE — diagram.open does not re-emit).
87
87
  */
88
- export async function openMcpAppCore(
89
- input: OpenMcpAppCoreInput,
90
- ctx: OperationContext,
91
- ): Promise<OpenMcpAppCoreResult> {
88
+ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationContext): Promise<OpenMcpAppCoreResult> {
92
89
  // The canvas node is created as a side-effect of the `ext-app-open` SSE event
93
90
  // (syncEventToCanvasState). Inside a suppressed-emit run (canvas.batch) that
94
91
  // event is dropped, so the node would never be created and the just-opened
@@ -155,10 +152,12 @@ export async function openMcpAppCore(
155
152
  success: toolResult.isError !== true,
156
153
  result: toolResult,
157
154
  });
158
- const nodeId = input.nodeId ?? findCanvasExtAppNodeId(toolCallId, {
159
- getNode: (id) => canvasState.getNode(id),
160
- listNodes: () => canvasState.getLayout().nodes,
161
- });
155
+ const nodeId =
156
+ input.nodeId ??
157
+ findCanvasExtAppNodeId(toolCallId, {
158
+ getNode: (id) => canvasState.getNode(id),
159
+ listNodes: () => canvasState.getLayout().nodes,
160
+ });
162
161
  return {
163
162
  ok: true,
164
163
  ...(nodeId ? { id: nodeId } : {}),
@@ -180,15 +179,10 @@ function buildOpenMcpAppInput(body: Record<string, unknown>): OpenMcpAppCoreInpu
180
179
  throw new OperationError('Missing valid transport or toolName.');
181
180
  }
182
181
  const toolArguments = isRecord(body.toolArguments) ? body.toolArguments : undefined;
183
- const serverName = typeof body.serverName === 'string' && body.serverName.trim().length > 0
184
- ? body.serverName.trim()
185
- : undefined;
186
- const title = typeof body.title === 'string' && body.title.trim().length > 0
187
- ? body.title.trim()
188
- : undefined;
189
- const nodeId = typeof body.nodeId === 'string' && body.nodeId.trim().length > 0
190
- ? body.nodeId.trim()
191
- : undefined;
182
+ const serverName =
183
+ typeof body.serverName === 'string' && body.serverName.trim().length > 0 ? body.serverName.trim() : undefined;
184
+ const title = typeof body.title === 'string' && body.title.trim().length > 0 ? body.title.trim() : undefined;
185
+ const nodeId = typeof body.nodeId === 'string' && body.nodeId.trim().length > 0 ? body.nodeId.trim() : undefined;
192
186
  return {
193
187
  transport,
194
188
  toolName,
@@ -216,21 +210,26 @@ const openMcpAppShape = {
216
210
  y: z.number().optional().describe('Y position (auto-placed if omitted)'),
217
211
  width: z.number().optional().describe('Width in pixels (default: 720)'),
218
212
  height: z.number().optional().describe('Height in pixels (default: 500)'),
219
- timeoutMs: z.number().optional().describe('Optional MCP request timeout in milliseconds for cold external app servers'),
220
- transport: z.union([
221
- z.object({
222
- type: z.literal('stdio'),
223
- command: z.string().describe('Executable used to start the external MCP server'),
224
- args: z.array(z.string()).optional().describe('Arguments for the executable'),
225
- cwd: z.string().optional().describe('Optional working directory'),
226
- env: z.record(z.string(), z.string()).optional().describe('Optional environment overrides'),
227
- }),
228
- z.object({
229
- type: z.literal('http'),
230
- url: z.string().describe('Streamable HTTP MCP endpoint URL'),
231
- headers: z.record(z.string(), z.string()).optional().describe('Optional HTTP headers'),
232
- }),
233
- ]).describe('How PMX Canvas should connect to the external MCP server'),
213
+ timeoutMs: z
214
+ .number()
215
+ .optional()
216
+ .describe('Optional MCP request timeout in milliseconds for cold external app servers'),
217
+ transport: z
218
+ .union([
219
+ z.object({
220
+ type: z.literal('stdio'),
221
+ command: z.string().describe('Executable used to start the external MCP server'),
222
+ args: z.array(z.string()).optional().describe('Arguments for the executable'),
223
+ cwd: z.string().optional().describe('Optional working directory'),
224
+ env: z.record(z.string(), z.string()).optional().describe('Optional environment overrides'),
225
+ }),
226
+ z.object({
227
+ type: z.literal('http'),
228
+ url: z.string().describe('Streamable HTTP MCP endpoint URL'),
229
+ headers: z.record(z.string(), z.string()).optional().describe('Optional HTTP headers'),
230
+ }),
231
+ ])
232
+ .describe('How PMX Canvas should connect to the external MCP server'),
234
233
  };
235
234
  const openMcpAppSchema = z.looseObject(openMcpAppShape);
236
235
 
@@ -257,17 +256,29 @@ const openMcpAppOperation = defineOperation<z.infer<typeof openMcpAppSchema>, Op
257
256
  // ── diagram.open (Excalidraw preset → mcpapp.open core) ───────
258
257
 
259
258
  const diagramShape = {
260
- elements: z.union([
261
- z.string().describe('JSON array string of Excalidraw elements'),
262
- z.array(z.record(z.string(), z.unknown())).describe('Array of Excalidraw elements'),
263
- ]).describe('Excalidraw elements to render. See https://github.com/excalidraw/excalidraw-mcp for the element format.'),
264
- nodeId: z.string().optional().describe('Existing Excalidraw mcp-app node ID to update in place instead of creating a new node.'),
259
+ elements: z
260
+ .union([
261
+ z.string().describe('JSON array string of Excalidraw elements'),
262
+ z.array(z.record(z.string(), z.unknown())).describe('Array of Excalidraw elements'),
263
+ ])
264
+ .describe(
265
+ 'Excalidraw elements to render. See https://github.com/excalidraw/excalidraw-mcp for the element format.',
266
+ ),
267
+ nodeId: z
268
+ .string()
269
+ .optional()
270
+ .describe('Existing Excalidraw mcp-app node ID to update in place instead of creating a new node.'),
265
271
  title: z.string().optional().describe('Optional canvas node title override'),
266
272
  x: z.number().optional().describe('X position (auto-placed if omitted)'),
267
273
  y: z.number().optional().describe('Y position (auto-placed if omitted)'),
268
274
  width: z.number().optional().describe('Width in pixels (default: 720)'),
269
275
  height: z.number().optional().describe('Height in pixels (default: 500)'),
270
- timeoutMs: z.number().optional().describe('Optional MCP request timeout in milliseconds for Excalidraw cold starts. Client-side MCP hosts may still enforce their own total request timeout.'),
276
+ timeoutMs: z
277
+ .number()
278
+ .optional()
279
+ .describe(
280
+ 'Optional MCP request timeout in milliseconds for Excalidraw cold starts. Client-side MCP hosts may still enforce their own total request timeout.',
281
+ ),
271
282
  };
272
283
  const diagramSchema = z.looseObject(diagramShape);
273
284
 
@@ -289,7 +300,7 @@ const diagramOperation = defineOperation<z.infer<typeof diagramSchema>, OpenMcpA
289
300
  }),
290
301
  },
291
302
  handler: (input, ctx) => {
292
- let built;
303
+ let built: ReturnType<typeof buildExcalidrawOpenMcpAppInput>;
293
304
  try {
294
305
  built = buildExcalidrawOpenMcpAppInput(input);
295
306
  } catch (error) {
@@ -297,19 +308,22 @@ const diagramOperation = defineOperation<z.infer<typeof diagramSchema>, OpenMcpA
297
308
  }
298
309
  // Delegate to the shared open core (the SSE pair fires once — diagram.open
299
310
  // does not re-emit).
300
- return openMcpAppCore({
301
- transport: built.transport,
302
- toolName: built.toolName,
303
- toolArguments: built.toolArguments,
304
- serverName: built.serverName,
305
- ...(built.nodeId ? { nodeId: built.nodeId } : {}),
306
- ...(built.title ? { title: built.title } : {}),
307
- ...(typeof built.x === 'number' ? { x: built.x } : {}),
308
- ...(typeof built.y === 'number' ? { y: built.y } : {}),
309
- ...(typeof built.width === 'number' ? { width: built.width } : {}),
310
- ...(typeof built.height === 'number' ? { height: built.height } : {}),
311
- ...(typeof built.timeoutMs === 'number' ? { timeoutMs: built.timeoutMs } : {}),
312
- }, ctx);
311
+ return openMcpAppCore(
312
+ {
313
+ transport: built.transport,
314
+ toolName: built.toolName,
315
+ toolArguments: built.toolArguments,
316
+ serverName: built.serverName,
317
+ ...(built.nodeId ? { nodeId: built.nodeId } : {}),
318
+ ...(built.title ? { title: built.title } : {}),
319
+ ...(typeof built.x === 'number' ? { x: built.x } : {}),
320
+ ...(typeof built.y === 'number' ? { y: built.y } : {}),
321
+ ...(typeof built.width === 'number' ? { width: built.width } : {}),
322
+ ...(typeof built.height === 'number' ? { height: built.height } : {}),
323
+ ...(typeof built.timeoutMs === 'number' ? { timeoutMs: built.timeoutMs } : {}),
324
+ },
325
+ ctx,
326
+ );
313
327
  },
314
328
  });
315
329
 
@@ -321,23 +335,42 @@ const webArtifactShape = {
321
335
  indexCss: z.string().optional().describe('Optional contents for src/index.css'),
322
336
  mainTsx: z.string().optional().describe('Optional contents for src/main.tsx'),
323
337
  indexHtml: z.string().optional().describe('Optional contents for index.html'),
324
- files: z.record(z.string(), z.string()).optional().describe('Optional map of additional project-relative file paths to file contents'),
325
- deps: z.array(z.string()).optional().describe('Optional npm dependencies to install before bundling (e.g. ["recharts", "framer-motion@^11"]). Validated against npm-name format; flags and shell metacharacters are rejected.'),
326
- projectPath: z.string().optional().describe('Optional workspace-relative reusable project path. Defaults to .pmx-canvas/artifacts/.web-artifacts/<slug>'),
327
- outputPath: z.string().optional().describe('Optional workspace-relative HTML output path. Defaults to .pmx-canvas/artifacts/<slug>.html'),
338
+ files: z
339
+ .record(z.string(), z.string())
340
+ .optional()
341
+ .describe('Optional map of additional project-relative file paths to file contents'),
342
+ deps: z
343
+ .array(z.string())
344
+ .optional()
345
+ .describe(
346
+ 'Optional npm dependencies to install before bundling (e.g. ["recharts", "framer-motion@^11"]). Validated against npm-name format; flags and shell metacharacters are rejected.',
347
+ ),
348
+ projectPath: z
349
+ .string()
350
+ .optional()
351
+ .describe(
352
+ 'Optional workspace-relative reusable project path. Defaults to .pmx-canvas/artifacts/.web-artifacts/<slug>',
353
+ ),
354
+ outputPath: z
355
+ .string()
356
+ .optional()
357
+ .describe('Optional workspace-relative HTML output path. Defaults to .pmx-canvas/artifacts/<slug>.html'),
328
358
  openInCanvas: z.boolean().optional().describe('Open the generated artifact in canvas after build (default true)'),
329
359
  includeLogs: z.boolean().optional().describe('Include raw build stdout/stderr in the response (default false)'),
330
- initScriptPath: z.string().optional().describe('Optional script path override for tests/debugging. Must resolve inside the workspace.'),
331
- bundleScriptPath: z.string().optional().describe('Optional script path override for tests/debugging. Must resolve inside the workspace.'),
360
+ initScriptPath: z
361
+ .string()
362
+ .optional()
363
+ .describe('Optional script path override for tests/debugging. Must resolve inside the workspace.'),
364
+ bundleScriptPath: z
365
+ .string()
366
+ .optional()
367
+ .describe('Optional script path override for tests/debugging. Must resolve inside the workspace.'),
332
368
  timeoutMs: z.number().optional().describe('Optional timeout in milliseconds for init and bundle commands'),
333
369
  };
334
370
  const webArtifactSchema = z.looseObject(webArtifactShape);
335
371
 
336
372
  /** Shape the byte-identical web-artifact response envelope from the build result. */
337
- function webArtifactEnvelope(
338
- result: WebArtifactCanvasBuildResult,
339
- includeLogs: boolean,
340
- ): Record<string, unknown> {
373
+ function webArtifactEnvelope(result: WebArtifactCanvasBuildResult, includeLogs: boolean): Record<string, unknown> {
341
374
  return {
342
375
  ok: true,
343
376
  path: result.filePath,
@@ -415,12 +448,8 @@ const webArtifactOperation = defineOperation<z.infer<typeof webArtifactSchema>,
415
448
  // legacy HTTP handler used resolveWorkspacePath; the MCP tool used its
416
449
  // own safeWorkspacePath — both enforce containment). resolveWorkspacePath
417
450
  // resolves against the active canvas workspace root.
418
- ...(typeof input.projectPath === 'string'
419
- ? { projectPath: resolveWorkspacePath(input.projectPath) }
420
- : {}),
421
- ...(typeof input.outputPath === 'string'
422
- ? { outputPath: resolveWorkspacePath(input.outputPath) }
423
- : {}),
451
+ ...(typeof input.projectPath === 'string' ? { projectPath: resolveWorkspacePath(input.projectPath) } : {}),
452
+ ...(typeof input.outputPath === 'string' ? { outputPath: resolveWorkspacePath(input.outputPath) } : {}),
424
453
  // Script-path overrides are honored only when contained inside the
425
454
  // workspace (enforced by resolveTrustedScriptPath in
426
455
  // executeWebArtifactBuild), so they cannot point at an arbitrary host
@@ -440,8 +469,4 @@ const webArtifactOperation = defineOperation<z.infer<typeof webArtifactSchema>,
440
469
  },
441
470
  });
442
471
 
443
- export const appOperations: Operation[] = [
444
- openMcpAppOperation,
445
- diagramOperation,
446
- webArtifactOperation,
447
- ];
472
+ export const appOperations: Operation[] = [openMcpAppOperation, diagramOperation, webArtifactOperation];
@@ -92,7 +92,13 @@ const awaitInputSchema = z.looseObject(awaitInputShape);
92
92
 
93
93
  const awaitMcpExtraShape = {
94
94
  id: z.string(),
95
- timeoutMs: z.number().int().min(0).max(120000).optional().describe('Max ms to block (default 30000; 0 = immediate read; capped at 120000).'),
95
+ timeoutMs: z
96
+ .number()
97
+ .int()
98
+ .min(0)
99
+ .max(120000)
100
+ .optional()
101
+ .describe('Max ms to block (default 30000; 0 = immediate read; capped at 120000).'),
96
102
  };
97
103
 
98
104
  // ── ax.approval.get (canvas_await_approval) ───────────────────
@@ -110,7 +116,8 @@ const axApprovalGetOperation = defineOperation<z.infer<typeof awaitInputSchema>,
110
116
  },
111
117
  mcp: {
112
118
  toolName: 'canvas_await_approval',
113
- description: 'Block until an approval gate resolves (the human approves/rejects it in the browser) or the timeout elapses — primitive D, gates that actually gate. timeoutMs 0 = read immediately without waiting. Returns { approvalGate, pending } (pending=true → still unresolved after the wait).',
119
+ description:
120
+ 'Block until an approval gate resolves (the human approves/rejects it in the browser) or the timeout elapses — primitive D, gates that actually gate. timeoutMs 0 = read immediately without waiting. Returns { approvalGate, pending } (pending=true → still unresolved after the wait).',
114
121
  extraShape: awaitMcpExtraShape,
115
122
  buildInput: buildAwaitInput,
116
123
  formatResult: (result) => {
@@ -118,7 +125,9 @@ const axApprovalGetOperation = defineOperation<z.infer<typeof awaitInputSchema>,
118
125
  const approvalGate = (body.approvalGate ?? null) as PmxAxApprovalGate | null;
119
126
  const pending = body.pending === true;
120
127
  return {
121
- content: [{ type: 'text' as const, text: JSON.stringify({ ok: approvalGate !== null, approvalGate, pending }) }],
128
+ content: [
129
+ { type: 'text' as const, text: JSON.stringify({ ok: approvalGate !== null, approvalGate, pending }) },
130
+ ],
122
131
  };
123
132
  },
124
133
  },
@@ -148,7 +157,8 @@ const axElicitationGetOperation = defineOperation<z.infer<typeof awaitInputSchem
148
157
  },
149
158
  mcp: {
150
159
  toolName: 'canvas_await_elicitation',
151
- description: 'Block until an elicitation is answered (the human responds in the browser) or the timeout elapses — primitive D. timeoutMs 0 = read immediately. Returns { elicitation, pending }.',
160
+ description:
161
+ 'Block until an elicitation is answered (the human responds in the browser) or the timeout elapses — primitive D. timeoutMs 0 = read immediately. Returns { elicitation, pending }.',
152
162
  extraShape: awaitMcpExtraShape,
153
163
  buildInput: buildAwaitInput,
154
164
  formatResult: (result) => {
@@ -186,7 +196,8 @@ const axModeGetOperation = defineOperation<z.infer<typeof awaitInputSchema>, Rec
186
196
  },
187
197
  mcp: {
188
198
  toolName: 'canvas_await_mode',
189
- description: 'Block until a mode request resolves (approved/rejected in the browser) or the timeout elapses — primitive D. timeoutMs 0 = read immediately. Returns { modeRequest, pending }.',
199
+ description:
200
+ 'Block until a mode request resolves (approved/rejected in the browser) or the timeout elapses — primitive D. timeoutMs 0 = read immediately. Returns { modeRequest, pending }.',
190
201
  extraShape: awaitMcpExtraShape,
191
202
  buildInput: buildAwaitInput,
192
203
  formatResult: (result) => {
@@ -209,8 +220,4 @@ const axModeGetOperation = defineOperation<z.infer<typeof awaitInputSchema>, Rec
209
220
  },
210
221
  });
211
222
 
212
- export const axAwaitOperations: Operation[] = [
213
- axApprovalGetOperation,
214
- axElicitationGetOperation,
215
- axModeGetOperation,
216
- ];
223
+ export const axAwaitOperations: Operation[] = [axApprovalGetOperation, axElicitationGetOperation, axModeGetOperation];
@@ -0,0 +1,347 @@
1
+ /**
2
+ * AX read + wire-compat operations (plan-009 C1 slice 2): the GET read
3
+ * surface (`ax/work`, `ax/approval`, `ax/review`, `ax/elicitation`,
4
+ * `ax/mode`, `ax/command`, `ax/policy`, `ax/host-capability`, `ax/context`,
5
+ * `ax/surface-snapshot`, `pinned-context`, `code-graph`) plus the three
6
+ * remaining AX writes (`ax/activity` ingest, `ax/interaction` submit, and
7
+ * the legacy `PATCH /api/canvas/ax` focus shape). Wire envelopes are
8
+ * byte-identical to the legacy server.ts handlers they replace. HTTP-only:
9
+ * the MCP read surface stays the composites/resources — no new tools, the
10
+ * frozen 27-tool surface is untouched.
11
+ *
12
+ * This module must never import server.ts or index.ts.
13
+ */
14
+ import { z } from 'zod';
15
+ import { buildAgentContextPreamble, serializeNodeForAgentContext } from '../../agent-context.js';
16
+ import { buildCanvasAxContext, buildCanvasAxSurfaceSnapshot } from '../../ax-context.js';
17
+ import { applyAxInteraction } from '../../ax-interaction.js';
18
+ import {
19
+ isAxActivityKind,
20
+ isAxEvidenceKind,
21
+ type PmxAxEvidenceKind,
22
+ type PmxAxReviewAnchorType,
23
+ type PmxAxReviewKind,
24
+ type PmxAxReviewSeverity,
25
+ type PmxAxWorkItemStatus,
26
+ } from '../../ax-state.js';
27
+ import { canvasState, type CanvasNodeState } from '../../canvas-state.js';
28
+ import { buildCodeGraphSummary } from '../../code-graph.js';
29
+ import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
30
+ import { normalizeAxNodeIds, normalizeAxSource } from './ax-shared.js';
31
+ import { isRecord } from './nodes.js';
32
+
33
+ // ── Activity-reaction validation (moved from server.ts) ───────
34
+
35
+ const AX_WORK_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
36
+
37
+ function normalizeAxWorkItemStatus(value: unknown): PmxAxWorkItemStatus | undefined {
38
+ return typeof value === 'string' && AX_WORK_STATUSES.has(value) ? (value as PmxAxWorkItemStatus) : undefined;
39
+ }
40
+
41
+ function isReviewSeverity(v: unknown): v is PmxAxReviewSeverity {
42
+ return v === 'info' || v === 'warning' || v === 'error';
43
+ }
44
+ function isReviewKind(v: unknown): v is PmxAxReviewKind {
45
+ return v === 'comment' || v === 'finding';
46
+ }
47
+ function isReviewAnchor(v: unknown): v is PmxAxReviewAnchorType {
48
+ return v === 'node' || v === 'file' || v === 'region';
49
+ }
50
+
51
+ // Validate untrusted activity `reactions` from an HTTP body into the typed override
52
+ // shape ingestActivity expects. `false` suppresses a default reaction; an object
53
+ // overrides its fields (invalid fields are dropped, not stored raw).
54
+ function normalizeActivityReactions(input: Record<string, unknown>): {
55
+ workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
56
+ evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
57
+ review?:
58
+ | false
59
+ | {
60
+ severity?: PmxAxReviewSeverity;
61
+ kind?: PmxAxReviewKind;
62
+ anchorType?: PmxAxReviewAnchorType;
63
+ nodeId?: string | null;
64
+ };
65
+ } {
66
+ const out: ReturnType<typeof normalizeActivityReactions> = {};
67
+ if (input.workItem === false) out.workItem = false;
68
+ else if (isRecord(input.workItem)) {
69
+ const status = normalizeAxWorkItemStatus(input.workItem.status);
70
+ out.workItem = {
71
+ ...(status ? { status } : {}),
72
+ ...(typeof input.workItem.detail === 'string' ? { detail: input.workItem.detail } : {}),
73
+ };
74
+ }
75
+ if (input.evidence === false) out.evidence = false;
76
+ else if (isRecord(input.evidence)) {
77
+ out.evidence = {
78
+ ...(isAxEvidenceKind(input.evidence.kind) ? { kind: input.evidence.kind } : {}),
79
+ ...(typeof input.evidence.body === 'string' ? { body: input.evidence.body } : {}),
80
+ };
81
+ }
82
+ if (input.review === false) out.review = false;
83
+ else if (isRecord(input.review)) {
84
+ out.review = {
85
+ ...(isReviewSeverity(input.review.severity) ? { severity: input.review.severity } : {}),
86
+ ...(isReviewKind(input.review.kind) ? { kind: input.review.kind } : {}),
87
+ ...(isReviewAnchor(input.review.anchorType) ? { anchorType: input.review.anchorType } : {}),
88
+ ...(typeof input.review.nodeId === 'string' ? { nodeId: input.review.nodeId } : {}),
89
+ };
90
+ }
91
+ return out;
92
+ }
93
+
94
+ // ── GET list/read factory ─────────────────────────────────────
95
+
96
+ const emptyShape = {};
97
+ const emptySchema = z.looseObject(emptyShape);
98
+
99
+ /** A no-input GET whose body is `{ ok: true, ...read() }` (legacy list shape). */
100
+ function defineAxListOperation(name: string, path: string, read: () => Record<string, unknown>): Operation {
101
+ return defineOperation<z.infer<typeof emptySchema>, Record<string, unknown>>({
102
+ name,
103
+ mutates: false,
104
+ input: emptySchema,
105
+ inputShape: emptyShape,
106
+ http: {
107
+ method: 'GET',
108
+ path,
109
+ },
110
+ handler: () => ({ ok: true, ...read() }),
111
+ });
112
+ }
113
+
114
+ // ── ax.context.get / ax.surface-snapshot.get ──────────────────
115
+
116
+ const axContextShape = {
117
+ consumer: z.unknown().optional().describe('Optional consumer label filtering the pending-delivery lead block'),
118
+ };
119
+ const axContextSchema = z.looseObject(axContextShape);
120
+
121
+ const axContextGetOperation = defineOperation<z.infer<typeof axContextSchema>, Record<string, unknown>>({
122
+ name: 'ax.context.get',
123
+ mutates: false,
124
+ input: axContextSchema,
125
+ inputShape: axContextShape,
126
+ http: {
127
+ method: 'GET',
128
+ path: '/api/canvas/ax/context',
129
+ },
130
+ // Optional ?consumer= filters the compact `delivery` lead block (loop-safe — a
131
+ // consumer never sees steering/activity it originated), so a host adapter can
132
+ // inject its own un-truncated pending block per turn (report #54 hardening).
133
+ handler: (input) => {
134
+ const consumer = typeof input.consumer === 'string' ? input.consumer : undefined;
135
+ return buildCanvasAxContext(consumer) as unknown as Record<string, unknown>;
136
+ },
137
+ });
138
+
139
+ const axSurfaceSnapshotOperation = defineOperation<z.infer<typeof emptySchema>, Record<string, unknown>>({
140
+ name: 'ax.surface-snapshot.get',
141
+ mutates: false,
142
+ input: emptySchema,
143
+ inputShape: emptyShape,
144
+ http: {
145
+ method: 'GET',
146
+ path: '/api/canvas/ax/surface-snapshot',
147
+ },
148
+ // Compact AX state for surfaces (the same shape seeded into AX-enabled iframes).
149
+ // The client fetches this and pushes it to surfaces over the ax-update channel.
150
+ handler: () => buildCanvasAxSurfaceSnapshot() as unknown as Record<string, unknown>,
151
+ });
152
+
153
+ // ── pinned-context.get ────────────────────────────────────────
154
+
155
+ const pinnedContextOperation = defineOperation<z.infer<typeof emptySchema>, Record<string, unknown>>({
156
+ name: 'pinned-context.get',
157
+ mutates: false,
158
+ input: emptySchema,
159
+ inputShape: emptyShape,
160
+ http: {
161
+ method: 'GET',
162
+ path: '/api/canvas/pinned-context',
163
+ },
164
+ handler: () => {
165
+ const pinnedIds = Array.from(canvasState.contextPinnedNodeIds);
166
+ const nodes = pinnedIds
167
+ .map((id) => canvasState.getNode(id))
168
+ .filter((node): node is CanvasNodeState => node !== undefined);
169
+ const preamble =
170
+ pinnedIds.length > 0 ? buildAgentContextPreamble(nodes, { defaultTextLength: 700, webpageTextLength: 1600 }) : '';
171
+ const serialized = nodes.map((node) =>
172
+ serializeNodeForAgentContext(node, {
173
+ defaultTextLength: 700,
174
+ webpageTextLength: 1600,
175
+ includePosition: true,
176
+ }),
177
+ );
178
+ return { preamble, nodeIds: pinnedIds, count: pinnedIds.length, nodes: serialized };
179
+ },
180
+ });
181
+
182
+ // ── code-graph.get ────────────────────────────────────────────
183
+
184
+ const codeGraphOperation = defineOperation<z.infer<typeof emptySchema>, Record<string, unknown>>({
185
+ name: 'code-graph.get',
186
+ mutates: false,
187
+ input: emptySchema,
188
+ inputShape: emptyShape,
189
+ http: {
190
+ method: 'GET',
191
+ path: '/api/canvas/code-graph',
192
+ },
193
+ handler: () => buildCodeGraphSummary() as unknown as Record<string, unknown>,
194
+ });
195
+
196
+ // ── ax.activity.ingest ────────────────────────────────────────
197
+
198
+ const activityShape = {
199
+ kind: z
200
+ .unknown()
201
+ .optional()
202
+ .describe('Activity kind: tool-start, tool-result, failure, error, session-start, session-end, command, note'),
203
+ title: z.unknown().optional().describe('Activity title'),
204
+ summary: z.unknown().optional(),
205
+ outcome: z.unknown().optional().describe('success or failure'),
206
+ ref: z.unknown().optional().describe('File path, URL, or commit the activity refers to'),
207
+ nodeIds: z.unknown().optional(),
208
+ data: z.unknown().optional(),
209
+ reactions: z.unknown().optional().describe('Override or suppress the kind-driven default reactions'),
210
+ source: z.unknown().optional(),
211
+ };
212
+ const activitySchema = z.looseObject(activityShape);
213
+
214
+ // Report primitive A: ingest a harness-forwarded agent activity; the board auto-reacts.
215
+ const activityIngestOperation = defineOperation<z.infer<typeof activitySchema>, Record<string, unknown>>({
216
+ name: 'ax.activity.ingest',
217
+ mutates: false,
218
+ input: activitySchema,
219
+ inputShape: activityShape,
220
+ http: {
221
+ method: 'POST',
222
+ path: '/api/canvas/ax/activity',
223
+ },
224
+ handler: (input, ctx: OperationContext) => {
225
+ const body: Record<string, unknown> = input;
226
+ if (!isAxActivityKind(body.kind)) {
227
+ throw new OperationError(
228
+ "activity requires a valid 'kind': one of tool-start, tool-result, failure, error, session-start, session-end, command, note.",
229
+ );
230
+ }
231
+ if (typeof body.title !== 'string' || !body.title.trim()) {
232
+ throw new OperationError('activity requires a title.');
233
+ }
234
+ const result = canvasState.ingestActivity(
235
+ {
236
+ kind: body.kind,
237
+ title: body.title,
238
+ ...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
239
+ ...(body.outcome === 'success' || body.outcome === 'failure' ? { outcome: body.outcome } : {}),
240
+ ...(typeof body.ref === 'string' ? { ref: body.ref } : {}),
241
+ ...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
242
+ ...(isRecord(body.data) ? { data: body.data } : {}),
243
+ ...(isRecord(body.reactions) ? { reactions: normalizeActivityReactions(body.reactions) } : {}),
244
+ },
245
+ { source: normalizeAxSource(body.source, 'api') },
246
+ );
247
+ ctx.emit('ax-event-created', { event: result.event });
248
+ if (result.workItem) ctx.emit('ax-state-changed', { workItem: result.workItem });
249
+ if (result.evidence) ctx.emit('ax-event-created', { evidence: result.evidence });
250
+ if (result.review) ctx.emit('ax-state-changed', { reviewAnnotation: result.review });
251
+ return { ok: true, ...result };
252
+ },
253
+ });
254
+
255
+ // ── ax.interaction.submit ─────────────────────────────────────
256
+
257
+ const interactionShape = {
258
+ type: z.unknown().optional().describe('Interaction type, e.g. ax.work.create'),
259
+ sourceNodeId: z.unknown().optional(),
260
+ payload: z.unknown().optional(),
261
+ sourceSurface: z.unknown().optional(),
262
+ correlationId: z.unknown().optional(),
263
+ source: z.unknown().optional(),
264
+ };
265
+ const interactionSchema = z.looseObject(interactionShape);
266
+
267
+ const interactionOperation = defineOperation<z.infer<typeof interactionSchema>, Record<string, unknown>>({
268
+ name: 'ax.interaction.submit',
269
+ mutates: false,
270
+ input: interactionSchema,
271
+ inputShape: interactionShape,
272
+ http: {
273
+ method: 'POST',
274
+ path: '/api/canvas/ax/interaction',
275
+ // Legacy wire: 200 when accepted, the envelope's own status when refused.
276
+ status: (result) =>
277
+ isRecord(result) && result.ok !== true && typeof result.status === 'number' ? result.status : 200,
278
+ },
279
+ handler: (input, ctx: OperationContext) => {
280
+ const body: Record<string, unknown> = input;
281
+ const { result, events } = applyAxInteraction(canvasState, body, normalizeAxSource(body.source, 'api'));
282
+ for (const e of events) {
283
+ ctx.emit(e.event, e.payload);
284
+ }
285
+ return result as unknown as Record<string, unknown>;
286
+ },
287
+ });
288
+
289
+ // ── ax.state.patch (legacy PATCH /api/canvas/ax focus shape) ──
290
+
291
+ const statePatchShape = {
292
+ focus: z.unknown().optional().describe('Focus object: { nodeIds, source? }'),
293
+ };
294
+ const statePatchSchema = z.looseObject(statePatchShape);
295
+
296
+ const statePatchOperation = defineOperation<z.infer<typeof statePatchSchema>, Record<string, unknown>>({
297
+ name: 'ax.state.patch',
298
+ mutates: false,
299
+ input: statePatchSchema,
300
+ inputShape: statePatchShape,
301
+ http: {
302
+ method: 'PATCH',
303
+ path: '/api/canvas/ax',
304
+ },
305
+ handler: (input, ctx: OperationContext) => {
306
+ const body: Record<string, unknown> = input;
307
+ if (!body.focus || typeof body.focus !== 'object' || Array.isArray(body.focus)) {
308
+ throw new OperationError('PATCH /api/canvas/ax currently requires a focus object.');
309
+ }
310
+ const focusInput = body.focus as Record<string, unknown>;
311
+ const focus = canvasState.setAxFocus(normalizeAxNodeIds(focusInput.nodeIds), {
312
+ source: normalizeAxSource(focusInput.source, 'api'),
313
+ });
314
+ ctx.emit('ax-state-changed', { focus });
315
+ return { ok: true, state: canvasState.getAxState() };
316
+ },
317
+ });
318
+
319
+ export const axReadOperations: Operation[] = [
320
+ defineAxListOperation('ax.work.list', '/api/canvas/ax/work', () => ({ workItems: canvasState.getWorkItems() })),
321
+ defineAxListOperation('ax.approval.list', '/api/canvas/ax/approval', () => ({
322
+ approvalGates: canvasState.getApprovalGates(),
323
+ })),
324
+ defineAxListOperation('ax.review.list', '/api/canvas/ax/review', () => ({
325
+ reviewAnnotations: canvasState.getReviewAnnotations(),
326
+ })),
327
+ defineAxListOperation('ax.elicitation.list', '/api/canvas/ax/elicitation', () => ({
328
+ elicitations: canvasState.getElicitations(),
329
+ })),
330
+ defineAxListOperation('ax.mode.list', '/api/canvas/ax/mode', () => ({
331
+ modeRequests: canvasState.getModeRequests(),
332
+ })),
333
+ defineAxListOperation('ax.command.list', '/api/canvas/ax/command', () => ({
334
+ commands: canvasState.getCommandRegistry(),
335
+ })),
336
+ defineAxListOperation('ax.policy.get', '/api/canvas/ax/policy', () => ({ policy: canvasState.getPolicy() })),
337
+ defineAxListOperation('ax.host-capability.get', '/api/canvas/ax/host-capability', () => ({
338
+ host: canvasState.getHostCapability(),
339
+ })),
340
+ axContextGetOperation,
341
+ axSurfaceSnapshotOperation,
342
+ pinnedContextOperation,
343
+ codeGraphOperation,
344
+ activityIngestOperation,
345
+ interactionOperation,
346
+ statePatchOperation,
347
+ ];