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
@@ -35,13 +35,13 @@ import {
35
35
  type JsonRenderNodeInput,
36
36
  type JsonRenderSpec,
37
37
  } from '../json-render/server.js';
38
- import {
39
- fetchWebpageSnapshot,
40
- getWebpageFetchErrorDetails,
41
- normalizeWebpageUrl,
42
- } from './webpage-node.js';
38
+ import { fetchWebpageSnapshot, getWebpageFetchErrorDetails, normalizeWebpageUrl } from './webpage-node.js';
43
39
  import { validateLocalImageFile } from './image-source.js';
44
- import { buildExcalidrawRestoreCheckpointToolInput, ensureExcalidrawCheckpointId, isExcalidrawCreateView } from './diagram-presets.js';
40
+ import {
41
+ buildExcalidrawRestoreCheckpointToolInput,
42
+ ensureExcalidrawCheckpointId,
43
+ isExcalidrawCreateView,
44
+ } from './diagram-presets.js';
45
45
 
46
46
  export type CanvasArrangeMode = 'grid' | 'column' | 'flow';
47
47
  export type CanvasPinMode = 'set' | 'add' | 'remove';
@@ -175,13 +175,16 @@ function isJsonRenderSpecLike(value: unknown): boolean {
175
175
  }
176
176
 
