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
@@ -72,10 +72,8 @@ function applyLayoutOverrides(node: CanvasNodeState): CanvasNodeState {
72
72
  }
73
73
 
74
74
  // ── Default positions by type ─────────────────────────────────
75
- const DEFAULT_POSITIONS: Record<
76
- CanvasNodeState['type'],
77
- { x: number; y: number; w: number; h: number }
78
- > & Record<'prompt' | 'response', { x: number; y: number; w: number; h: number }> = {
75
+ const DEFAULT_POSITIONS: Record<CanvasNodeState['type'], { x: number; y: number; w: number; h: number }> &
76
+ Record<'prompt' | 'response', { x: number; y: number; w: number; h: number }> = {
79
77
  status: { x: 40, y: 80, w: 300, h: 120 },
80
78
  markdown: { x: 380, y: 80, w: 720, h: 600 },
81
79
  context: { x: 1130, y: 80, w: 320, h: 400 },
@@ -114,11 +112,7 @@ function makeNode(
114
112
  }
115
113
 
116
114
  function getMarkdownPlacement(): { x: number; y: number } {
117
- return findOpenCanvasPosition(
118
- [...nodes.value.values()],
119
- DEFAULT_POSITIONS.markdown.w,
120
- DEFAULT_POSITIONS.markdown.h,
121
- );
115
+ return findOpenCanvasPosition([...nodes.value.values()], DEFAULT_POSITIONS.markdown.w, DEFAULT_POSITIONS.markdown.h);
122
116
  }
123
117
 
124
118
  // ── Node ensure helpers ───────────────────────────────────────
@@ -243,11 +237,7 @@ function findExtAppNodeId(toolCallId: string): string | null {
243
237
  const legacyDirectId = `ext-app-${toolCallId}`;
244
238
  if (legacyDirectId !== directId && nodes.value.has(legacyDirectId)) return legacyDirectId;
245
239
  for (const [nodeId, node] of nodes.value.entries()) {
246
- if (
247
- node.type === 'mcp-app' &&
248
- node.data.mode === 'ext-app' &&
249
- node.data.toolCallId === toolCallId
250
- ) {
240
+ if (node.type === 'mcp-app' && node.data.mode === 'ext-app' && node.data.toolCallId === toolCallId) {
251
241
  return nodeId;
252
242
  }
253
243
  }
@@ -301,28 +291,27 @@ function applyCanvasTheme(theme: string): void {
301
291
  }
302
292
 
303
293
  function isCanvasNodeType(value: unknown): value is CanvasNodeState['type'] {
304
- return value === 'markdown'
305
- || value === 'mcp-app'
306
- || value === 'webpage'
307
- || value === 'json-render'
308
- || value === 'graph'
309
- || value === 'prompt'
310
- || value === 'response'
311
- || value === 'status'
312
- || value === 'context'
313
- || value === 'ledger'
314
- || value === 'trace'
315
- || value === 'file'
316
- || value === 'image'
317
- || value === 'html'
318
- || value === 'group';
294
+ return (
295
+ value === 'markdown' ||
296
+ value === 'mcp-app' ||
297
+ value === 'webpage' ||
298
+ value === 'json-render' ||
299
+ value === 'graph' ||
300
+ value === 'prompt' ||
301
+ value === 'response' ||
302
+ value === 'status' ||
303
+ value === 'context' ||
304
+ value === 'ledger' ||
305
+ value === 'trace' ||
306
+ value === 'file' ||
307
+ value === 'image' ||
308
+ value === 'html' ||
309
+ value === 'group'
310
+ );
319
311
  }
320
312
 
321
313
  function isCanvasEdgeType(value: unknown): value is CanvasEdge['type'] {
322
- return value === 'relation'
323
- || value === 'depends-on'
324
- || value === 'flow'
325
- || value === 'references';
314
+ return value === 'relation' || value === 'depends-on' || value === 'flow' || value === 'references';
326
315
  }
327
316
 
328
317
  function parseCanvasPosition(value: unknown): { x: number; y: number } | null {
@@ -353,10 +342,8 @@ function parseCanvasNode(raw: Record<string, unknown>): CanvasNodeState | null {
353
342
  const size = parseCanvasSize(raw.size);
354
343
  if (!position || !size) return null;
355
344
 
356
- const dockPosition =
357
- raw.dockPosition === 'left' || raw.dockPosition === 'right' ? raw.dockPosition : null;
358
- const data =
359
- raw.data && typeof raw.data === 'object' ? Object.fromEntries(Object.entries(raw.data)) : {};
345
+ const dockPosition = raw.dockPosition === 'left' || raw.dockPosition === 'right' ? raw.dockPosition : null;
346
+ const data = raw.data && typeof raw.data === 'object' ? Object.fromEntries(Object.entries(raw.data)) : {};
360
347
 
361
348
  return {
362
349
  id: raw.id,
@@ -383,9 +370,7 @@ function parseCanvasEdge(raw: Record<string, unknown>): CanvasEdge | null {
383
370
  to: raw.to,
384
371
  type: raw.type,
385
372
  ...(typeof raw.label === 'string' ? { label: raw.label } : {}),
386
- ...(raw.style === 'solid' || raw.style === 'dashed' || raw.style === 'dotted'
387
- ? { style: raw.style }
388
- : {}),
373
+ ...(raw.style === 'solid' || raw.style === 'dashed' || raw.style === 'dotted' ? { style: raw.style } : {}),
389
374
  ...(raw.animated === true ? { animated: true } : {}),
390
375
  };
391
376
  }
@@ -429,8 +414,7 @@ function handleWorkbenchOpen(data: Record<string, unknown>): void {
429
414
  // H6: Guard — path must be a string for node ID stability
430
415
  if (typeof data.path !== 'string' || !data.path) return;
431
416
  const path = data.path;
432
- const title =
433
- (typeof data.title === 'string' ? data.title : '') || path.split('/').pop() || 'Untitled';
417
+ const title = (typeof data.title === 'string' ? data.title : '') || path.split('/').pop() || 'Untitled';
434
418
 
435
419
  ensureMarkdownNode(path, title);
436
420
  if (data.ledgerSummary) {
@@ -515,9 +499,7 @@ function handleAuxClose(data: Record<string, unknown>): void {
515
499
  } else {
516
500
  const existing = nodes.value.get(id);
517
501
  if (!existing) return;
518
- const auxTabs = ((existing.data.auxTabs as Array<Record<string, unknown>>) ?? []).filter(
519
- (t) => t.id !== data.id,
520
- );
502
+ const auxTabs = ((existing.data.auxTabs as Array<Record<string, unknown>>) ?? []).filter((t) => t.id !== data.id);
521
503
  updateNodeData(id, { auxTabs });
522
504
  }
523
505
  }
@@ -587,8 +569,7 @@ function handleExtAppOpen(data: Record<string, unknown>): void {
587
569
 
588
570
  function handleExtAppUpdate(data: Record<string, unknown>): void {
589
571
  if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
590
- const id =
591
- findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
572
+ const id = findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
592
573
  if (!id) return;
593
574
  if (nodes.value.has(id)) {
594
575
  updateNodeData(id, { html: data.html });
@@ -597,8 +578,7 @@ function handleExtAppUpdate(data: Record<string, unknown>): void {
597
578
 
598
579
  function handleExtAppResult(data: Record<string, unknown>): void {
599
580
  if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
600
- const id =
601
- findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
581
+ const id = findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
602
582
  if (!id) return;
603
583
  if (nodes.value.has(id)) {
604
584
  if (data.success === false) {
@@ -611,8 +591,7 @@ function handleExtAppResult(data: Record<string, unknown>): void {
611
591
  success: typeof data.success === 'boolean' ? data.success : undefined,
612
592
  error: typeof data.error === 'string' ? data.error : undefined,
613
593
  content: typeof data.content === 'string' ? data.content : undefined,
614
- detailedContent:
615
- typeof data.detailedContent === 'string' ? data.detailedContent : undefined,
594
+ detailedContent: typeof data.detailedContent === 'string' ? data.detailedContent : undefined,
616
595
  }),
617
596
  });
618
597
  }
@@ -843,14 +822,14 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
843
822
  const shouldApplyViewport = !hasInitialServerLayout.value;
844
823
  hasInitialServerLayout.value = true;
845
824
 
846
- const serverNodes = layout.nodes
847
- .map(parseCanvasNode)
848
- .filter((node): node is CanvasNodeState => node !== null);
825
+ const serverNodes = layout.nodes.map(parseCanvasNode).filter((node): node is CanvasNodeState => node !== null);
849
826
  const serverEdges = Array.isArray(layout.edges)
850
827
  ? layout.edges.map(parseCanvasEdge).filter((edge): edge is CanvasEdge => edge !== null)
851
828
  : Array.from(edges.value.values());
852
829
  const serverAnnotations = Array.isArray(layout.annotations)
853
- ? layout.annotations.map(parseCanvasAnnotation).filter((annotation): annotation is CanvasAnnotation => annotation !== null)
830
+ ? layout.annotations
831
+ .map(parseCanvasAnnotation)
832
+ .filter((annotation): annotation is CanvasAnnotation => annotation !== null)
854
833
  : undefined;
855
834
  const nextViewport = layout.viewport
856
835
  ? {
@@ -861,12 +840,15 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
861
840
  : undefined;
862
841
 
863
842
  cancelViewportAnimation();
864
- applyServerCanvasLayout({
865
- ...(nextViewport ? { viewport: nextViewport } : {}),
866
- nodes: serverNodes,
867
- edges: serverEdges,
868
- ...(serverAnnotations ? { annotations: serverAnnotations } : {}),
869
- }, { applyViewport: shouldApplyViewport });
843
+ applyServerCanvasLayout(
844
+ {
845
+ ...(nextViewport ? { viewport: nextViewport } : {}),
846
+ nodes: serverNodes,
847
+ edges: serverEdges,
848
+ ...(serverAnnotations ? { annotations: serverAnnotations } : {}),
849
+ },
850
+ { applyViewport: shouldApplyViewport },
851
+ );
870
852
 
871
853
  syncAttentionFromSse({ event: 'canvas-layout-update', data });
872
854
  }
@@ -923,9 +905,7 @@ function handleThemeChanged(data: Record<string, unknown>): void {
923
905
  }
924
906
 
925
907
  function handleContextPinsChanged(data: Record<string, unknown>): void {
926
- const nodeIds = Array.isArray(data.nodeIds)
927
- ? data.nodeIds.filter((id): id is string => typeof id === 'string')
928
- : [];
908
+ const nodeIds = Array.isArray(data.nodeIds) ? data.nodeIds.filter((id): id is string => typeof id === 'string') : [];
929
909
  replaceContextPinsFromServer(nodeIds);
930
910
  syncAttentionFromSse({ event: 'context-pins-changed', data });
931
911
  }
@@ -939,7 +919,9 @@ function handleAxStateChanged(): void {
939
919
  if (axRefreshTimer) clearTimeout(axRefreshTimer);
940
920
  axRefreshTimer = setTimeout(() => {
941
921
  axRefreshTimer = null;
942
- void fetchAxSurfaceState().then((state) => { axSurfaceState.value = state; });
922
+ void fetchAxSurfaceState().then((state) => {
923
+ axSurfaceState.value = state;
924
+ });
943
925
  }, 150);
944
926
  }
945
927
 
@@ -951,11 +933,12 @@ function handleAxIntent(data: Record<string, unknown>): void {
951
933
  // be pruned if its `clear` frame were dropped. The server always sets it, so this
952
934
  // only rejects a malformed frame — keeping the backstop's guarantee real.
953
935
  if (
954
- !intent
955
- || typeof intent.id !== 'string'
956
- || typeof intent.kind !== 'string'
957
- || typeof intent.expiresAt !== 'number'
958
- ) return;
936
+ !intent ||
937
+ typeof intent.id !== 'string' ||
938
+ typeof intent.kind !== 'string' ||
939
+ typeof intent.expiresAt !== 'number'
940
+ )
941
+ return;
959
942
  upsertIntent(intent);
960
943
  }
961
944
 
@@ -23,7 +23,9 @@ export const allComponentDefinitions = {
23
23
  Badge: {
24
24
  ...badgeDefinition,
25
25
  props: badgeDefinition.props.extend({
26
- variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'success', 'info', 'warning', 'error', 'danger']).nullable(),
26
+ variant: z
27
+ .enum(['default', 'secondary', 'destructive', 'outline', 'success', 'info', 'warning', 'error', 'danger'])
28
+ .nullable(),
27
29
  }),
28
30
  },
29
31
  Button: {
@@ -40,9 +42,12 @@ export const allComponentDefinitions = {
40
42
  ...tufteChartComponentDefinitions,
41
43
  };
42
44
 
43
- export const catalog = defineCatalog(schema as never, {
44
- components: allComponentDefinitions,
45
- } as never);
45
+ export const catalog = defineCatalog(
46
+ schema as never,
47
+ {
48
+ components: allComponentDefinitions,
49
+ } as never,
50
+ );
46
51
 
47
52
  export interface JsonRenderIssue {
48
53
  path?: PropertyKey[];
@@ -134,12 +139,7 @@ function unwrapSchema(value: unknown): {
134
139
 
135
140
  const record = asRecord(current);
136
141
  const def = asRecord(record?._def) ?? asRecord(record?.def);
137
- const inner =
138
- def?.innerType ??
139
- def?.schema ??
140
- def?.type ??
141
- def?.out ??
142
- def?.in;
142
+ const inner = def?.innerType ?? def?.schema ?? def?.type ?? def?.out ?? def?.in;
143
143
 
144
144
  if (!inner || inner === current || (!isOptionalSchema(current) && !isNullableSchema(current))) {
145
145
  break;
@@ -178,9 +178,7 @@ function schemaTypeLabel(value: unknown): string {
178
178
  : Array.isArray(def?.options)
179
179
  ? def.options
180
180
  : [];
181
- label = rawValues.length > 0
182
- ? rawValues.map((entry) => JSON.stringify(entry)).join(' | ')
183
- : 'enum';
181
+ label = rawValues.length > 0 ? rawValues.map((entry) => JSON.stringify(entry)).join(' | ') : 'enum';
184
182
  } else if (typeName === 'ZodLiteral' || typeName === 'literal') {
185
183
  label = JSON.stringify(def?.value ?? def?.literal ?? 'literal');
186
184
  } else if (typeName === 'ZodAny' || typeName === 'any') {
@@ -255,9 +253,7 @@ export function validateShadcnElementProps(spec: unknown): JsonRenderValidationR
255
253
  if (!definition || !hasSafeParse(definition.props)) continue;
256
254
 
257
255
  const elementProps = asRecord(element.props) ?? {};
258
- const parsed = definition.props.safeParse(
259
- normalizePropsForSchema(definition.props, elementProps),
260
- );
256
+ const parsed = definition.props.safeParse(normalizePropsForSchema(definition.props, elementProps));
261
257
  if (parsed.success) continue;
262
258
 
263
259
  for (const issue of parsed.error?.issues ?? []) {
@@ -120,8 +120,9 @@ export function useChartFrameHeight(explicitHeight: number | null | undefined, f
120
120
 
121
121
  // Standalone "Open as site" tab (#65): fill the full browser viewport — there is no
122
122
  // card chrome below the chart, so drop the ~44px reserve and use a larger floor.
123
- const isSite = typeof window !== 'undefined'
124
- && (window as { __PMX_CANVAS_JSON_RENDER_DISPLAY__?: string }).__PMX_CANVAS_JSON_RENDER_DISPLAY__ === 'site';
123
+ const isSite =
124
+ typeof window !== 'undefined' &&
125
+ (window as { __PMX_CANVAS_JSON_RENDER_DISPLAY__?: string }).__PMX_CANVAS_JSON_RENDER_DISPLAY__ === 'site';
125
126
 
126
127
  useEffect(() => {
127
128
  const frame = frameRef.current;
@@ -169,16 +170,21 @@ export function useChartFrameHeight(explicitHeight: number | null | undefined, f
169
170
  // the node/viewport height. That makes the document's scrollHeight stable so the
170
171
  // node can grow to it once and converge (no fill-down feedback loop). When NOT in
171
172
  // content-fit (strictSize / user-resized nodes), it fills the frame down as before.
172
- const fitContent = typeof window !== 'undefined'
173
- && (window as { __PMX_CANVAS_FIT_CONTENT__?: boolean }).__PMX_CANVAS_FIT_CONTENT__ === true;
173
+ const fitContent =
174
+ typeof window !== 'undefined' &&
175
+ (window as { __PMX_CANVAS_FIT_CONTENT__?: boolean }).__PMX_CANVAS_FIT_CONTENT__ === true;
174
176
  // Site mode (#65): fill the viewport (autoHeight), ignoring an explicit/configured
175
177
  // chart height that would otherwise cap it to a shallow card. Content-fit is off in
176
178
  // site mode (the server skips it), so site never takes the intrinsic-height branch.
177
179
  const height = isSite
178
180
  ? autoHeight
179
181
  : fitContent
180
- ? (typeof explicitHeight === 'number' ? explicitHeight : fallbackHeight)
181
- : (typeof explicitHeight === 'number' ? Math.min(explicitHeight, autoHeight) : autoHeight);
182
+ ? typeof explicitHeight === 'number'
183
+ ? explicitHeight
184
+ : fallbackHeight
185
+ : typeof explicitHeight === 'number'
186
+ ? Math.min(explicitHeight, autoHeight)
187
+ : autoHeight;
182
188
  return {
183
189
  frameRef,
184
190
  height,
@@ -251,11 +257,7 @@ function ChartLineChart({ props }: BaseComponentProps<CartesianChartProps>) {
251
257
  * 'max'/'min' pick the tallest/shortest yKey value; a number is used as-is
252
258
  * (clamped to range); null/out-of-range means no bar is emphasized.
253
259
  */
254
- function resolveHighlightIndex(
255
- data: Record<string, unknown>[],
256
- yKey: string,
257
- highlight: BarHighlight,
258
- ): number {
260
+ function resolveHighlightIndex(data: Record<string, unknown>[], yKey: string, highlight: BarHighlight): number {
259
261
  if (highlight === null || data.length === 0) return -1;
260
262
  if (typeof highlight === 'number') {
261
263
  return highlight >= 0 && highlight < data.length ? highlight : -1;
@@ -299,9 +301,7 @@ function barCellFill(
299
301
  case 'series':
300
302
  default:
301
303
  // Tufte-safe emphasis: one accent bar, the rest a muted version of it.
302
- return index === highlightIndex
303
- ? accent
304
- : `color-mix(in oklch, ${accent} 32%, transparent)`;
304
+ return index === highlightIndex ? accent : `color-mix(in oklch, ${accent} 32%, transparent)`;
305
305
  }
306
306
  }
307
307
 
@@ -317,8 +317,7 @@ function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>) {
317
317
  min: values.length ? Math.min(...values) : 0,
318
318
  max: values.length ? Math.max(...values) : 0,
319
319
  };
320
- const highlightIndex =
321
- mode === 'series' ? resolveHighlightIndex(data, props.yKey, highlight) : -1;
320
+ const highlightIndex = mode === 'series' ? resolveHighlightIndex(data, props.yKey, highlight) : -1;
322
321
  return (
323
322
  <RechartsBarChart data={data} margin={chartMargin}>
324
323
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
@@ -327,10 +326,7 @@ function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>) {
327
326
  <Tooltip contentStyle={tooltipStyle} cursor={false} />
328
327
  <Bar dataKey={props.yKey} radius={[4, 4, 0, 0]}>
329
328
  {data.map((_, i) => (
330
- <Cell
331
- key={i}
332
- fill={barCellFill(mode, accent, i, values[i], range, highlightIndex)}
333
- />
329
+ <Cell key={i} fill={barCellFill(mode, accent, i, values[i], range, highlightIndex)} />
334
330
  ))}
335
331
  </Bar>
336
332
  </RechartsBarChart>
@@ -136,16 +136,7 @@ function ChartRadarChart({ props }: BaseComponentProps<RadarChartProps>) {
136
136
  {props.showLegend !== false && <Legend wrapperStyle={legendMargin} />}
137
137
  {metrics.map((metric, i) => {
138
138
  const color = CHART_COLORS[i % CHART_COLORS.length];
139
- return (
140
- <Radar
141
- key={metric}
142
- name={metric}
143
- dataKey={metric}
144
- stroke={color}
145
- fill={color}
146
- fillOpacity={0.25}
147
- />
148
- );
139
+ return <Radar key={metric} name={metric} dataKey={metric} stroke={color} fill={color} fillOpacity={0.25} />;
149
140
  })}
150
141
  </RechartsRadarChart>
151
142
  </ResponsiveContainer>
@@ -167,7 +158,7 @@ function ChartStackedBarChart({ props }: BaseComponentProps<StackedBarChartProps
167
158
  const series = (props.series ?? []).filter((s) => typeof s === 'string' && s.length > 0);
168
159
  const chartData = props.aggregate
169
160
  ? mergeAggregated(props.data ?? [], props.xKey, series, props.aggregate)
170
- : props.data ?? [];
161
+ : (props.data ?? []);
171
162
  const { frameRef, height } = useChartFrameHeight(props.height, 300);
172
163
 
173
164
  return (
@@ -212,11 +203,12 @@ function mergeAggregated(
212
203
  bucket[s].push(Number.isNaN(n) ? 0 : n);
213
204
  }
214
205
  }
215
- const reducer = aggregate === 'count'
216
- ? (vs: number[]) => vs.length
217
- : aggregate === 'avg'
218
- ? (vs: number[]) => vs.reduce((a, b) => a + b, 0) / vs.length
219
- : (vs: number[]) => vs.reduce((a, b) => a + b, 0);
206
+ const reducer =
207
+ aggregate === 'count'
208
+ ? (vs: number[]) => vs.length
209
+ : aggregate === 'avg'
210
+ ? (vs: number[]) => vs.reduce((a, b) => a + b, 0) / vs.length
211
+ : (vs: number[]) => vs.reduce((a, b) => a + b, 0);
220
212
  return Array.from(grouped.entries()).map(([key, buckets]) => {
221
213
  const out: Record<string, unknown> = { [xKey]: key };
222
214
  for (const s of series) out[s] = reducer(buckets[s] ?? []);
@@ -130,8 +130,7 @@ export const extraChartComponentDefinitions = {
130
130
  height: z.number().nullable(),
131
131
  showLegend: z.boolean().optional(),
132
132
  }),
133
- description:
134
- 'Combined bar + line chart for paired metrics (e.g. counts + a derived rate) on the same axis.',
133
+ description: 'Combined bar + line chart for paired metrics (e.g. counts + a derived rate) on the same axis.',
135
134
  example: {
136
135
  title: 'Visits and conversion',
137
136
  data: [
@@ -100,9 +100,7 @@ function ChartSparkline({ props }: BaseComponentProps<SparklineProps>) {
100
100
  role="img"
101
101
  aria-label={props.title ?? 'sparkline'}
102
102
  >
103
- {props.fill && areaPath && (
104
- <path d={areaPath} fill={stroke} fillOpacity={0.12} stroke="none" />
105
- )}
103
+ {props.fill && areaPath && <path d={areaPath} fill={stroke} fillOpacity={0.12} stroke="none" />}
106
104
  {points.length > 1 && (
107
105
  <path d={linePath} fill="none" stroke={stroke} strokeWidth={1.5} vectorEffect="non-scaling-stroke" />
108
106
  )}
@@ -166,7 +164,13 @@ function ChartDotPlot({ props }: BaseComponentProps<DotPlotProps>) {
166
164
  return (
167
165
  <div ref={frameRef} className="pmx-chart pmx-chart--dot-plot" style={{ height }}>
168
166
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
169
- <svg className="pmx-chart__dot-plot-svg" width="100%" height={rows.length * rowH} role="img" aria-label={props.title ?? 'dot plot'}>
167
+ <svg
168
+ className="pmx-chart__dot-plot-svg"
169
+ width="100%"
170
+ height={rows.length * rowH}
171
+ role="img"
172
+ aria-label={props.title ?? 'dot plot'}
173
+ >
170
174
  {rows.map((row, i) => {
171
175
  const cy = i * rowH + rowH / 2;
172
176
  // Reference rule runs from the axis origin to the dot, so the line's
@@ -245,12 +249,17 @@ function ChartBulletChart({ props }: BaseComponentProps<BulletChartProps>) {
245
249
  return (
246
250
  <div ref={frameRef} className="pmx-chart pmx-chart--bullet" style={{ height }}>
247
251
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
248
- <svg className="pmx-chart__bullet-svg" width="100%" height={rows.length * rowH} role="img" aria-label={props.title ?? 'bullet chart'}>
252
+ <svg
253
+ className="pmx-chart__bullet-svg"
254
+ width="100%"
255
+ height={rows.length * rowH}
256
+ role="img"
257
+ aria-label={props.title ?? 'bullet chart'}
258
+ >
249
259
  {rows.map((row, i) => {
250
260
  const top = i * rowH;
251
261
  const cy = top + rowH / 2;
252
- const domainMax =
253
- Math.max(row.value, row.target ?? 0, ...(row.ranges ?? [0])) || 1;
262
+ const domainMax = Math.max(row.value, row.target ?? 0, ...(row.ranges ?? [0])) || 1;
254
263
  const ranges = row.ranges ?? [];
255
264
  const left = labelW + padX;
256
265
  const rightInset = padX;
@@ -261,9 +270,11 @@ function ChartBulletChart({ props }: BaseComponentProps<BulletChartProps>) {
261
270
  const barH = Math.min(20, rowH * 0.5);
262
271
  const measureH = barH * 0.4;
263
272
  // Qualitative bands: lightest (worst) to darkest (best) grayscale.
264
- const bandShades = ['color-mix(in oklch, var(--muted) 35%, transparent)',
265
- 'color-mix(in oklch, var(--muted) 60%, transparent)',
266
- 'color-mix(in oklch, var(--muted) 90%, transparent)'];
273
+ const bandShades = [
274
+ 'color-mix(in oklch, var(--muted) 35%, transparent)',
275
+ 'color-mix(in oklch, var(--muted) 60%, transparent)',
276
+ 'color-mix(in oklch, var(--muted) 90%, transparent)',
277
+ ];
267
278
  return (
268
279
  <g key={`${row.label}-${i}`}>
269
280
  <text x={labelW} y={cy} textAnchor="end" dominantBaseline="central" fontSize={12} fill={INK}>
@@ -285,7 +296,14 @@ function ChartBulletChart({ props }: BaseComponentProps<BulletChartProps>) {
285
296
  {/* Per-row scale ticks at each band boundary so the reader does not
286
297
  compare bar lengths across rows that may be independently scaled. */}
287
298
  {ranges.map((hi, idx) => (
288
- <text key={`tick-${idx}`} x={xAt(hi)} y={cy + barH / 2 + 10} textAnchor="middle" fontSize={9} fill={MUTED}>
299
+ <text
300
+ key={`tick-${idx}`}
301
+ x={xAt(hi)}
302
+ y={cy + barH / 2 + 10}
303
+ textAnchor="middle"
304
+ fontSize={9}
305
+ fill={MUTED}
306
+ >
289
307
  {hi}
290
308
  </text>
291
309
  ))}
@@ -345,7 +363,13 @@ function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
345
363
  return (
346
364
  <div ref={frameRef} className="pmx-chart pmx-chart--slopegraph" style={{ height }}>
347
365
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
348
- <svg className="pmx-chart__slopegraph-svg" width="100%" height={plotH} role="img" aria-label={props.title ?? 'slopegraph'}>
366
+ <svg
367
+ className="pmx-chart__slopegraph-svg"
368
+ width="100%"
369
+ height={plotH}
370
+ role="img"
371
+ aria-label={props.title ?? 'slopegraph'}
372
+ >
349
373
  <text x={leftX} y={14} textAnchor="end" fontSize={12} fontWeight={600} fill={MUTED}>
350
374
  {props.beforeLabel ?? props.beforeKey}
351
375
  </text>
@@ -363,14 +387,7 @@ function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
363
387
  const lineColor = props.colorByDirection ? (rose ? stroke : MUTED) : stroke;
364
388
  return (
365
389
  <g key={`${row.label}-${i}`}>
366
- <line
367
- x1={leftX}
368
- y1={y1}
369
- x2={rightX}
370
- y2={y2}
371
- stroke={lineColor}
372
- strokeWidth={1.5}
373
- />
390
+ <line x1={leftX} y1={y1} x2={rightX} y2={y2} stroke={lineColor} strokeWidth={1.5} />
374
391
  <circle cx={leftX} cy={y1} r={2.5} fill={lineColor} />
375
392
  <circle cx={rightX} cy={y2} r={2.5} fill={lineColor} />
376
393
  <text x={leftX - 8} y={y1} textAnchor="end" dominantBaseline="central" fontSize={11} fill={INK}>
@@ -25,7 +25,13 @@ export const tufteChartComponentDefinitions = {
25
25
  'Word-sized sparkline: a single trend line with no axes, grid, or labels. Optional end dot, min/max markers, light area fill, and an inline last value. The canonical Tufte primitive for showing a trajectory in minimal space.',
26
26
  example: {
27
27
  title: 'Latency p95',
28
- data: [{ t: 0, ms: 120 }, { t: 1, ms: 138 }, { t: 2, ms: 117 }, { t: 3, ms: 152 }, { t: 4, ms: 109 }],
28
+ data: [
29
+ { t: 0, ms: 120 },
30
+ { t: 1, ms: 138 },
31
+ { t: 2, ms: 117 },
32
+ { t: 3, ms: 152 },
33
+ { t: 4, ms: 109 },
34
+ ],
29
35
  valueKey: 'ms',
30
36
  color: null,
31
37
  fill: true,
@@ -107,7 +113,7 @@ export const tufteChartComponentDefinitions = {
107
113
  height: z.number().nullable(),
108
114
  }),
109
115
  description:
110
- "Tufte slopegraph: two value columns (before/after) with a connecting line per category. Lines use one neutral ink by default; set colorByDirection to accent rising lines and mute falling ones. Ideal for paired change across many items.",
116
+ 'Tufte slopegraph: two value columns (before/after) with a connecting line per category. Lines use one neutral ink by default; set colorByDirection to accent rising lines and mute falling ones. Ideal for paired change across many items.',
111
117
  example: {
112
118
  title: 'Coverage before/after refactor',
113
119
  data: [