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
@@ -9,12 +9,7 @@
9
9
  * This module must never import server.ts or index.ts.
10
10
  */
11
11
  import { z } from 'zod';
12
- import {
13
- canvasState,
14
- type CanvasAnnotation,
15
- type CanvasLayout,
16
- type CanvasNodeState,
17
- } from '../../canvas-state.js';
12
+ import { canvasState, type CanvasAnnotation, type CanvasLayout, type CanvasNodeState } from '../../canvas-state.js';
18
13
  import {
19
14
  addCanvasNode,
20
15
  createCanvasGroup,
@@ -122,18 +117,22 @@ function normalizeGeometryInput(body: Record<string, unknown>): {
122
117
  ...(pickFiniteNumber(body, 'y') !== undefined ? { y: pickFiniteNumber(body, 'y') } : {}),
123
118
  ...(pickFiniteNumber(body, 'width') !== undefined ? { width: pickFiniteNumber(body, 'width') } : {}),
124
119
  ...(pickFiniteNumber(body, 'height') !== undefined ? { height: pickFiniteNumber(body, 'height') } : {}),
125
- ...(position ? {
126
- position: {
127
- ...(pickFiniteNumber(position, 'x') !== undefined ? { x: pickFiniteNumber(position, 'x') } : {}),
128
- ...(pickFiniteNumber(position, 'y') !== undefined ? { y: pickFiniteNumber(position, 'y') } : {}),
129
- },
130
- } : {}),
131
- ...(size ? {
132
- size: {
133
- ...(pickFiniteNumber(size, 'width') !== undefined ? { width: pickFiniteNumber(size, 'width') } : {}),
134
- ...(pickFiniteNumber(size, 'height') !== undefined ? { height: pickFiniteNumber(size, 'height') } : {}),
135
- },
136
- } : {}),
120
+ ...(position
121
+ ? {
122
+ position: {
123
+ ...(pickFiniteNumber(position, 'x') !== undefined ? { x: pickFiniteNumber(position, 'x') } : {}),
124
+ ...(pickFiniteNumber(position, 'y') !== undefined ? { y: pickFiniteNumber(position, 'y') } : {}),
125
+ },
126
+ }
127
+ : {}),
128
+ ...(size
129
+ ? {
130
+ size: {
131
+ ...(pickFiniteNumber(size, 'width') !== undefined ? { width: pickFiniteNumber(size, 'width') } : {}),
132
+ ...(pickFiniteNumber(size, 'height') !== undefined ? { height: pickFiniteNumber(size, 'height') } : {}),
133
+ },
134
+ }
135
+ : {}),
137
136
  };
138
137
  }
139
138
 
@@ -217,8 +216,9 @@ export function setGroupChildrenFromApi(groupId: string, childIds: string[]): bo
217
216
  const dataChildIds = Array.isArray(group.data.children)
218
217
  ? group.data.children.filter((id): id is string => typeof id === 'string')
219
218
  : [];
220
- const parentBackrefIds = canvasState.getLayout().nodes
221
- .filter((node) => node.id !== groupId && node.data.parentGroup === groupId)
219
+ const parentBackrefIds = canvasState
220
+ .getLayout()
221
+ .nodes.filter((node) => node.id !== groupId && node.data.parentGroup === groupId)
222
222
  .map((node) => node.id);
223
223
  const currentChildIds = [...new Set([...dataChildIds, ...parentBackrefIds])];