177
177
  function isGraphPayloadLike(value: unknown): value is Record<string, unknown> {
178
- return isRecord(value) && !isJsonRenderSpecLike(value) && (
179
- Array.isArray(value.data) || typeof value.graphType === 'string'
178
+ return (
179
+ isRecord(value) &&
180
+ !isJsonRenderSpecLike(value) &&
181
+ (Array.isArray(value.data) || typeof value.graphType === 'string')
180
182
  );
181
183
  }
182
184
 
183
185
  function hasGraphUpdateFields(input: Record<string, unknown>): boolean {
184
- return input.graphType !== undefined ||
186
+ return (
187
+ input.graphType !== undefined ||
185
188
  input.type !== undefined ||
186
189
  Array.isArray(input.data) ||
187
190
  input.xKey !== undefined ||
@@ -200,18 +203,13 @@ function hasGraphUpdateFields(input: Record<string, unknown>): boolean {
200
203
  input.color !== undefined ||
201
204
  input.barColor !== undefined ||
202
205
  input.lineColor !== undefined ||
203
- input.chartHeight !== undefined;
206
+ input.chartHeight !== undefined
207
+ );
204
208
  }
205
209
 
206
210
  function graphUpdateInput(input: CanvasStructuredNodeUpdateInput): CanvasGraphNodeUpdateInput {
207
211
  const data = pickGraphData(input as Record<string, unknown>, 'data');
208
- const {
209
- data: _data,
210
- content: _content,
211
- arrangeLocked: _arrangeLocked,
212
- chartHeight,
213
- ...graphFields
214
- } = input;
212
+ const { data: _data, content: _content, arrangeLocked: _arrangeLocked, chartHeight, ...graphFields } = input;
215
213
  return {
216
214
  ...graphFields,
217
215
  ...(data ? { data } : {}),
@@ -251,10 +249,13 @@ export function buildStructuredNodeUpdate(
251
249
  throw new Error('json-render structured updates require a spec.');
252
250
  }
253
251
  return {
254
- data: mergeNodeDataFields(buildJsonRenderNodeUpdate(node, {
255
- ...(typeof input.title === 'string' ? { title: input.title } : {}),
256
- spec: input.spec,
257
- }).data, input),
252
+ data: mergeNodeDataFields(
253
+ buildJsonRenderNodeUpdate(node, {
254
+ ...(typeof input.title === 'string' ? { title: input.title } : {}),
255
+ spec: input.spec,
256
+ }).data,
257
+ input,
258
+ ),
258
259
  };
259
260
  }
260
261
 
@@ -264,7 +265,9 @@ export function buildStructuredNodeUpdate(
264
265
  };
265
266
  }
266
267
 
267
- throw new Error(`Structured spec and graph updates can only be used with json-render or graph nodes, not ${node.type} nodes.`);
268
+ throw new Error(
269
+ `Structured spec and graph updates can only be used with json-render or graph nodes, not ${node.type} nodes.`,
270
+ );
268
271
  }
269
272
 
270
273
  function graphConfigToInput(config: Record<string, unknown>, fallbackTitle: string): GraphNodeInput | null {
@@ -296,7 +299,8 @@ function graphConfigToInput(config: Record<string, unknown>, fallbackTitle: stri
296
299
 
297
300
  function mergeGraphInput(source: Record<string, unknown>, fallback: GraphNodeInput | null): GraphNodeInput {
298
301
  const data = pickGraphData(source, 'data') ?? fallback?.data;
299
- if (!data) throw new Error('Graph update requires a data array, either in the update payload or the existing graphConfig.');
302
+ if (!data)
303
+ throw new Error('Graph update requires a data array, either in the update payload or the existing graphConfig.');
300
304
  return {
301
305
  title: pickString(source, 'title') ?? fallback?.title ?? 'Graph',
302
306
  graphType: pickString(source, 'graphType') ?? pickString(source, 'type') ?? fallback?.graphType ?? 'line',
@@ -304,24 +308,48 @@ function mergeGraphInput(source: Record<string, unknown>, fallback: GraphNodeInp
304
308
  ...((pickString(source, 'xKey') ?? fallback?.xKey) ? { xKey: pickString(source, 'xKey') ?? fallback?.xKey } : {}),
305
309
  ...((pickString(source, 'yKey') ?? fallback?.yKey) ? { yKey: pickString(source, 'yKey') ?? fallback?.yKey } : {}),
306
310
  ...((pickString(source, 'zKey') ?? fallback?.zKey) ? { zKey: pickString(source, 'zKey') ?? fallback?.zKey } : {}),
307
- ...((pickString(source, 'nameKey') ?? fallback?.nameKey) ? { nameKey: pickString(source, 'nameKey') ?? fallback?.nameKey } : {}),
308
- ...((pickString(source, 'valueKey') ?? fallback?.valueKey) ? { valueKey: pickString(source, 'valueKey') ?? fallback?.valueKey } : {}),
311
+ ...((pickString(source, 'nameKey') ?? fallback?.nameKey)
312
+ ? { nameKey: pickString(source, 'nameKey') ?? fallback?.nameKey }
313
+ : {}),
314
+ ...((pickString(source, 'valueKey') ?? fallback?.valueKey)
315
+ ? { valueKey: pickString(source, 'valueKey') ?? fallback?.valueKey }
316
+ : {}),
309
317
  ...(typeof source.showLegend === 'boolean' || typeof fallback?.showLegend === 'boolean'
310
318
  ? { showLegend: typeof source.showLegend === 'boolean' ? source.showLegend : fallback?.showLegend }
311
319
  : {}),
312
320
  ...(typeof source.showLabels === 'boolean' || typeof fallback?.showLabels === 'boolean'
313
321
  ? { showLabels: typeof source.showLabels === 'boolean' ? source.showLabels : fallback?.showLabels }
314
322
  : {}),
315
- ...((pickString(source, 'axisKey') ?? fallback?.axisKey) ? { axisKey: pickString(source, 'axisKey') ?? fallback?.axisKey } : {}),
316
- ...((pickStringArray(source, 'metrics') ?? fallback?.metrics) ? { metrics: pickStringArray(source, 'metrics') ?? fallback?.metrics } : {}),
317
- ...((pickStringArray(source, 'series') ?? fallback?.series) ? { series: pickStringArray(source, 'series') ?? fallback?.series } : {}),
318
- ...((pickString(source, 'barKey') ?? fallback?.barKey) ? { barKey: pickString(source, 'barKey') ?? fallback?.barKey } : {}),
319
- ...((pickString(source, 'lineKey') ?? fallback?.lineKey) ? { lineKey: pickString(source, 'lineKey') ?? fallback?.lineKey } : {}),
320
- ...((pickAggregate(source, 'aggregate') ?? fallback?.aggregate) ? { aggregate: pickAggregate(source, 'aggregate') ?? fallback?.aggregate } : {}),
321
- ...((pickString(source, 'color') ?? fallback?.color) ? { color: pickString(source, 'color') ?? fallback?.color } : {}),
322
- ...((pickString(source, 'barColor') ?? fallback?.barColor) ? { barColor: pickString(source, 'barColor') ?? fallback?.barColor } : {}),
323
- ...((pickString(source, 'lineColor') ?? fallback?.lineColor) ? { lineColor: pickString(source, 'lineColor') ?? fallback?.lineColor } : {}),
324
- ...((pickNumber(source, 'height') ?? fallback?.height) !== undefined ? { height: pickNumber(source, 'height') ?? fallback?.height } : {}),
323
+ ...((pickString(source, 'axisKey') ?? fallback?.axisKey)
324
+ ? { axisKey: pickString(source, 'axisKey') ?? fallback?.axisKey }
325
+ : {}),
326
+ ...((pickStringArray(source, 'metrics') ?? fallback?.metrics)
327
+ ? { metrics: pickStringArray(source, 'metrics') ?? fallback?.metrics }
328
+ : {}),
329
+ ...((pickStringArray(source, 'series') ?? fallback?.series)
330
+ ? { series: pickStringArray(source, 'series') ?? fallback?.series }
331
+ : {}),
332
+ ...((pickString(source, 'barKey') ?? fallback?.barKey)
333
+ ? { barKey: pickString(source, 'barKey') ?? fallback?.barKey }
334
+ : {}),
335
+ ...((pickString(source, 'lineKey') ?? fallback?.lineKey)
336
+ ? { lineKey: pickString(source, 'lineKey') ?? fallback?.lineKey }
337
+ : {}),
338
+ ...((pickAggregate(source, 'aggregate') ?? fallback?.aggregate)
339
+ ? { aggregate: pickAggregate(source, 'aggregate') ?? fallback?.aggregate }
340
+ : {}),
341
+ ...((pickString(source, 'color') ?? fallback?.color)
342
+ ? { color: pickString(source, 'color') ?? fallback?.color }
343
+ : {}),
344
+ ...((pickString(source, 'barColor') ?? fallback?.barColor)
345
+ ? { barColor: pickString(source, 'barColor') ?? fallback?.barColor }
346
+ : {}),
347
+ ...((pickString(source, 'lineColor') ?? fallback?.lineColor)
348
+ ? { lineColor: pickString(source, 'lineColor') ?? fallback?.lineColor }
349
+ : {}),
350
+ ...((pickNumber(source, 'height') ?? fallback?.height) !== undefined
351
+ ? { height: pickNumber(source, 'height') ?? fallback?.height }
352
+ : {}),
325
353
  };
326
354
  }
327
355
 
@@ -351,9 +379,7 @@ export function buildGraphNodeUpdate(
351
379
  const fallback = graphConfigToInput(currentConfig, fallbackTitle);
352
380
  const source = isGraphPayloadLike(input.spec)
353
381
  ? input.spec
354
- : Object.fromEntries(
355
- Object.entries(input).filter(([key, value]) => key !== 'spec' && value !== undefined),
356
- );
382
+ : Object.fromEntries(Object.entries(input).filter(([key, value]) => key !== 'spec' && value !== undefined));
357
383
 
358
384
  if (input.spec !== undefined && !isGraphPayloadLike(input.spec)) {
359
385
  const spec = normalizeAndValidateJsonRenderSpec(input.spec);
@@ -531,7 +557,8 @@ function prepareExtAppNodesForSessionSync(forceRehydrate: boolean): string[] {
531
557
  setExtAppRuntimeState(node.id, {
532
558
  appSessionId: null,
533
559
  sessionStatus: 'error',
534
- sessionError: 'Saved app session cannot be restored because its transport details are missing. Reopen the app to restore interactivity.',
560
+ sessionError:
561
+ 'Saved app session cannot be restored because its transport details are missing. Reopen the app to restore interactivity.',
535
562
  });
536
563
  continue;
537
564
  }
@@ -549,9 +576,7 @@ function prepareExtAppNodesForSessionSync(forceRehydrate: boolean): string[] {
549
576
  return targetIds;
550
577
  }
551
578
 
552
- export function primeCanvasRuntimeBackends(
553
- options: { forceRehydrateExtApps?: boolean } = {},
554
- ): { targetIds: string[] } {
579
+ export function primeCanvasRuntimeBackends(options: { forceRehydrateExtApps?: boolean } = {}): { targetIds: string[] } {
555
580
  const forceRehydrateExtApps = options.forceRehydrateExtApps === true;
556
581
  rewatchAllFileNodes();
557
582
 
@@ -574,11 +599,13 @@ export function primeCanvasRuntimeBackends(
574
599
  export async function syncCanvasRuntimeBackends(
575
600
  options: { forceRehydrateExtApps?: boolean; alreadyPrimed?: boolean } = {},
576
601
  ): Promise<{ rehydrated: number; failed: number }> {
577
- const targetIds = options.alreadyPrimed === true
578
- ? canvasState.getLayout().nodes
579
- .filter((node) => isExtAppNode(node) && node.data.sessionStatus === 'rehydrating')
580
- .map((node) => node.id)
581
- : primeCanvasRuntimeBackends(options).targetIds;
602
+ const targetIds =
603
+ options.alreadyPrimed === true
604
+ ? canvasState
605
+ .getLayout()
606
+ .nodes.filter((node) => isExtAppNode(node) && node.data.sessionStatus === 'rehydrating')
607
+ .map((node) => node.id)
608
+ : primeCanvasRuntimeBackends(options).targetIds;
582
609
  let rehydrated = 0;
583
610
  let failed = 0;
584
611
 
@@ -593,7 +620,8 @@ export async function syncCanvasRuntimeBackends(
593
620
  setExtAppRuntimeState(nodeId, {
594
621
  appSessionId: null,
595
622
  sessionStatus: 'error',
596
- sessionError: 'Saved app session cannot be restored because its launch metadata is incomplete. Reopen the app to restore interactivity.',
623
+ sessionError:
624
+ 'Saved app session cannot be restored because its launch metadata is incomplete. Reopen the app to restore interactivity.',
597
625
  });
598
626
  });
599
627
  failed++;
@@ -696,9 +724,8 @@ function buildFileNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
696
724
  };
697
725
  }
698
726
 
699
- const rawPath = typeof input.data?.path === 'string' && input.data.path.length > 0
700
- ? input.data.path
701
- : (input.content ?? '');
727
+ const rawPath =
728
+ typeof input.data?.path === 'string' && input.data.path.length > 0 ? input.data.path : (input.content ?? '');
702
729
  const resolved = resolve(rawPath);
703
730
  const fileName = resolved.split('/').pop() ?? rawPath;
704
731
  const data: Record<string, unknown> = {
@@ -754,20 +781,20 @@ function buildImageNodeData(input: CanvasAddNodeInput): Record<string, unknown>
754
781
  return {
755
782
  ...(input.data ?? {}),
756
783
  src,
757
- title: input.title ?? (isUrl ? src.split('/').pop() ?? 'Image' : 'Image'),
784
+ title: input.title ?? (isUrl ? (src.split('/').pop() ?? 'Image') : 'Image'),
758
785
  };
759
786
  }
760
787
 
761
788
  function buildWebpageNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
762
- const rawUrl = typeof input.data?.url === 'string' && input.data.url.length > 0
763
- ? input.data.url
764
- : (input.content ?? '');
789
+ const rawUrl =
790
+ typeof input.data?.url === 'string' && input.data.url.length > 0 ? input.data.url : (input.content ?? '');
765
791
  const url = normalizeWebpageUrl(rawUrl);
766
- const explicitTitle = typeof input.title === 'string' && input.title.trim().length > 0
767
- ? input.title.trim()
768
- : typeof input.data?.title === 'string' && input.data.title.trim().length > 0
769
- ? input.data.title.trim()
770
- : '';
792
+ const explicitTitle =
793
+ typeof input.title === 'string' && input.title.trim().length > 0
794
+ ? input.title.trim()
795
+ : typeof input.data?.title === 'string' && input.data.title.trim().length > 0
796
+ ? input.data.title.trim()
797
+ : '';
771
798
 
772
799
  return {
773
800
  ...(input.data ?? {}),
@@ -873,9 +900,10 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
873
900
 
874
901
  const width = input.width ?? input.defaultWidth ?? 720;
875
902
  const height = input.height ?? input.defaultHeight ?? 600;
876
- const position = input.x !== undefined && input.y !== undefined
877
- ? { x: input.x, y: input.y }
878
- : findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
903
+ const position =
904
+ input.x !== undefined && input.y !== undefined
905
+ ? { x: input.x, y: input.y }
906
+ : findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
879
907
  const id = `node-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
880
908
  const data = buildNodeData(input);
881
909
  const node: CanvasNodeState = {
@@ -901,13 +929,15 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
901
929
  return { id, node: storedNode, needsCodeGraphRecompute: input.type === 'file' };
902
930
  }
903
931
 
904
- export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput): {
905
- ok: true;
906
- node: CanvasNodeState;
907
- } | {
908
- ok: false;
909
- error: string;
910
- } {
932
+ export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput):
933
+ | {
934
+ ok: true;
935
+ node: CanvasNodeState;
936
+ }
937
+ | {
938
+ ok: false;
939
+ error: string;
940
+ } {
911
941
  if (typeof nodeRef.id === 'string' && nodeRef.id.trim().length > 0) {
912
942
  const node = canvasState.getNode(nodeRef.id.trim());
913
943
  if (!node) {
@@ -940,7 +970,10 @@ export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput): {
940
970
  if (matches.length > 1) {
941
971
  return {
942
972
  ok: false,
943
- error: `Search "${query}" is ambiguous. Matches: ${matches.slice(0, 5).map((match) => `${match.title ?? match.id} (${match.id})`).join(', ')}`,
973
+ error: `Search "${query}" is ambiguous. Matches: ${matches
974
+ .slice(0, 5)
975
+ .map((match) => `${match.title ?? match.id} (${match.id})`)
976
+ .join(', ')}`,
944
977
  };
945
978
  }
946
979
  const node = canvasState.getNode(matches[0]!.id);
@@ -963,11 +996,12 @@ export async function refreshCanvasWebpageNode(
963
996
  }
964
997
 
965
998
  const currentData = existing.data;
966
- const configuredUrl = typeof options.url === 'string' && options.url.trim().length > 0
967
- ? options.url
968
- : typeof currentData.url === 'string'
969
- ? currentData.url
970
- : '';
999
+ const configuredUrl =
1000
+ typeof options.url === 'string' && options.url.trim().length > 0
1001
+ ? options.url
1002
+ : typeof currentData.url === 'string'
1003
+ ? currentData.url
1004
+ : '';
971
1005
 
972
1006
  let normalizedUrl: string;
973
1007
  try {
@@ -1005,9 +1039,8 @@ export async function refreshCanvasWebpageNode(
1005
1039
  const latestData = latest.data;
1006
1040
  const titleSource = latestData.titleSource === 'user' ? 'user' : 'page';
1007
1041
  const currentTitle = typeof latestData.title === 'string' ? latestData.title.trim() : '';
1008
- const nextTitle = titleSource === 'user' && currentTitle.length > 0
1009
- ? currentTitle
1010
- : snapshot.pageTitle ?? snapshot.url;
1042
+ const nextTitle =
1043
+ titleSource === 'user' && currentTitle.length > 0 ? currentTitle : (snapshot.pageTitle ?? snapshot.url);
1011
1044
 
1012
1045
  const nextData: Record<string, unknown> = {
1013
1046
  ...latestData,
@@ -1203,9 +1236,9 @@ function shiftGridUpdatesBelowObstacles(
1203
1236
  const deltaY = blockerBottom + GRID_OBSTACLE_GAP_Y - minPlannedY;
1204
1237
  if (deltaY <= 0) return shifted;
1205
1238
 
1206
- shifted = shifted.map((update) => update.position
1207
- ? { ...update, position: { x: update.position.x, y: update.position.y + deltaY } }
1208
- : update);
1239
+ shifted = shifted.map((update) =>
1240
+ update.position ? { ...update, position: { x: update.position.x, y: update.position.y + deltaY } } : update,
1241
+ );
1209
1242
  }
1210
1243
 
1211
1244
  return shifted;
@@ -1213,9 +1246,8 @@ function shiftGridUpdatesBelowObstacles(
1213
1246
 
1214
1247
  export function arrangeCanvasNodes(layout: CanvasArrangeMode): { arranged: number; layout: CanvasArrangeMode } {
1215
1248
  const nodes = canvasState.getLayoutForPersistence().nodes;
1216
- const excludedIds = layout === 'grid'
1217
- ? collectGridArrangeExcludedNodeIds(nodes)
1218
- : collectArrangeExcludedNodeIds(nodes);
1249
+ const excludedIds =
1250
+ layout === 'grid' ? collectGridArrangeExcludedNodeIds(nodes) : collectArrangeExcludedNodeIds(nodes);
1219
1251
  const movableNodes = nodes.filter((node) => !excludedIds.has(node.id));
1220
1252
  const oldPositions = nodes.map((node) => ({ id: node.id, position: { ...node.position } }));
1221
1253
  const oldSizes = nodes.map((node) => ({ id: node.id, size: { ...node.size } }));
@@ -1268,26 +1300,34 @@ export function arrangeCanvasNodes(layout: CanvasArrangeMode): { arranged: numbe
1268
1300
  mutationHistory.record({
1269
1301
  description: `Auto-arranged ${movableNodes.length} nodes (${layout})`,
1270
1302
  operationType: 'arrange',
1271
- forward: () => canvasState.withSuppressedRecording(() => {
1272
- canvasState.applyUpdates(newPositions.map((position) => {
1273
- const size = newSizesById.get(position.id);
1274
- return {
1275
- id: position.id,
1276
- position: position.position,
1277
- ...(size ? { size } : {}),
1278
- };
1279
- }), layout === 'grid' ? { skipGroupChildTranslation: true } : {});
1280
- }),
1281
- inverse: () => canvasState.withSuppressedRecording(() => {
1282
- canvasState.applyUpdates(oldPositions.map((position) => {
1283
- const size = oldSizesById.get(position.id);
1284
- return {
1285
- id: position.id,
1286
- position: position.position,
1287
- ...(size ? { size } : {}),
1288
- };
1289
- }), layout === 'grid' ? { skipGroupChildTranslation: true } : {});
1290
- }),
1303
+ forward: () =>
1304
+ canvasState.withSuppressedRecording(() => {
1305
+ canvasState.applyUpdates(
1306
+ newPositions.map((position) => {
1307
+ const size = newSizesById.get(position.id);
1308
+ return {
1309
+ id: position.id,
1310
+ position: position.position,
1311
+ ...(size ? { size } : {}),
1312
+ };
1313
+ }),
1314
+ layout === 'grid' ? { skipGroupChildTranslation: true } : {},
1315
+ );
1316
+ }),
1317
+ inverse: () =>
1318
+ canvasState.withSuppressedRecording(() => {
1319
+ canvasState.applyUpdates(
1320
+ oldPositions.map((position) => {
1321
+ const size = oldSizesById.get(position.id);
1322
+ return {
1323
+ id: position.id,
1324
+ position: position.position,
1325
+ ...(size ? { size } : {}),
1326
+ };
1327
+ }),
1328
+ layout === 'grid' ? { skipGroupChildTranslation: true } : {},
1329
+ );
1330
+ }),
1291
1331
  });
1292
1332
 
1293
1333
  return { arranged: movableNodes.length, layout };
@@ -1302,7 +1342,8 @@ export function setCanvasContextPins(
1302
1342
  nodeIds: string[],
1303
1343
  mode: CanvasPinMode = 'set',
1304
1344
  ): { count: number; nodeIds: string[] } {
1305
- const normalizePins = (ids: string[]): string[] => ids.filter((id, index) => ids.indexOf(id) === index).slice(0, MAX_CONTEXT_PINS);
1345
+ const normalizePins = (ids: string[]): string[] =>
1346
+ ids.filter((id, index) => ids.indexOf(id) === index).slice(0, MAX_CONTEXT_PINS);
1306
1347
  const normalizedNodeIds = normalizePins(nodeIds);
1307
1348
  if (mode === 'set') {
1308
1349
  canvasState.setContextPins(normalizedNodeIds);
@@ -1344,7 +1385,9 @@ export function deleteCanvasSnapshot(id: string): { ok: boolean } {
1344
1385
  return { ok: canvasState.deleteSnapshot(id) };
1345
1386
  }
1346
1387
 
1347
- export function gcCanvasSnapshots(options?: Parameters<typeof canvasState.gcSnapshots>[0]): ReturnType<typeof canvasState.gcSnapshots> {
1388
+ export function gcCanvasSnapshots(
1389
+ options?: Parameters<typeof canvasState.gcSnapshots>[0],
1390
+ ): ReturnType<typeof canvasState.gcSnapshots> {
1348
1391
  return canvasState.gcSnapshots(options);
1349
1392
  }
1350
1393
 
@@ -1399,7 +1442,8 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): { id: string;
1399
1442
  let y = input.y;
1400
1443
  let width = input.width ?? 600;
1401
1444
  let height = input.height ?? 400;
1402
- const explicitFrame = input.x !== undefined || input.y !== undefined || input.width !== undefined || input.height !== undefined;
1445
+ const explicitFrame =
1446
+ input.x !== undefined || input.y !== undefined || input.width !== undefined || input.height !== undefined;
1403
1447
 
1404
1448
  const childIds = input.childIds ?? [];
1405
1449
  if (childIds.length > 0 && x === undefined && y === undefined) {
@@ -1415,9 +1459,10 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): { id: string;
1415
1459
  }
1416
1460
  }
1417
1461
 
1418
- const position = x !== undefined && y !== undefined
1419
- ? { x, y }
1420
- : findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
1462
+ const position =
1463
+ x !== undefined && y !== undefined
1464
+ ? { x, y }
1465
+ : findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
1421
1466
 
1422
1467
  const id = `group-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
1423
1468
  const data: Record<string, unknown> = {
@@ -1480,9 +1525,12 @@ export function clearCanvas(): { ok: boolean } {
1480
1525
  return { ok: true };
1481
1526
  }
1482
1527
 
1483
- export function createCanvasJsonRenderNode(
1484
- input: JsonRenderNodeInput,
1485
- ): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState } {
1528
+ export function createCanvasJsonRenderNode(input: JsonRenderNodeInput): {
1529
+ id: string;
1530
+ url: string;
1531
+ spec: JsonRenderSpec;
1532
+ node: CanvasNodeState;
1533
+ } {
1486
1534
  const spec = normalizeAndValidateJsonRenderSpec(input.spec);
1487
1535
  const width = input.width ?? JSON_RENDER_NODE_SIZE.width;
1488
1536
  const height = input.height ?? JSON_RENDER_NODE_SIZE.height;
@@ -1561,7 +1609,14 @@ export function appendCanvasJsonRenderStream(
1561
1609
  patches: unknown[],
1562
1610
  done: boolean,
1563
1611
  ):
1564
- | { ok: true; applied: number; skipped: number; specVersion: number; elementCount: number; streamStatus: 'open' | 'closed' }
1612
+ | {
1613
+ ok: true;
1614
+ applied: number;
1615
+ skipped: number;
1616
+ specVersion: number;
1617
+ elementCount: number;
1618
+ streamStatus: 'open' | 'closed';
1619
+ }
1565
1620
  | { ok: false; error: string } {
1566
1621
  const node = canvasState.getNode(nodeId);
1567
1622
  if (!node) return { ok: false, error: `Node "${nodeId}" not found.` };
@@ -1577,14 +1632,16 @@ export function appendCanvasJsonRenderStream(
1577
1632
  data: { ...node.data, spec, specVersion, streamStatus },
1578
1633
  });
1579
1634
 
1580
- const elementCount =
1581
- spec.elements && typeof spec.elements === 'object' ? Object.keys(spec.elements).length : 0;
1635
+ const elementCount = spec.elements && typeof spec.elements === 'object' ? Object.keys(spec.elements).length : 0;
1582
1636
  return { ok: true, applied, skipped, specVersion, elementCount, streamStatus };
1583
1637
  }
1584
1638
 
1585
- export function createCanvasGraphNode(
1586
- input: GraphNodeInput,
1587
- ): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState } {
1639
+ export function createCanvasGraphNode(input: GraphNodeInput): {
1640
+ id: string;
1641
+ url: string;
1642
+ spec: JsonRenderSpec;
1643
+ node: CanvasNodeState;
1644
+ } {
1588
1645
  const title = input.title?.trim() || 'Graph';
1589
1646
  const spec = buildGraphSpec(input);
1590
1647
  const width = input.width ?? GRAPH_NODE_SIZE.width;
@@ -225,10 +225,7 @@ export function inferCanvasNodeProvenance(
225
225
  return null;
226
226
  }
227
227
 
228
- export function normalizeCanvasNodeData<T extends Record<string, unknown>>(
229
- nodeType: CanvasNodeType,
230
- data: T,
231
- ): T {
228
+ export function normalizeCanvasNodeData<T extends Record<string, unknown>>(nodeType: CanvasNodeType, data: T): T {
232
229
  const semanticData = nodeType === 'html' ? normalizeHtmlNodeSemanticData(data) : data;
233
230
  const existing = normalizeExistingProvenance(semanticData.provenance);
234
231
  const inferred = inferCanvasNodeProvenance(nodeType, semanticData);