pmx-canvas 0.6.4 → 0.7.0

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 (204) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +67 -3
  2. package/CHANGELOG.md +73 -0
  3. package/Readme.md +35 -5
  4. package/dist/canvas/global.css +498 -28
  5. package/dist/canvas/index.js +110 -95
  6. package/dist/canvas/mermaid-entry.js +258 -258
  7. package/dist/types/cli/agent.d.ts +4 -0
  8. package/dist/types/cli/commands/backup.d.ts +1 -0
  9. package/dist/types/cli/commands/boards.d.ts +1 -0
  10. package/dist/types/cli/commands/export.d.ts +1 -0
  11. package/dist/types/cli/commands/record.d.ts +12 -0
  12. package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
  13. package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
  14. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
  15. package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
  16. package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
  17. package/dist/types/client/canvas/HomeView.d.ts +9 -0
  18. package/dist/types/client/canvas/Presentation.d.ts +13 -0
  19. package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
  20. package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
  21. package/dist/types/client/nodes/BoardNode.d.ts +5 -0
  22. package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
  23. package/dist/types/client/state/boards-store.d.ts +79 -0
  24. package/dist/types/client/state/canvas-store.d.ts +2 -0
  25. package/dist/types/client/state/intent-bridge.d.ts +1 -0
  26. package/dist/types/client/state/presentation.d.ts +3 -0
  27. package/dist/types/client/state/session-store.d.ts +7 -0
  28. package/dist/types/client/types.d.ts +2 -2
  29. package/dist/types/json-render/catalog.d.ts +3 -3
  30. package/dist/types/json-render/charts/definitions.d.ts +1 -1
  31. package/dist/types/json-render/server.d.ts +8 -0
  32. package/dist/types/mcp/canvas-access.d.ts +10 -3
  33. package/dist/types/mcp/context-uri-template.d.ts +6 -0
  34. package/dist/types/server/agent-presence.d.ts +1 -1
  35. package/dist/types/server/attribution.d.ts +11 -0
  36. package/dist/types/server/ax-context.d.ts +3 -1
  37. package/dist/types/server/ax-interaction.d.ts +2 -2
  38. package/dist/types/server/ax-state.d.ts +13 -3
  39. package/dist/types/server/backup.d.ts +36 -0
  40. package/dist/types/server/board-export.d.ts +45 -0
  41. package/dist/types/server/canvas-db.d.ts +65 -9
  42. package/dist/types/server/canvas-operations.d.ts +45 -0
  43. package/dist/types/server/canvas-provenance.d.ts +1 -1
  44. package/dist/types/server/canvas-schema.d.ts +1 -0
  45. package/dist/types/server/canvas-state.d.ts +146 -3
  46. package/dist/types/server/canvas-validation.d.ts +3 -0
  47. package/dist/types/server/context-brief.d.ts +111 -0
  48. package/dist/types/server/context-reads.d.ts +55 -0
  49. package/dist/types/server/document-import.d.ts +66 -0
  50. package/dist/types/server/html-surface.d.ts +1 -1
  51. package/dist/types/server/index.d.ts +107 -4
  52. package/dist/types/server/mutation-history.d.ts +1 -0
  53. package/dist/types/server/operations/invoker.d.ts +2 -0
  54. package/dist/types/server/operations/mcp.d.ts +7 -1
  55. package/dist/types/server/operations/ops/app.d.ts +2 -0
  56. package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
  57. package/dist/types/server/operations/ops/backup.d.ts +2 -0
  58. package/dist/types/server/operations/ops/boards.d.ts +10 -0
  59. package/dist/types/server/operations/ops/export.d.ts +2 -0
  60. package/dist/types/server/operations/ops/imports.d.ts +2 -0
  61. package/dist/types/server/operations/ops/json-render.d.ts +2 -0
  62. package/dist/types/server/operations/ops/nodes.d.ts +4 -2
  63. package/dist/types/server/operations/ops/query.d.ts +2 -0
  64. package/dist/types/server/operations/registry.d.ts +9 -1
  65. package/dist/types/server/operations/types.d.ts +1 -1
  66. package/dist/types/server/server.d.ts +1 -0
  67. package/dist/types/server/trace-manager.d.ts +7 -0
  68. package/dist/types/server/web-artifacts.d.ts +3 -0
  69. package/dist/types/server/webpage-node.d.ts +22 -1
  70. package/dist/types/server/workbench-auth.d.ts +3 -0
  71. package/dist/types/shared/boards.d.ts +6 -0
  72. package/dist/types/shared/tour.d.ts +72 -0
  73. package/docs/RELEASE.md +5 -0
  74. package/docs/cli.md +157 -0
  75. package/docs/http-api.md +277 -6
  76. package/docs/mcp.md +96 -7
  77. package/docs/node-types.md +106 -0
  78. package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
  79. package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
  80. package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
  81. package/docs/plans/plan-013-static-board-export.md +47 -0
  82. package/docs/plans/plan-014-connected-board-memory.md +140 -0
  83. package/docs/product-vision-2026-09.md +118 -18
  84. package/docs/sdk.md +50 -0
  85. package/package.json +1 -1
  86. package/skills/pmx-canvas/SKILL.md +62 -5
  87. package/skills/pmx-canvas/references/full-reference.md +44 -8
  88. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
  89. package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
  90. package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
  91. package/skills/pmx-canvas-testing/SKILL.md +4 -0
  92. package/src/cli/agent.ts +9 -0
  93. package/src/cli/commands/ax.ts +13 -0
  94. package/src/cli/commands/backup.ts +66 -0
  95. package/src/cli/commands/boards.ts +71 -0
  96. package/src/cli/commands/export.ts +30 -0
  97. package/src/cli/commands/nodes.ts +14 -1
  98. package/src/cli/commands/pins.ts +6 -2
  99. package/src/cli/commands/pump.ts +3 -0
  100. package/src/cli/commands/query.ts +33 -7
  101. package/src/cli/commands/record.ts +218 -0
  102. package/src/cli/commands/view.ts +7 -5
  103. package/src/cli/index.ts +21 -9
  104. package/src/client/App.tsx +28 -4
  105. package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
  106. package/src/client/canvas/BoardCopyDialog.tsx +153 -0
  107. package/src/client/canvas/BoardFolderDialog.tsx +120 -0
  108. package/src/client/canvas/BoardLinkDialog.tsx +93 -0
  109. package/src/client/canvas/BoardSwitcher.tsx +149 -0
  110. package/src/client/canvas/CanvasNode.tsx +8 -0
  111. package/src/client/canvas/CanvasViewport.tsx +3 -0
  112. package/src/client/canvas/CommandPalette.tsx +107 -4
  113. package/src/client/canvas/ContextMenu.tsx +4 -0
  114. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
  115. package/src/client/canvas/ExportDialog.tsx +197 -0
  116. package/src/client/canvas/HomeView.tsx +277 -0
  117. package/src/client/canvas/IntentLayer.tsx +9 -2
  118. package/src/client/canvas/Presentation.tsx +108 -0
  119. package/src/client/canvas/SessionPanel.tsx +28 -1
  120. package/src/client/canvas/ToolRail.tsx +38 -3
  121. package/src/client/canvas/TopBar.tsx +49 -7
  122. package/src/client/canvas/import-files.ts +19 -1
  123. package/src/client/canvas/kind-colors.ts +1 -0
  124. package/src/client/canvas/use-node-drag.ts +8 -3
  125. package/src/client/icons.tsx +2 -0
  126. package/src/client/mermaid-entry.ts +39 -0
  127. package/src/client/nodes/AttachmentNode.tsx +148 -0
  128. package/src/client/nodes/BoardNode.tsx +35 -0
  129. package/src/client/nodes/FileNode.tsx +6 -0
  130. package/src/client/nodes/HtmlNode.tsx +12 -13
  131. package/src/client/nodes/MarkdownNode.tsx +47 -19
  132. package/src/client/nodes/McpAppNode.tsx +105 -13
  133. package/src/client/nodes/MermaidNode.tsx +3 -2
  134. package/src/client/nodes/ax-node-actions.ts +2 -5
  135. package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
  136. package/src/client/state/boards-store.ts +167 -0
  137. package/src/client/state/canvas-store.ts +22 -0
  138. package/src/client/state/intent-bridge.ts +30 -2
  139. package/src/client/state/intent-store.ts +5 -1
  140. package/src/client/state/node-factory.ts +1 -0
  141. package/src/client/state/presentation.ts +7 -0
  142. package/src/client/state/session-store.ts +8 -0
  143. package/src/client/state/sse-bridge.ts +52 -5
  144. package/src/client/theme/global.css +498 -28
  145. package/src/client/types.ts +2 -0
  146. package/src/json-render/server.ts +45 -9
  147. package/src/mcp/canvas-access.ts +59 -22
  148. package/src/mcp/context-uri-template.ts +28 -0
  149. package/src/mcp/server.ts +122 -15
  150. package/src/server/agent-context.ts +116 -29
  151. package/src/server/agent-presence.ts +15 -9
  152. package/src/server/attribution.ts +29 -0
  153. package/src/server/ax-context.ts +4 -2
  154. package/src/server/ax-interaction.ts +1 -0
  155. package/src/server/ax-state-manager.ts +72 -3
  156. package/src/server/ax-state.ts +41 -0
  157. package/src/server/backup.ts +178 -0
  158. package/src/server/board-export.ts +686 -0
  159. package/src/server/canvas-db.ts +612 -105
  160. package/src/server/canvas-operations.ts +183 -31
  161. package/src/server/canvas-provenance.ts +1 -0
  162. package/src/server/canvas-schema.ts +22 -0
  163. package/src/server/canvas-state.ts +812 -48
  164. package/src/server/canvas-validation.ts +9 -1
  165. package/src/server/context-brief.ts +372 -0
  166. package/src/server/context-reads.ts +238 -0
  167. package/src/server/demo-state.json +670 -9
  168. package/src/server/document-import.ts +223 -0
  169. package/src/server/html-primitives.ts +199 -91
  170. package/src/server/html-surface.ts +8 -3
  171. package/src/server/index.ts +243 -5
  172. package/src/server/mutation-history.ts +10 -13
  173. package/src/server/operations/composites.ts +29 -5
  174. package/src/server/operations/http.ts +9 -3
  175. package/src/server/operations/index.ts +8 -0
  176. package/src/server/operations/invoker.ts +14 -3
  177. package/src/server/operations/mcp.ts +50 -11
  178. package/src/server/operations/ops/app.ts +15 -0
  179. package/src/server/operations/ops/ax-read.ts +10 -6
  180. package/src/server/operations/ops/ax-timeline.ts +75 -0
  181. package/src/server/operations/ops/backup.ts +134 -0
  182. package/src/server/operations/ops/batch.ts +20 -4
  183. package/src/server/operations/ops/boards.ts +351 -0
  184. package/src/server/operations/ops/export.ts +74 -0
  185. package/src/server/operations/ops/ext-app.ts +4 -0
  186. package/src/server/operations/ops/groups.ts +14 -7
  187. package/src/server/operations/ops/imports.ts +220 -0
  188. package/src/server/operations/ops/json-render.ts +37 -5
  189. package/src/server/operations/ops/nodes.ts +99 -21
  190. package/src/server/operations/ops/query.ts +206 -9
  191. package/src/server/operations/ops/viewport.ts +32 -0
  192. package/src/server/operations/registry.ts +129 -31
  193. package/src/server/operations/types.ts +1 -1
  194. package/src/server/scope-fence.ts +1 -1
  195. package/src/server/server.ts +155 -8
  196. package/src/server/spatial-analysis.ts +9 -17
  197. package/src/server/trace-manager.ts +19 -0
  198. package/src/server/web-artifacts.ts +11 -3
  199. package/src/server/webpage-node.ts +173 -13
  200. package/src/server/workbench-auth.ts +11 -0
  201. package/src/server/workboard.ts +3 -3
  202. package/src/shared/boards.ts +18 -0
  203. package/src/shared/node-sizes.ts +1 -0
  204. package/src/shared/tour.ts +87 -0