224
224
  if (currentChildIds.length > 0) {
@@ -310,7 +310,9 @@ export function buildSummaryFromLayout(layout: CanvasLayout, pinnedIds: string[]
310
310
  totalNodes: layout.nodes.length,
311
311
  totalEdges: layout.edges.length,
312
312
  totalAnnotations: (layout.annotations ?? []).length,
313
- annotations: (layout.annotations ?? []).map((annotation: CanvasAnnotation) => summarizeCanvasAnnotationForContext(annotation, layout.nodes)),
313
+ annotations: (layout.annotations ?? []).map((annotation: CanvasAnnotation) =>
314
+ summarizeCanvasAnnotationForContext(annotation, layout.nodes),
315
+ ),
314
316
  nodesByType,
315
317
  pinnedCount: pinned.size,
316
318
  pinnedTitles,
@@ -322,8 +324,12 @@ export function compactLayoutPayload(layout: CanvasLayout, pinnedIds: string[]):
322
324
  return {
323
325
  summary: buildSummaryFromLayout(layout, pinnedIds),
324
326
  viewport: layout.viewport,
325
- annotations: (layout.annotations ?? []).map((annotation) => summarizeCanvasAnnotationForContext(annotation, layout.nodes)),
326
- nodes: layout.nodes.map((node) => compactNodePayload(node)).filter((node): node is Record<string, unknown> => node !== null),
327
+ annotations: (layout.annotations ?? []).map((annotation) =>
328
+ summarizeCanvasAnnotationForContext(annotation, layout.nodes),
329
+ ),
330
+ nodes: layout.nodes
331
+ .map((node) => compactNodePayload(node))
332
+ .filter((node): node is Record<string, unknown> => node !== null),
327
333
  edges: layout.edges.map((edge) => ({
328
334
  id: edge.id,
329
335
  from: edge.from,
@@ -339,7 +345,9 @@ export function compactLayoutPayload(layout: CanvasLayout, pinnedIds: string[]):
339
345
  export function agentSafeFullLayoutPayload(layout: CanvasLayout): Record<string, unknown> {
340
346
  return {
341
347
  ...serializeCanvasLayoutForAgent(layout),
342
- annotations: (layout.annotations ?? []).map((annotation) => summarizeCanvasAnnotationForContext(annotation, layout.nodes)),
348
+ annotations: (layout.annotations ?? []).map((annotation) =>
349
+ summarizeCanvasAnnotationForContext(annotation, layout.nodes),
350
+ ),
343
351
  };
344
352
  }
345
353
 
@@ -348,7 +356,10 @@ export function agentSafeFullLayoutPayload(layout: CanvasLayout): Record<string,
348
356
  * agents using either key (or a cached schema) work — matching the
349
357
  * external-app / web-artifact responses that already return both.
350
358
  */
351
- export function createdNodePayloadFromNode(node: CanvasNodeState, options: Record<string, unknown> = {}): Record<string, unknown> {
359
+ export function createdNodePayloadFromNode(
360
+ node: CanvasNodeState,
361
+ options: Record<string, unknown> = {},
362
+ ): Record<string, unknown> {
352
363
  if (!wantsFullPayload(options)) {
353
364
  return { ok: true, node: compactNodePayload(node), id: node.id, nodeId: node.id };
354
365
  }
@@ -377,28 +388,28 @@ export function createBasicCanvasNode(
377
388
  throw new OperationError('HTML node field "data.html" must be a string.');
378
389
  }
379
390
  }
380
- const content = type === 'image' && typeof body.path === 'string' && typeof body.content !== 'string'
381
- ? body.path
382
- : body.content;
391
+ const content =
392
+ type === 'image' && typeof body.path === 'string' && typeof body.content !== 'string' ? body.path : body.content;
383
393
  // For html nodes, accept top-level `html` AND `axCapabilities` and merge into data
384
394
  // so callers can POST { type: 'html', title, html, axCapabilities } without nesting
385
395
  // under `data` (report #53 — transport parity with MCP canvas_add_html_node). A
386
396
  // top-level value overrides the same key under `data` (mirrors the `html` precedence).
387
397
  const topAxCapabilities = type === 'html' ? normalizeNodeAxCapabilities(body.axCapabilities) : null;
388
- const htmlMergedData = type === 'html'
389
- ? {
390
- ...(extraData ?? {}),
391
- ...(typeof body.html === 'string' ? { html: resolveHtmlContent(body.html) } : {}),
392
- ...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
393
- ...(typeof body.agentSummary === 'string' ? { agentSummary: body.agentSummary } : {}),
394
- ...(typeof body.description === 'string' ? { description: body.description } : {}),
395
- ...(body.presentation === true ? { presentation: true } : {}),
396
- ...(Array.isArray(body.slideTitles) ? { slideTitles: body.slideTitles } : {}),
397
- ...(Array.isArray(body.embeddedNodeIds) ? { embeddedNodeIds: body.embeddedNodeIds } : {}),
398
- ...(Array.isArray(body.embeddedUrls) ? { embeddedUrls: body.embeddedUrls } : {}),
399
- ...(topAxCapabilities ? { axCapabilities: topAxCapabilities } : {}),
400
- }
401
- : extraData;
398
+ const htmlMergedData =
399
+ type === 'html'
400
+ ? {
401
+ ...(extraData ?? {}),
402
+ ...(typeof body.html === 'string' ? { html: resolveHtmlContent(body.html) } : {}),
403
+ ...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
404
+ ...(typeof body.agentSummary === 'string' ? { agentSummary: body.agentSummary } : {}),
405
+ ...(typeof body.description === 'string' ? { description: body.description } : {}),
406
+ ...(body.presentation === true ? { presentation: true } : {}),
407
+ ...(Array.isArray(body.slideTitles) ? { slideTitles: body.slideTitles } : {}),
408
+ ...(Array.isArray(body.embeddedNodeIds) ? { embeddedNodeIds: body.embeddedNodeIds } : {}),
409
+ ...(Array.isArray(body.embeddedUrls) ? { embeddedUrls: body.embeddedUrls } : {}),
410
+ ...(topAxCapabilities ? { axCapabilities: topAxCapabilities } : {}),
411
+ }
412
+ : extraData;
402
413
  const geometry = resolveCreateGeometry(body);
403
414
  const defaults = defaultNodeSize(type);
404
415
  try {
@@ -494,11 +505,12 @@ export function buildNodePatch(
494
505
  const patchAxCapabilities = normalizeNodeAxCapabilities(body.axCapabilities);
495
506
  if (patchAxCapabilities) data.axCapabilities = patchAxCapabilities;
496
507
  if (existing.type === 'webpage') {
497
- const nextUrl = typeof body.url === 'string'
498
- ? body.url
499
- : typeof (body.data as Record<string, unknown> | undefined)?.url === 'string'
500
- ? (body.data as Record<string, unknown>).url as string
501
- : undefined;
508
+ const nextUrl =
509
+ typeof body.url === 'string'
510
+ ? body.url
511
+ : typeof (body.data as Record<string, unknown> | undefined)?.url === 'string'
512
+ ? ((body.data as Record<string, unknown>).url as string)
513
+ : undefined;
502
514
  if (typeof nextUrl === 'string' && nextUrl.trim().length > 0) {
503
515
  try {
504
516
  data.url = normalizeWebpageUrl(nextUrl);
@@ -507,9 +519,7 @@ export function buildNodePatch(
507
519
  }
508
520
  }
509
521
  }
510
- patch.data = existing.type === 'trace'
511
- ? mergeTraceNodeDataFields(data, body)
512
- : data;
522
+ patch.data = existing.type === 'trace' ? mergeTraceNodeDataFields(data, body) : data;
513
523
  }
514
524
  const error = validateCanvasNodePatch({
515
525
  ...(patch.position ? { position: patch.position as { x: number; y: number } } : {}),
@@ -543,11 +553,12 @@ interface NodeAddResult {
543
553
  }
544
554
 
545
555
  async function createWebpageNode(body: Record<string, unknown>, ctx: OperationContext): Promise<NodeAddResult> {
546
- const rawUrl = typeof body.url === 'string' && body.url.trim().length > 0
547
- ? body.url
548
- : typeof body.content === 'string'
549
- ? body.content
550
- : '';
556
+ const rawUrl =
557
+ typeof body.url === 'string' && body.url.trim().length > 0
558
+ ? body.url
559
+ : typeof body.content === 'string'
560
+ ? body.content
561
+ : '';
551
562
 
552
563
  let normalizedUrl: string;
553
564
  try {
@@ -577,9 +588,7 @@ async function createWebpageNode(body: Record<string, unknown>, ctx: OperationCo
577
588
  return {
578
589
  node: created,
579
590
  extras: {
580
- fetch: refreshed.ok
581
- ? { ok: true }
582
- : { ok: false, error: refreshed.error ?? 'Failed to fetch webpage content.' },
591
+ fetch: refreshed.ok ? { ok: true } : { ok: false, error: refreshed.error ?? 'Failed to fetch webpage content.' },
583
592
  ...(refreshed.ok ? {} : { error: refreshed.error }),
584
593
  },
585
594
  };
@@ -639,9 +648,10 @@ function createGroupNode(body: Record<string, unknown>): NodeAddResult {
639
648
  const childIds = childList.value ?? [];
640
649
  const childError = validateGroupChildIds('', childIds);
641
650
  if (childError) throw new OperationError(`Cannot create group: ${childError}`);
642
- const childLayout = body.childLayout === 'grid' || body.childLayout === 'column' || body.childLayout === 'flow'
643
- ? body.childLayout
644
- : undefined;
651
+ const childLayout =
652
+ body.childLayout === 'grid' || body.childLayout === 'column' || body.childLayout === 'flow'
653
+ ? body.childLayout
654
+ : undefined;
645
655
  const { node } = createCanvasGroup({
646
656
  ...(typeof body.title === 'string' ? { title: body.title } : {}),
647
657
  childIds,
@@ -653,20 +663,54 @@ function createGroupNode(body: Record<string, unknown>): NodeAddResult {
653
663
  }
654
664
 
655
665
  const nodeAddShape = {
656
- intentId: z.string().optional().catch(undefined).describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
666
+ intentId: z
667
+ .string()
668
+ .optional()
669
+ .catch(undefined)
670
+ .describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
657
671
  type: z.string().optional().catch(undefined).describe('Node type (prefer canvas_group {action:"create"} for groups)'),
658
672
  title: z.string().optional().catch(undefined).describe('Node title'),
659
- content: z.string().optional().catch(undefined).describe('Node content (markdown for markdown nodes, file path for file nodes, image path/URL/data-URI for image nodes, URL for webpage nodes)'),
660
- path: z.string().optional().catch(undefined).describe('Compatibility alias for image node content. Prefer content for image paths.'),
661
- url: z.string().optional().catch(undefined).describe('Canonical webpage URL field for webpage nodes. Overrides content when both are provided.'),
673
+ content: z
674
+ .string()
675
+ .optional()
676
+ .catch(undefined)
677
+ .describe(
678
+ 'Node content (markdown for markdown nodes, file path for file nodes, image path/URL/data-URI for image nodes, URL for webpage nodes)',
679
+ ),
680
+ path: z
681
+ .string()
682
+ .optional()
683
+ .catch(undefined)
684
+ .describe('Compatibility alias for image node content. Prefer content for image paths.'),
685
+ url: z
686
+ .string()
687
+ .optional()
688
+ .catch(undefined)
689
+ .describe('Canonical webpage URL field for webpage nodes. Overrides content when both are provided.'),
662
690
  x: z.number().optional().catch(undefined).describe('X position (auto-placed if omitted)'),
663
691
  y: z.number().optional().catch(undefined).describe('Y position (auto-placed if omitted)'),
664
692
  width: z.number().optional().catch(undefined).describe('Width in pixels (default: 720)'),
665
693
  height: z.number().optional().catch(undefined).describe('Height in pixels (default: 600)'),
666
- strictSize: z.boolean().optional().catch(undefined).describe('Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting'),
667
- children: z.unknown().optional().describe('Group-only alias for childIds. Node IDs to include in a generic group node.'),
668
- childIds: z.unknown().optional().describe('Group-only field. Node IDs to include in a generic group node. Prefer canvas_group {action:"create"} for groups.'),
669
- childLayout: z.enum(['grid', 'column', 'flow']).optional().catch(undefined).describe('Group-only optional layout for grouped children.'),
694
+ strictSize: z
695
+ .boolean()
696
+ .optional()
697
+ .catch(undefined)
698
+ .describe('Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting'),
699
+ children: z
700
+ .unknown()
701
+ .optional()
702
+ .describe('Group-only alias for childIds. Node IDs to include in a generic group node.'),
703
+ childIds: z
704
+ .unknown()
705
+ .optional()
706
+ .describe(
707
+ 'Group-only field. Node IDs to include in a generic group node. Prefer canvas_group {action:"create"} for groups.',
708
+ ),
709
+ childLayout: z
710
+ .enum(['grid', 'column', 'flow'])
711
+ .optional()
712
+ .catch(undefined)
713
+ .describe('Group-only optional layout for grouped children.'),
670
714
  color: z.string().optional().catch(undefined).describe('Group-only frame accent color.'),
671
715
  toolName: z.string().optional().catch(undefined).describe('Trace node tool or operation label'),
672
716
  category: z.string().optional().catch(undefined).describe('Trace node category: mcp, file, subagent, or other'),
@@ -680,12 +724,25 @@ const nodeAddShape = {
680
724
  kind: z.unknown().optional().describe('Alias for primitive.'),
681
725
  summary: z.string().optional().catch(undefined).describe('Agent-readable semantic summary (html nodes).'),
682
726
  agentSummary: z.string().optional().catch(undefined).describe('Explicit agent-readable summary (html nodes).'),
683
- description: z.string().optional().catch(undefined).describe('Short description included in search and pinned/spatial context (html nodes).'),
684
- presentation: z.boolean().optional().catch(undefined).describe('Marks an html node as a fullscreen presentation/deck.'),
727
+ description: z
728
+ .string()
729
+ .optional()
730
+ .catch(undefined)
731
+ .describe('Short description included in search and pinned/spatial context (html nodes).'),
732
+ presentation: z
733
+ .boolean()
734
+ .optional()
735
+ .catch(undefined)
736
+ .describe('Marks an html node as a fullscreen presentation/deck.'),
685
737
  slideTitles: z.unknown().optional().describe('Agent-readable slide titles for presentation HTML.'),
686
738
  embeddedNodeIds: z.unknown().optional().describe('Canvas node IDs embedded or represented by this HTML surface.'),
687
739
  embeddedUrls: z.unknown().optional().describe('URLs embedded or represented by this HTML surface.'),
688
- axCapabilities: z.unknown().optional().describe('Opt an html node into AX interactions. Merged into node data for html nodes; clamped to the node-type ceiling server-side.'),
740
+ axCapabilities: z
741
+ .unknown()
742
+ .optional()
743
+ .describe(
744
+ 'Opt an html node into AX interactions. Merged into node data for html nodes; clamped to the node-type ceiling server-side.',
745
+ ),
689
746
  position: z.unknown().optional().describe('Geometry alias: { x, y } object form.'),
690
747
  size: z.unknown().optional().describe('Geometry alias: { width, height } object form.'),
691
748
  };
@@ -693,7 +750,10 @@ const nodeAddShape = {
693
750
  const nodeAddSchema = z.looseObject(nodeAddShape);
694
751
 
695
752
  const fullVerboseShape = {
696
- full: z.boolean().optional().describe('Return the full created node payload. Default false returns compact metadata.'),
753
+ full: z
754
+ .boolean()
755
+ .optional()
756
+ .describe('Return the full created node payload. Default false returns compact metadata.'),
697
757
  verbose: z.boolean().optional().describe('Alias for full:true.'),
698
758
  };
699
759
 
@@ -708,22 +768,50 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
708
768
  },
709
769
  mcp: {
710
770
  toolName: 'canvas_add_node',
711
- description: 'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, image, webpage, mcp-app, html, group. Dedicated routes: json-render -> canvas_render {action:"add-json-render"}, graph -> canvas_render {action:"add-graph"}, web-artifact -> canvas_app {action:"build-artifact"}, external apps -> canvas_app {action:"open-mcp-app"}, groups -> canvas_group {action:"create"}. Call canvas_render {action:"describe-schema"} for the full nodeTypeRouting table.',
771
+ description:
772
+ 'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, image, webpage, mcp-app, html, group. Dedicated routes: json-render -> canvas_render {action:"add-json-render"}, graph -> canvas_render {action:"add-graph"}, web-artifact -> canvas_app {action:"build-artifact"}, external apps -> canvas_app {action:"open-mcp-app"}, groups -> canvas_group {action:"create"}. Call canvas_render {action:"describe-schema"} for the full nodeTypeRouting table.',
712
773
  extraShape: {
713
- type: z.enum(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'webpage', 'mcp-app', 'html', 'group'])
774
+ type: z
775
+ .enum([
776
+ 'markdown',
777
+ 'status',
778
+ 'context',
779
+ 'ledger',
780
+ 'trace',
781
+ 'file',
782
+ 'image',
783
+ 'webpage',
784
+ 'mcp-app',
785
+ 'html',
786
+ 'group',
787
+ ])
714
788
  .describe('Node type (prefer canvas_group {action:"create"} for groups)'),
715
- children: z.array(z.string()).optional().describe('Group-only alias for childIds. Node IDs to include in a generic group node.'),
716
- childIds: z.array(z.string()).optional().describe('Group-only field. Node IDs to include in a generic group node. Prefer canvas_group {action:"create"} for groups.'),
717
- axCapabilities: z.object({
718
- enabled: z.boolean().optional(),
719
- allowed: z.array(z.string()).optional(),
720
- }).optional().describe('Opt an html node into AX interactions (e.g. { enabled: true, allowed: ["ax.work.create"] }) so its sandboxed UI can emit ax.* via window.PMX_AX.emit. html nodes are AX-disabled by default; merged into node data, clamped to the node-type ceiling server-side.'),
789
+ children: z
790
+ .array(z.string())
791
+ .optional()
792
+ .describe('Group-only alias for childIds. Node IDs to include in a generic group node.'),
793
+ childIds: z
794
+ .array(z.string())
795
+ .optional()
796
+ .describe(
797
+ 'Group-only field. Node IDs to include in a generic group node. Prefer canvas_group {action:"create"} for groups.',
798
+ ),
799
+ axCapabilities: z
800
+ .object({
801
+ enabled: z.boolean().optional(),
802
+ allowed: z.array(z.string()).optional(),
803
+ })
804
+ .optional()
805
+ .describe(
806
+ 'Opt an html node into AX interactions (e.g. { enabled: true, allowed: ["ax.work.create"] }) so its sandboxed UI can emit ax.* via window.PMX_AX.emit. html nodes are AX-disabled by default; merged into node data, clamped to the node-type ceiling server-side.',
807
+ ),
721
808
  ...fullVerboseShape,
722
809
  },
723
810
  buildInput: (input) => {
724
811
  if (input.type === 'webpage') {
725
- const url = (typeof input.url === 'string' ? input.url : undefined)
726
- ?? (typeof input.content === 'string' ? input.content : undefined);
812
+ const url =
813
+ (typeof input.url === 'string' ? input.url : undefined) ??
814
+ (typeof input.content === 'string' ? input.content : undefined);
727
815
  if (!url) {
728
816
  throw new OperationError('Webpage nodes require a page URL via "url" (preferred) or "content".');
729
817
  }
@@ -755,13 +843,19 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
755
843
  }
756
844
  if (!NODE_TYPE_SET.has(type)) {
757
845
  if (type === 'json-render') {
758
- throw new OperationError('Node type "json-render" is created via POST /api/canvas/json-render. See /api/canvas/schema for the required spec shape.');
846
+ throw new OperationError(
847
+ 'Node type "json-render" is created via POST /api/canvas/json-render. See /api/canvas/schema for the required spec shape.',
848
+ );
759
849
  }
760
850
  if (type === 'graph') {
761
- throw new OperationError('Node type "graph" is created via POST /api/canvas/graph. See /api/canvas/schema for graphType + data fields.');
851
+ throw new OperationError(
852
+ 'Node type "graph" is created via POST /api/canvas/graph. See /api/canvas/schema for graphType + data fields.',
853
+ );
762
854
  }
763
855
  if (type === 'web-artifact') {
764
- throw new OperationError('Node type "web-artifact" is created via POST /api/canvas/web-artifact with appTsx + title.');
856
+ throw new OperationError(
857
+ 'Node type "web-artifact" is created via POST /api/canvas/web-artifact with appTsx + title.',
858
+ );
765
859
  }
766
860
  if (type === 'html-primitive') {
767
861
  return createHtmlPrimitiveNode(body);
@@ -808,7 +902,8 @@ const nodeGetOperation = defineOperation<z.infer<typeof nodeGetSchema>, Serializ
808
902
  },
809
903
  mcp: {
810
904
  toolName: 'canvas_get_node',
811
- description: 'Get a single node by ID. Defaults to compact metadata; pass full:true to include full data/tool results.',
905
+ description:
906
+ 'Get a single node by ID. Defaults to compact metadata; pass full:true to include full data/tool results.',
812
907
  extraShape: {
813
908
  full: z.boolean().optional().describe('Include full node data, including mcp-app tool results. Default false.'),
814
909
  verbose: z.boolean().optional().describe('Alias for full:true.'),
@@ -825,9 +920,7 @@ const nodeGetOperation = defineOperation<z.infer<typeof nodeGetSchema>, Serializ
825
920
  const node = full ? canvasState.getNode(id) : canvasState.getNodeForPersistence(id);
826
921
  if (!node) throw new OperationError(`Node "${id}" not found.`, 404);
827
922
  const responseNode = withContextPinReadState(node);
828
- return full
829
- ? serializeCanvasNode(responseNode)
830
- : serializeCanvasNodeWithBlobSummaries(responseNode);
923
+ return full ? serializeCanvasNode(responseNode) : serializeCanvasNodeWithBlobSummaries(responseNode);
831
924
  },
832
925
  });
833
926
 
@@ -835,7 +928,11 @@ const nodeGetOperation = defineOperation<z.infer<typeof nodeGetSchema>, Serializ
835
928
 
836
929
  const nodeUpdateShape = {
837
930
  id: z.string().describe('Node ID to update'),
838
- intentId: z.string().optional().catch(undefined).describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
931
+ intentId: z
932
+ .string()
933
+ .optional()
934
+ .catch(undefined)
935
+ .describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
839
936
  title: z.unknown().optional().describe('New title'),
840
937
  content: z.unknown().optional().describe('New content'),
841
938
  x: z.number().optional().catch(undefined).describe('New X position'),
@@ -846,7 +943,12 @@ const nodeUpdateShape = {
846
943
  size: z.unknown().optional().describe('Geometry alias: { width, height } object form.'),
847
944
  spec: z.unknown().optional().describe('New json-render spec, or a graph payload with graphType/data for graph nodes'),
848
945
  graphType: z.unknown().optional().describe('Graph type when updating a graph node'),
849
- data: z.unknown().optional().describe('Graph dataset (array) when updating a graph node, or extra data fields (object) merged into the node data'),
946
+ data: z
947
+ .unknown()
948
+ .optional()
949
+ .describe(
950
+ 'Graph dataset (array) when updating a graph node, or extra data fields (object) merged into the node data',
951
+ ),
850
952
  xKey: z.unknown().optional().describe('Graph x/category key'),
851
953
  yKey: z.unknown().optional().describe('Graph y/value key'),
852
954
  chartHeight: z.unknown().optional().describe('Graph chart content height, distinct from node height'),
@@ -857,11 +959,27 @@ const nodeUpdateShape = {
857
959
  resultSummary: z.string().optional().catch(undefined).describe('Trace node result summary'),
858
960
  error: z.string().optional().catch(undefined).describe('Trace node error message'),
859
961
  collapsed: z.unknown().optional().describe('Collapse or expand the node'),
860
- dockPosition: z.unknown().optional().describe('Dock the node to the left/right HUD column, or pass null to return it to the canvas'),
962
+ dockPosition: z
963
+ .unknown()
964
+ .optional()
965
+ .describe('Dock the node to the left/right HUD column, or pass null to return it to the canvas'),
861
966
  pinned: z.unknown().optional().describe('Pin or unpin the node to exclude it from auto-arrange'),
862
- arrangeLocked: z.boolean().optional().catch(undefined).describe('Prevent auto-arrange from moving this node. Pinned nodes are also skipped.'),
863
- strictSize: z.boolean().optional().catch(undefined).describe('Keep explicit width/height fixed and scroll overflowing content.'),
864
- axCapabilities: z.unknown().optional().describe('Enable/disable AX interactions on an existing node. Merged into the node data; clamped to the node-type ceiling server-side.'),
967
+ arrangeLocked: z
968
+ .boolean()
969
+ .optional()
970
+ .catch(undefined)
971
+ .describe('Prevent auto-arrange from moving this node. Pinned nodes are also skipped.'),
972
+ strictSize: z
973
+ .boolean()
974
+ .optional()
975
+ .catch(undefined)
976
+ .describe('Keep explicit width/height fixed and scroll overflowing content.'),
977
+ axCapabilities: z
978
+ .unknown()
979
+ .optional()
980
+ .describe(
981
+ 'Enable/disable AX interactions on an existing node. Merged into the node data; clamped to the node-type ceiling server-side.',
982
+ ),
865
983
  html: z.unknown().optional().describe('New HTML document/fragment (html nodes only).'),
866
984
  url: z.unknown().optional().describe('New URL for webpage nodes.'),
867
985
  refresh: z.unknown().optional().describe('Webpage nodes: pass true to re-fetch the page instead of patching fields.'),
@@ -889,21 +1007,39 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
889
1007
  extraShape: {
890
1008
  title: z.string().optional().describe('New title'),
891
1009
  content: z.string().optional().describe('New content'),
892
- spec: z.record(z.string(), z.unknown()).optional().describe('New json-render spec, or a graph payload with graphType/data for graph nodes'),
1010
+ spec: z
1011
+ .record(z.string(), z.unknown())
1012
+ .optional()
1013
+ .describe('New json-render spec, or a graph payload with graphType/data for graph nodes'),
893
1014
  graphType: z.string().optional().describe('Graph type when updating a graph node'),
894
1015
  data: z.array(z.record(z.string(), z.unknown())).optional().describe('Graph dataset when updating a graph node'),
895
1016
  xKey: z.string().optional().describe('Graph x/category key'),
896
1017
  yKey: z.string().optional().describe('Graph y/value key'),
897
1018
  chartHeight: z.number().optional().describe('Graph chart content height, distinct from node height'),
898
1019
  collapsed: z.boolean().optional().describe('Collapse or expand the node'),
899
- dockPosition: z.enum(['left', 'right']).nullable().optional().describe('Dock the node to the left/right HUD column, or pass null to return it to the canvas'),
1020
+ dockPosition: z
1021
+ .enum(['left', 'right'])
1022
+ .nullable()
1023
+ .optional()
1024
+ .describe('Dock the node to the left/right HUD column, or pass null to return it to the canvas'),
900
1025
  pinned: z.boolean().optional().describe('Pin or unpin the node to exclude it from auto-arrange'),
901
- arrangeLocked: z.boolean().optional().describe('Prevent auto-arrange from moving this node. Pinned nodes are also skipped.'),
902
- axCapabilities: z.object({
903
- enabled: z.boolean().optional(),
904
- allowed: z.array(z.string()).optional(),
905
- }).optional().describe('Enable/disable AX interactions on an existing node (e.g. flip an html node on with { enabled: true, allowed: ["ax.work.create"] }). Merged into the node data; clamped to the node-type ceiling server-side.'),
906
- full: z.boolean().optional().describe('Return the full updated node payload. Default false returns compact metadata.'),
1026
+ arrangeLocked: z
1027
+ .boolean()
1028
+ .optional()
1029
+ .describe('Prevent auto-arrange from moving this node. Pinned nodes are also skipped.'),
1030
+ axCapabilities: z
1031
+ .object({
1032
+ enabled: z.boolean().optional(),
1033
+ allowed: z.array(z.string()).optional(),
1034
+ })
1035
+ .optional()
1036
+ .describe(
1037
+ 'Enable/disable AX interactions on an existing node (e.g. flip an html node on with { enabled: true, allowed: ["ax.work.create"] }). Merged into the node data; clamped to the node-type ceiling server-side.',
1038
+ ),
1039
+ full: z
1040
+ .boolean()
1041
+ .optional()
1042
+ .describe('Return the full updated node payload. Default false returns compact metadata.'),
907
1043
  verbose: z.boolean().optional().describe('Alias for full:true.'),
908
1044
  },
909
1045
  buildInput: (input) => {
@@ -967,7 +1103,11 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
967
1103
 
968
1104
  const nodeRemoveShape = {
969
1105
  id: z.string().describe('Node ID to remove'),
970
- intentId: z.string().optional().catch(undefined).describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
1106
+ intentId: z
1107
+ .string()
1108
+ .optional()
1109
+ .catch(undefined)
1110
+ .describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
971
1111
  };
972
1112
 
973
1113
  const nodeRemoveSchema = z.looseObject(nodeRemoveShape);
@@ -1018,9 +1158,13 @@ const layoutGetOperation = defineOperation<z.infer<typeof layoutGetSchema>, Reco
1018
1158
  },
1019
1159
  mcp: {
1020
1160
  toolName: 'canvas_get_layout',
1021
- description: 'Get the canvas layout. Defaults to a compact agent-safe projection; pass full:true for full node data.',
1161
+ description:
1162
+ 'Get the canvas layout. Defaults to a compact agent-safe projection; pass full:true for full node data.',
1022
1163
  extraShape: {
1023
- full: z.boolean().optional().describe('Return the full layout including node data. Default false keeps responses compact.'),
1164
+ full: z
1165
+ .boolean()
1166
+ .optional()
1167
+ .describe('Return the full layout including node data. Default false keeps responses compact.'),
1024
1168
  verbose: z.boolean().optional().describe('Alias for full:true.'),
1025
1169
  },
1026
1170
  buildInput: () => ({ includeBlobs: true }),
@@ -1036,8 +1180,52 @@ const layoutGetOperation = defineOperation<z.infer<typeof layoutGetSchema>, Reco
1036
1180
  const full = includeBlobs === true || includeBlobs === 'true';
1037
1181
  return (full
1038
1182
  ? serializeCanvasLayout(canvasState.getLayout())
1039
- : serializeCanvasLayoutWithBlobSummaries(withContextPinLayoutReadState(canvasState.getLayoutForPersistence()))
1040
- ) as unknown as Record<string, unknown>;
1183
+ : serializeCanvasLayoutWithBlobSummaries(
1184
+ withContextPinLayoutReadState(canvasState.getLayoutForPersistence()),
1185
+ )) as unknown as Record<string, unknown>;
1186
+ },
1187
+ });
1188
+
1189
+ // ── node.refresh (plan-009 C1 slice 3) ────────────────────────
1190
+
1191
+ const nodeRefreshShape = {
1192
+ id: z.string().optional().catch(undefined).describe('Webpage node ID to refresh'),
1193
+ url: z.unknown().optional().describe('Optional replacement page URL'),
1194
+ };
1195
+
1196
+ const nodeRefreshSchema = z.looseObject(nodeRefreshShape);
1197
+
1198
+ const nodeRefreshOperation = defineOperation<z.infer<typeof nodeRefreshSchema>, Record<string, unknown>>({
1199
+ name: 'node.refresh',
1200
+ mutates: false,
1201
+ input: nodeRefreshSchema,
1202
+ inputShape: nodeRefreshShape,
1203
+ http: {
1204
+ method: 'POST',
1205
+ path: '/api/canvas/node/:id/refresh',
1206
+ // Legacy wire: 200 on success, 400 with the refresh failure body otherwise.
1207
+ status: (result) => (isRecord(result) && result.ok === false ? 400 : 200),
1208
+ },
1209
+ handler: async (input, ctx) => {
1210
+ const nodeId = input.id ?? '';
1211
+ const existing = canvasState.getNode(nodeId);
1212
+ if (existing?.type !== 'webpage') {
1213
+ throw new OperationError(`Webpage node "${nodeId}" not found.`, 404);
1214
+ }
1215
+ const rawUrl = typeof input.url === 'string' ? input.url : undefined;
1216
+ let url: string | undefined;
1217
+ if (rawUrl && rawUrl.trim().length > 0) {
1218
+ try {
1219
+ url = normalizeWebpageUrl(rawUrl);
1220
+ } catch (error) {
1221
+ throw new OperationError(error instanceof Error ? error.message : 'Invalid webpage URL.');
1222
+ }
1223
+ }
1224
+ // Legacy wire: the layout update fires after every completed refresh
1225
+ // attempt, success or failure (the node may have partial refresh state).
1226
+ const result = await refreshCanvasWebpageNode(nodeId, { ...(url ? { url } : {}) });
1227
+ ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
1228
+ return result as unknown as Record<string, unknown>;
1041
1229
  },
1042
1230
  });
1043
1231
 
@@ -1047,4 +1235,5 @@ export const nodeOperations: Operation[] = [
1047
1235
  nodeUpdateOperation,
1048
1236
  nodeRemoveOperation,
1049
1237
  layoutGetOperation,
1238
+ nodeRefreshOperation,
1050
1239
  ];