@@ -5,6 +5,7 @@ import { recomputeCodeGraph } from './code-graph.js';
5
5
  import { diffLayouts } from './mutation-history.js';
6
6
  import {
7
7
  canvasState,
8
+ defaultBoardName,
8
9
  type CanvasEdge,
9
10
  type CanvasNodeState,
10
11
  type CanvasNodeUpdate,
@@ -20,6 +21,7 @@ import {
20
21
  type ExternalMcpTransportConfig,
21
22
  } from './mcp-app-runtime.js';
22
23
  import { mutationHistory } from './mutation-history.js';
24
+ import { intentRegistry } from './intent-registry.js';
23
25
  import { clampCreateNodeSize } from './canvas-validation.js';
24
26
  import { computeGroupBounds, findOpenCanvasPosition } from './placement.js';
25
27
  import { searchNodes } from './spatial-analysis.js';
@@ -33,6 +35,7 @@ import {
33
35
  emptyStreamingSpec,
34
36
  GRAPH_NODE_SIZE,
35
37
  inferJsonRenderNodeTitle,
38
+ jsonRenderFormWarnings,
36
39
  JSON_RENDER_NODE_SIZE,
37
40
  normalizeAndValidateJsonRenderSpec,
38
41
  type GraphNodeInput,
@@ -118,6 +121,29 @@ export const MCP_APP_NODE_DEFAULT_SIZE = { width: 960, height: 600 };
118
121
  export const IMAGE_NODE_DEFAULT_SIZE = { width: 480, height: 360 };
119
122
  export const LEDGER_NODE_DEFAULT_SIZE = { width: 420, height: 280 };
120
123
 
124
+ export interface CanvasSizeAdjustment {
125
+ requested: { width: number | null; height: number | null };
126
+ applied: { width: number; height: number };
127
+ reason: 'defaulted' | 'clamped-to-minimum' | 'defaulted-and-clamped-to-minimum' | 'fit-to-children';
128
+ }
129
+
130
+ export function describeCreateSize(
131
+ requestedWidth: number | undefined,
132
+ requestedHeight: number | undefined,
133
+ resolvedWidth: number,
134
+ resolvedHeight: number,
135
+ applied: { width: number; height: number },
136
+ ): CanvasSizeAdjustment | undefined {
137
+ const defaulted = requestedWidth === undefined || requestedHeight === undefined;
138
+ const clamped = applied.width !== resolvedWidth || applied.height !== resolvedHeight;
139
+ if (!defaulted && !clamped) return undefined;
140
+ return {
141
+ requested: { width: requestedWidth ?? null, height: requestedHeight ?? null },
142
+ applied,
143
+ reason: defaulted && clamped ? 'defaulted-and-clamped-to-minimum' : clamped ? 'clamped-to-minimum' : 'defaulted',
144
+ };
145
+ }
146
+
121
147
  // Per-type minimum creation sizes + clamp live in canvas-validation.ts
122
148
  // (NODE_MIN_CREATE_SIZES / clampCreateNodeSize) — shared with the validate
123
149
  // surface's sizeWarnings.
@@ -621,6 +647,7 @@ export function primeCanvasRuntimeBackends(options: { forceRehydrateExtApps?: bo
621
647
  export async function syncCanvasRuntimeBackends(
622
648
  options: { forceRehydrateExtApps?: boolean; alreadyPrimed?: boolean } = {},
623
649
  ): Promise<{ rehydrated: number; failed: number }> {
650
+ const boardGeneration = canvasState.boardGeneration;
624
651
  const targetIds =
625
652
  options.alreadyPrimed === true
626
653
  ? canvasState
@@ -632,6 +659,7 @@ export async function syncCanvasRuntimeBackends(
632
659
  let failed = 0;
633
660
 
634
661
  for (const nodeId of targetIds) {
662
+ if (canvasState.boardGeneration !== boardGeneration) break;
635
663
  const current = canvasState.getNode(nodeId);
636
664
  if (!isExtAppNode(current)) continue;
637
665
 
@@ -659,6 +687,10 @@ export async function syncCanvasRuntimeBackends(
659
687
  ? { serverName: current.data.serverName.trim() }
660
688
  : {}),
661
689
  });
690
+ if (canvasState.boardGeneration !== boardGeneration) {
691
+ closeMcpAppSession(opened.sessionId);
692
+ continue;
693
+ }
662
694
  const toolInput = resolveExtAppRehydratedToolInput(current, opened.toolInput);
663
695
  const storedCheckpointId = getStoredExcalidrawCheckpointId(current);
664
696
  const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
@@ -683,6 +715,7 @@ export async function syncCanvasRuntimeBackends(
683
715
  });
684
716
  rehydrated++;
685
717
  } catch (error) {
718
+ if (canvasState.boardGeneration !== boardGeneration) continue;
686
719
  canvasState.withSuppressedRecording(() => {
687
720
  setExtAppRuntimeState(nodeId, {
688
721
  appSessionId: null,
@@ -923,17 +956,15 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
923
956
  id: string;
924
957
  node: CanvasNodeState;
925
958
  needsCodeGraphRecompute: boolean;
959
+ sizeAdjustment?: CanvasSizeAdjustment;
926
960
  } {
927
961
  if (input.type === 'json-render' || input.type === 'graph') {
928
962
  throw new Error(`Use the dedicated ${input.type} node APIs for structured viewer nodes.`);
929
963
  }
930
964
 
931
- const { width, height } = clampCreateNodeSize(
932
- input.type,
933
- input.width ?? input.defaultWidth ?? 720,
934
- input.height ?? input.defaultHeight ?? 600,
935
- input.strictSize,
936
- );
965
+ const resolvedWidth = input.width ?? input.defaultWidth ?? 720;
966
+ const resolvedHeight = input.height ?? input.defaultHeight ?? 600;
967
+ const { width, height } = clampCreateNodeSize(input.type, resolvedWidth, resolvedHeight, input.strictSize);
937
968
  const position =
938
969
  input.x !== undefined && input.y !== undefined
939
970
  ? { x: input.x, y: input.y }
@@ -959,7 +990,16 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
959
990
  watchFileForNode(id, filePath);
960
991
  }
961
992
 
962
- return { id, node: storedNode, needsCodeGraphRecompute: input.type === 'file' };
993
+ const sizeAdjustment = describeCreateSize(input.width, input.height, resolvedWidth, resolvedHeight, {
994
+ width,
995
+ height,
996
+ });
997
+ return {
998
+ id,
999
+ node: storedNode,
1000
+ needsCodeGraphRecompute: input.type === 'file',
1001
+ ...(sizeAdjustment ? { sizeAdjustment } : {}),
1002
+ };
963
1003
  }
964
1004
 
965
1005
  export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput):
@@ -1029,6 +1069,7 @@ export async function refreshCanvasWebpageNode(
1029
1069
  }
1030
1070
 
1031
1071
  const currentData = existing.data;
1072
+ const boardGeneration = canvasState.boardGeneration;
1032
1073
  const configuredUrl =
1033
1074
  typeof options.url === 'string' && options.url.trim().length > 0
1034
1075
  ? options.url
@@ -1054,16 +1095,11 @@ export async function refreshCanvasWebpageNode(
1054
1095
  };
1055
1096
  }
1056
1097
 
1057
- const fetchingData: Record<string, unknown> = {
1058
- ...currentData,
1059
- url: normalizedUrl,
1060
- status: 'fetching',
1061
- };
1062
- delete fetchingData.error;
1063
- canvasState.updateNode(id, { data: fetchingData });
1064
-
1065
1098
  try {
1066
1099
  const snapshot = await fetchWebpageSnapshot(normalizedUrl);
1100
+ if (canvasState.boardGeneration !== boardGeneration) {
1101
+ return { ok: false, id, error: 'The board changed during webpage refresh.' };
1102
+ }
1067
1103
  const latest = canvasState.getNode(id);
1068
1104
  if (!latest || latest.type !== 'webpage') {
1069
1105
  return { ok: false, id, error: `Webpage node "${id}" disappeared during refresh.` };
@@ -1097,6 +1133,9 @@ export async function refreshCanvasWebpageNode(
1097
1133
  canvasState.updateNode(id, { data: nextData });
1098
1134
  return { ok: true, id };
1099
1135
  } catch (error) {
1136
+ if (canvasState.boardGeneration !== boardGeneration) {
1137
+ return { ok: false, id, error: 'The board changed during webpage refresh.' };
1138
+ }
1100
1139
  const details = getWebpageFetchErrorDetails(error);
1101
1140
  const latest = canvasState.getNode(id);
1102
1141
  if (latest?.type === 'webpage') {
@@ -1415,7 +1454,8 @@ export function saveCanvasSnapshotWithReuse(name: string): { snapshot: CanvasSna
1415
1454
  diff.removedNodes.length === 0 &&
1416
1455
  diff.modifiedNodes.length === 0 &&
1417
1456
  diff.addedEdges.length === 0 &&
1418
- diff.removedEdges.length === 0;
1457
+ diff.removedEdges.length === 0 &&
1458
+ JSON.stringify(newestData.tour) === JSON.stringify(canvasState.getTour());
1419
1459
  if (identical) return { snapshot: newest, reused: true };
1420
1460
  }
1421
1461
  }
@@ -1430,14 +1470,65 @@ export async function restoreCanvasSnapshot(idOrName: string): Promise<{ ok: boo
1430
1470
  const ok = canvasState.restoreSnapshot(idOrName);
1431
1471
  if (ok) {
1432
1472
  primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
1473
+ const boardGeneration = canvasState.boardGeneration;
1433
1474
  void syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true }).finally(() => {
1434
- emitCanvasLayoutUpdate();
1475
+ if (canvasState.boardGeneration === boardGeneration) emitCanvasLayoutUpdate();
1435
1476
  });
1436
1477
  canvasState.flushToDisk();
1437
1478
  }
1438
1479
  return { ok };
1439
1480
  }
1440
1481
 
1482
+ /**
1483
+ * Open a board (or Home for null): the state layer saves the current board and
1484
+ * loads the next, and everything else that holds the old board in memory is
1485
+ * reset — undo closures, pending intents, file watchers, app sessions and the
1486
+ * code graph (the trace chain restarts itself when its last node is gone).
1487
+ * Agent and human presence are kept: they belong to the people and agents,
1488
+ * not to a board.
1489
+ */
1490
+ export function openCanvasBoard(id: string | null): { ok: boolean } {
1491
+ if (!canvasState.switchBoard(id)) return { ok: false };
1492
+ mutationHistory.reset();
1493
+ intentRegistry.reset();
1494
+ startBoardRuntime();
1495
+ return { ok: true };
1496
+ }
1497
+
1498
+ function startBoardRuntime(): void {
1499
+ primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
1500
+ scheduleCodeGraphRecompute();
1501
+ emitCanvasLayoutUpdate();
1502
+ // App rehydration can take a network round trip; it finishes in the background.
1503
+ const boardGeneration = canvasState.boardGeneration;
1504
+ void syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true }).finally(() => {
1505
+ if (canvasState.boardGeneration === boardGeneration) emitCanvasLayoutUpdate();
1506
+ });
1507
+ }
1508
+
1509
+ /**
1510
+ * Restore the whole library from a backup file (the current file is kept as
1511
+ * `<db>.before-restore`); the restored library opens on its last open board.
1512
+ */
1513
+ export function restoreCanvasLibrary(file: string): void {
1514
+ canvasState.replaceDatabase(file);
1515
+ mutationHistory.reset();
1516
+ intentRegistry.reset();
1517
+ startBoardRuntime();
1518
+ }
1519
+
1520
+ /**
1521
+ * A write that arrives while no board is open (Home) lands on the most recently
1522
+ * opened board, or on a new board when the workspace has none — nothing on
1523
+ * Home is displaced, because Home holds nothing. Intents and undo history are
1524
+ * kept: whatever was signalled on Home was aimed at the board this opens.
1525
+ */
1526
+ export function openBoardForWrite(): void {
1527
+ if (canvasState.activeBoardId) return;
1528
+ const recent = canvasState.listBoards()[0] ?? canvasState.createBoard(defaultBoardName());
1529
+ if (recent && canvasState.switchBoard(recent.id)) startBoardRuntime();
1530
+ }
1531
+
1441
1532
  export function deleteCanvasSnapshot(id: string): { ok: boolean } {
1442
1533
  return { ok: canvasState.deleteSnapshot(id) };
1443
1534
  }
@@ -1508,7 +1599,11 @@ export function removeCanvasEdge(id: string): { removed: boolean } {
1508
1599
  return { removed: canvasState.removeEdge(id) };
1509
1600
  }
1510
1601
 
1511
- export function createCanvasGroup(input: CanvasCreateGroupInput): { id: string; node: CanvasNodeState } {
1602
+ export function createCanvasGroup(input: CanvasCreateGroupInput): {
1603
+ id: string;
1604
+ node: CanvasNodeState;
1605
+ sizeAdjustment?: CanvasSizeAdjustment;
1606
+ } {
1512
1607
  let x = input.x;
1513
1608
  let y = input.y;
1514
1609
  let width = input.width ?? 600;
@@ -1566,22 +1661,42 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): { id: string;
1566
1661
  if (!node) {
1567
1662
  throw new Error(`Group "${id}" was not created.`);
1568
1663
  }
1569
- return { id, node };
1664
+ const adjusted = input.width !== node.size.width || input.height !== node.size.height;
1665
+ const fitted = childIds.length > 0 && ((input.x === undefined && input.y === undefined) || !explicitFrame);
1666
+ return {
1667
+ id,
1668
+ node,
1669
+ ...(adjusted
1670
+ ? {
1671
+ sizeAdjustment: {
1672
+ requested: { width: input.width ?? null, height: input.height ?? null },
1673
+ applied: node.size,
1674
+ reason: fitted ? ('fit-to-children' as const) : ('defaulted' as const),
1675
+ },
1676
+ }
1677
+ : {}),
1678
+ };
1570
1679
  }
1571
1680
 
1572
1681
  export function groupCanvasNodes(
1573
1682
  groupId: string,
1574
1683
  childIds: string[],
1575
1684
  options: { childLayout?: CanvasArrangeMode } = {},
1576
- ): { ok: boolean } {
1685
+ ): { ok: boolean; sizeAdjustment?: CanvasSizeAdjustment } {
1686
+ const before = canvasState.getNode(groupId)?.size;
1687
+ const ok = canvasState.groupNodes(groupId, childIds, {
1688
+ // Preserve existing child positions unless an explicit layout is asked
1689
+ // for — matching createCanvasGroup and the batch group.add path. Without
1690
+ // this, grouping silently auto-packs the nodes into a grid.
1691
+ preservePositions: options.childLayout === undefined,
1692
+ ...(options.childLayout ? { layout: options.childLayout } : {}),
1693
+ });
1694
+ const applied = canvasState.getNode(groupId)?.size;
1577
1695
  return {
1578
- ok: canvasState.groupNodes(groupId, childIds, {
1579
- // Preserve existing child positions unless an explicit layout is asked
1580
- // for — matching createCanvasGroup and the batch group.add path. Without
1581
- // this, grouping silently auto-packs the nodes into a grid.
1582
- preservePositions: options.childLayout === undefined,
1583
- ...(options.childLayout ? { layout: options.childLayout } : {}),
1584
- }),
1696
+ ok,
1697
+ ...(before && applied && (before.width !== applied.width || before.height !== applied.height)
1698
+ ? { sizeAdjustment: { requested: before, applied, reason: 'fit-to-children' as const } }
1699
+ : {}),
1585
1700
  };
1586
1701
  }
1587
1702
 
@@ -1600,6 +1715,8 @@ export function createCanvasJsonRenderNode(input: JsonRenderNodeInput): {
1600
1715
  url: string;
1601
1716
  spec: JsonRenderSpec;
1602
1717
  node: CanvasNodeState;
1718
+ warnings: string[];
1719
+ sizeAdjustment?: CanvasSizeAdjustment;
1603
1720
  } {
1604
1721
  const spec = normalizeAndValidateJsonRenderSpec(input.spec);
1605
1722
  const { width, height } = clampCreateNodeSize(
@@ -1628,7 +1745,24 @@ export function createCanvasJsonRenderNode(input: JsonRenderNodeInput): {
1628
1745
  };
1629
1746
 
1630
1747
  canvasState.addJsonRenderNode(node);
1631
- return { id, url: String(node.data.url), spec, node };
1748
+ const sizeAdjustment = describeCreateSize(
1749
+ input.width,
1750
+ input.height,
1751
+ input.width ?? JSON_RENDER_NODE_SIZE.width,
1752
+ input.height ?? JSON_RENDER_NODE_SIZE.height,
1753
+ {
1754
+ width,
1755
+ height,
1756
+ },
1757
+ );
1758
+ return {
1759
+ id,
1760
+ url: String(node.data.url),
1761
+ spec,
1762
+ node,
1763
+ warnings: jsonRenderFormWarnings(spec),
1764
+ ...(sizeAdjustment ? { sizeAdjustment } : {}),
1765
+ };
1632
1766
  }
1633
1767
 
1634
1768
  /**
@@ -1643,7 +1777,7 @@ export function createCanvasStreamingJsonRenderNode(input: {
1643
1777
  width?: number;
1644
1778
  height?: number;
1645
1779
  strictSize?: boolean;
1646
- }): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState } {
1780
+ }): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState; sizeAdjustment?: CanvasSizeAdjustment } {
1647
1781
  const spec = emptyStreamingSpec();
1648
1782
  const { width, height } = clampCreateNodeSize(
1649
1783
  'json-render',
@@ -1673,7 +1807,17 @@ export function createCanvasStreamingJsonRenderNode(input: {
1673
1807
  };
1674
1808
 
1675
1809
  canvasState.addJsonRenderNode(node);
1676
- return { id, url: String(node.data.url), spec, node };
1810
+ const sizeAdjustment = describeCreateSize(
1811
+ input.width,
1812
+ input.height,
1813
+ input.width ?? JSON_RENDER_NODE_SIZE.width,
1814
+ input.height ?? JSON_RENDER_NODE_SIZE.height,
1815
+ {
1816
+ width,
1817
+ height,
1818
+ },
1819
+ );
1820
+ return { id, url: String(node.data.url), spec, node, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
1677
1821
  }
1678
1822
 
1679
1823
  /**
@@ -1717,6 +1861,7 @@ export function createCanvasGraphNode(input: GraphNodeInput): {
1717
1861
  url: string;
1718
1862
  spec: JsonRenderSpec;
1719
1863
  node: CanvasNodeState;
1864
+ sizeAdjustment?: CanvasSizeAdjustment;
1720
1865
  } {
1721
1866
  const title = input.title?.trim() || 'Graph';
1722
1867
  const spec = buildGraphSpec(input);
@@ -1747,7 +1892,14 @@ export function createCanvasGraphNode(input: GraphNodeInput): {
1747
1892
  };
1748
1893
 
1749
1894
  canvasState.addGraphNode(node);
1750
- return { id, url: String(node.data.url), spec, node };
1895
+ const sizeAdjustment = describeCreateSize(
1896
+ input.width,
1897
+ input.heightPx,
1898
+ input.width ?? GRAPH_NODE_SIZE.width,
1899
+ input.heightPx ?? GRAPH_NODE_SIZE.height,
1900
+ { width, height },
1901
+ );
1902
+ return { id, url: String(node.data.url), spec, node, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
1751
1903
  }
1752
1904
 
1753
1905
  /**
@@ -7,6 +7,7 @@ export type CanvasNodeType =
7
7
  | 'webpage'
8
8
  | 'json-render'
9
9
  | 'graph'
10
+ | 'board'
10
11
  | 'prompt'
11
12
  | 'response'
12
13
  | 'status'
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { describeJsonRenderCatalog, type JsonRenderComponentDescriptor } from '../json-render/catalog.js';
4
4
  import {
5
5
  buildGraphSpec,
6
+ jsonRenderFormWarnings,
6
7
  normalizeAndValidateJsonRenderSpec,
7
8
  normalizeGraphType,
8
9
  type GraphNodeInput,
@@ -38,6 +39,7 @@ export interface StructuredValidationResult {
38
39
  ok: true;
39
40
  type: 'json-render' | 'graph' | 'html-primitive';
40
41
  normalizedSpec?: JsonRenderSpec;
42
+ warnings?: string[];
41
43
  normalizedPrimitive?: {
42
44
  kind: string;
43
45
  title: string;
@@ -105,6 +107,18 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
105
107
  'When a markdown node is path-backed (`data.path`), the server persists `data.provenance` so snapshots keep a refreshable file source.',
106
108
  ],
107
109
  },
110
+ {
111
+ type: 'board',
112
+ kind: 'node',
113
+ description: 'Text-only link to another board by durable id.',
114
+ endpoint: '/api/canvas/node',
115
+ mcpTool: 'canvas_node (action:"add")',
116
+ fields: [
117
+ { name: 'title', type: 'string', required: false, description: 'Pinned fallback title.' },
118
+ { name: 'data', type: 'object', required: true, description: 'Contains boardId, the durable target board id.' },
119
+ ],
120
+ example: { type: 'board', title: 'Research', data: { boardId: 'board-abc123' } },
121
+ },
108
122
  {
109
123
  type: 'status',
110
124
  kind: 'node',
@@ -235,6 +249,13 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
235
249
  fields: [
236
250
  { name: 'title', type: 'string', required: false, description: 'Optional node title.' },
237
251
  { name: 'content', type: 'string', required: false, description: 'Mermaid diagram source text.' },
252
+ {
253
+ name: 'data.fit',
254
+ type: '"contain" | "none"',
255
+ required: false,
256
+ description:
257
+ 'Default contain: shrink overflowing diagrams proportionally, without enlarging. none: 100% with scrolling. Open as site always uses 100%. CLI: --fit contain|none.',
258
+ },
238
259
  ],
239
260
  example: {
240
261
  type: 'mermaid',
@@ -1013,6 +1034,7 @@ export function validateStructuredCanvasPayload(input: {
1013
1034
  ok: true,
1014
1035
  type: 'json-render',
1015
1036
  normalizedSpec,
1037
+ warnings: jsonRenderFormWarnings(normalizedSpec),
1016
1038
  summary: {
1017
1039
  root: normalizedSpec.root,
1018
1040
  elementCount: Object.keys(normalizedSpec.elements).length,