pmx-canvas 0.8.2 → 0.9.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 (338) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +59 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2724 -476
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2899 -596
  8. package/dist/json-render/index.js +20 -20
  9. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  10. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  11. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  12. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  13. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  14. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  15. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  16. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  17. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  19. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  20. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  21. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  22. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  23. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  24. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  25. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  26. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  27. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  28. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  29. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  30. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  31. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  32. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  33. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  34. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  35. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  36. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  37. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  38. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  39. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  40. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  41. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  42. package/dist/types/client/icons.d.ts +0 -10
  43. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  44. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  45. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  46. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  47. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  48. package/dist/types/client/state/boards-store.d.ts +81 -9
  49. package/dist/types/client/state/canvas-store.d.ts +14 -1
  50. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  51. package/dist/types/client/state/context-status-store.d.ts +35 -0
  52. package/dist/types/client/state/human-store.d.ts +2 -1
  53. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  54. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  55. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  56. package/dist/types/client/state/presence-store.d.ts +4 -4
  57. package/dist/types/client/state/presentation.d.ts +28 -0
  58. package/dist/types/client/state/relation-store.d.ts +10 -0
  59. package/dist/types/client/state/session-store.d.ts +41 -2
  60. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  61. package/dist/types/client/types.d.ts +13 -1
  62. package/dist/types/client/utils/keyboard.d.ts +2 -0
  63. package/dist/types/client/utils/time-ago.d.ts +4 -0
  64. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  65. package/dist/types/server/agent-presence.d.ts +10 -3
  66. package/dist/types/server/ax-context.d.ts +1 -1
  67. package/dist/types/server/canvas-db.d.ts +30 -7
  68. package/dist/types/server/canvas-operations.d.ts +24 -2
  69. package/dist/types/server/canvas-provenance.d.ts +22 -2
  70. package/dist/types/server/canvas-serialization.d.ts +2 -0
  71. package/dist/types/server/canvas-state.d.ts +93 -15
  72. package/dist/types/server/canvas-validation.d.ts +1 -1
  73. package/dist/types/server/context-brief.d.ts +94 -6
  74. package/dist/types/server/context-reads.d.ts +33 -7
  75. package/dist/types/server/edit-summary.d.ts +20 -0
  76. package/dist/types/server/index.d.ts +71 -10
  77. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  78. package/dist/types/server/lifecycle-status.d.ts +10 -0
  79. package/dist/types/server/memory-graph.d.ts +9 -0
  80. package/dist/types/server/mutation-history.d.ts +2 -0
  81. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  82. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  83. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  84. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  85. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  86. package/dist/types/server/operations/ops/query.d.ts +23 -0
  87. package/dist/types/server/operations/registry.d.ts +1 -0
  88. package/dist/types/server/placement.d.ts +7 -0
  89. package/dist/types/server/scope-fence.d.ts +2 -1
  90. package/dist/types/server/spatial-analysis.d.ts +6 -3
  91. package/dist/types/server/tour-control.d.ts +32 -0
  92. package/dist/types/server/wiki-links.d.ts +14 -0
  93. package/dist/types/server/workbench-auth.d.ts +2 -0
  94. package/dist/types/shared/agent-context.d.ts +2 -0
  95. package/dist/types/shared/agent-presence.d.ts +15 -12
  96. package/dist/types/shared/auto-arrange.d.ts +1 -0
  97. package/dist/types/shared/boards.d.ts +4 -4
  98. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  99. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  100. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  101. package/dist/types/shared/memory-graph.d.ts +53 -0
  102. package/dist/types/shared/relations.d.ts +68 -0
  103. package/dist/types/shared/semantic-attention.d.ts +2 -0
  104. package/dist/types/shared/tour.d.ts +38 -0
  105. package/dist/types/shared/workspace-settings.d.ts +11 -0
  106. package/docs/RELEASE.md +21 -3
  107. package/docs/adr-001-bun-only-runtime.md +7 -1
  108. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  109. package/docs/api-reference.md +109 -0
  110. package/docs/ax-host-adapter-contract.md +1 -1
  111. package/docs/ax-state-contract.md +5 -4
  112. package/docs/cli.md +153 -15
  113. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  114. package/docs/design/AgentContext.dc.html +3 -3
  115. package/docs/design/BoardPins.dc.html +37 -22
  116. package/docs/design/Categories.dc.html +208 -0
  117. package/docs/design/CategoryEditor.dc.html +33 -0
  118. package/docs/design/Chrome.dc.html +52 -9
  119. package/docs/design/ContextChip.dc.html +224 -0
  120. package/docs/design/ContextChip600.dc.html +136 -0
  121. package/docs/design/EmptyState.dc.html +45 -0
  122. package/docs/design/Flows.dc.html +23 -1
  123. package/docs/design/HeaderMarks.dc.html +183 -0
  124. package/docs/design/Home.dc.html +19 -1
  125. package/docs/design/HomeGraph.dc.html +86 -202
  126. package/docs/design/HomeMap.dc.html +23 -1
  127. package/docs/design/HomeTeam.dc.html +32 -14
  128. package/docs/design/Lifecycle.dc.html +22 -0
  129. package/docs/design/Lifecycle600.dc.html +18 -0
  130. package/docs/design/LifecycleVariants.dc.html +157 -0
  131. package/docs/design/LinkAuthorship.dc.html +139 -0
  132. package/docs/design/LinksChanged.dc.html +203 -0
  133. package/docs/design/LinksOptions.dc.html +137 -0
  134. package/docs/design/LinksOptions600.dc.html +134 -0
  135. package/docs/design/NearPin.dc.html +9 -10
  136. package/docs/design/Pane600.dc.html +4 -3
  137. package/docs/design/Relations.dc.html +3 -2
  138. package/docs/design/Restore.dc.html +152 -0
  139. package/docs/design/SeeChange.dc.html +267 -0
  140. package/docs/design/Uses.dc.html +12 -6
  141. package/docs/design/canvas.json +146 -19
  142. package/docs/design.md +449 -16
  143. package/docs/environment.md +0 -1
  144. package/docs/evals/curation-effect.md +128 -0
  145. package/docs/http-api.md +459 -37
  146. package/docs/mcp.md +146 -18
  147. package/docs/node-types.md +58 -3
  148. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  149. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  150. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  151. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  152. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  153. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  154. package/docs/product-context-vision-2026-09.md +17 -1
  155. package/docs/product-vision-2026-09.md +1082 -43
  156. package/docs/product-vision-review-2026-09.md +13 -1
  157. package/docs/screenshots/workbench-live-dark.png +0 -0
  158. package/docs/screenshots/workbench-live-light.png +0 -0
  159. package/docs/sdk.md +62 -2
  160. package/package.json +1 -1
  161. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  162. package/plugins/pmx-canvas/plugin.json +1 -1
  163. package/plugins/pmx-canvas/runtime.json +1 -1
  164. package/plugins/pmx-canvas/tools.json +601 -22
  165. package/skills/pmx-canvas/SKILL.md +216 -42
  166. package/skills/pmx-canvas/evals/evals.json +224 -0
  167. package/skills/pmx-canvas/references/full-reference.md +120 -21
  168. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  169. package/skills/pmx-canvas/references/importing-context.md +132 -0
  170. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  171. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  172. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  173. package/src/cli/agent.ts +2 -0
  174. package/src/cli/commands/ax.ts +12 -0
  175. package/src/cli/commands/boards.ts +88 -11
  176. package/src/cli/commands/edges.ts +28 -0
  177. package/src/cli/commands/groups.ts +7 -0
  178. package/src/cli/commands/nodes.ts +1 -0
  179. package/src/cli/commands/query.ts +34 -2
  180. package/src/cli/commands/record.ts +199 -48
  181. package/src/cli/index.ts +6 -1
  182. package/src/client/App.tsx +177 -70
  183. package/src/client/canvas/AttentionHistory.tsx +0 -2
  184. package/src/client/canvas/BarHint.tsx +40 -2
  185. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  186. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  187. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  188. package/src/client/canvas/BoardPinButton.tsx +33 -0
  189. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  190. package/src/client/canvas/CanvasNode.tsx +360 -237
  191. package/src/client/canvas/CanvasViewport.tsx +45 -7
  192. package/src/client/canvas/CommandBar.tsx +29 -23
  193. package/src/client/canvas/CommandPalette.tsx +5 -12
  194. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  195. package/src/client/canvas/ContextChip.tsx +296 -0
  196. package/src/client/canvas/ContextMenu.tsx +63 -5
  197. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  198. package/src/client/canvas/EdgeLayer.tsx +339 -76
  199. package/src/client/canvas/EmptyState.tsx +33 -18
  200. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  201. package/src/client/canvas/ExternalWriters.tsx +57 -42
  202. package/src/client/canvas/HomeGraphView.tsx +942 -0
  203. package/src/client/canvas/HomeMapView.tsx +641 -0
  204. package/src/client/canvas/HomeReviewView.tsx +167 -0
  205. package/src/client/canvas/HomeView.tsx +687 -160
  206. package/src/client/canvas/IntentLayer.tsx +19 -12
  207. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  208. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  209. package/src/client/canvas/LifecycleFields.tsx +91 -0
  210. package/src/client/canvas/LifecyclePanel.tsx +543 -0
  211. package/src/client/canvas/MemoryInspector.tsx +104 -0
  212. package/src/client/canvas/Minimap.tsx +3 -3
  213. package/src/client/canvas/NearPinLayer.tsx +187 -0
  214. package/src/client/canvas/NodeContextMark.tsx +175 -97
  215. package/src/client/canvas/Presentation.tsx +23 -57
  216. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  217. package/src/client/canvas/RelationsPanel.tsx +82 -0
  218. package/src/client/canvas/SelectionBar.tsx +3 -6
  219. package/src/client/canvas/SessionPanel.tsx +90 -50
  220. package/src/client/canvas/SessionReceipt.tsx +311 -39
  221. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  222. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  223. package/src/client/canvas/ToolRail.tsx +367 -192
  224. package/src/client/canvas/TopBar.tsx +354 -124
  225. package/src/client/canvas/canvas-area.ts +10 -3
  226. package/src/client/canvas/graph-geometry.ts +195 -0
  227. package/src/client/canvas/graph-model.ts +254 -0
  228. package/src/client/canvas/memory-layout.ts +106 -0
  229. package/src/client/canvas/use-chrome-height.ts +20 -0
  230. package/src/client/canvas/use-focus-trap.ts +31 -5
  231. package/src/client/canvas/use-memory-graph.ts +58 -0
  232. package/src/client/canvas/use-node-drag.ts +2 -1
  233. package/src/client/canvas/use-pan-zoom.ts +64 -7
  234. package/src/client/icons.tsx +0 -63
  235. package/src/client/mcp-app/style.css +0 -6
  236. package/src/client/nodes/AxStepControls.tsx +74 -50
  237. package/src/client/nodes/BoardNode.tsx +5 -1
  238. package/src/client/nodes/ContextNode.tsx +18 -11
  239. package/src/client/nodes/ExtAppFrame.tsx +6 -3
  240. package/src/client/nodes/FileNode.tsx +46 -42
  241. package/src/client/nodes/HtmlNode.tsx +32 -0
  242. package/src/client/nodes/ImageNode.tsx +1 -1
  243. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  244. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  245. package/src/client/nodes/MarkdownNode.tsx +377 -109
  246. package/src/client/nodes/McpAppNode.tsx +50 -20
  247. package/src/client/nodes/MdFormatBar.tsx +21 -18
  248. package/src/client/nodes/PromptNode.tsx +1 -1
  249. package/src/client/nodes/ResponseNode.tsx +1 -1
  250. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  251. package/src/client/state/attention-bridge.ts +64 -15
  252. package/src/client/state/boards-store.ts +193 -39
  253. package/src/client/state/canvas-store.ts +99 -12
  254. package/src/client/state/context-chip-store.ts +70 -0
  255. package/src/client/state/context-status-store.ts +53 -0
  256. package/src/client/state/human-store.ts +13 -7
  257. package/src/client/state/intent-bridge.ts +51 -30
  258. package/src/client/state/knowledge-store.ts +43 -0
  259. package/src/client/state/near-pin-store.ts +115 -6
  260. package/src/client/state/presence-store.ts +7 -10
  261. package/src/client/state/presentation.ts +91 -0
  262. package/src/client/state/relation-store.ts +81 -0
  263. package/src/client/state/session-store.ts +144 -18
  264. package/src/client/state/sse-bridge.ts +235 -32
  265. package/src/client/state/workspace-settings.ts +11 -0
  266. package/src/client/theme/global.css +2724 -476
  267. package/src/client/types.ts +12 -0
  268. package/src/client/utils/keyboard.ts +10 -0
  269. package/src/client/utils/time-ago.ts +14 -0
  270. package/src/json-render/renderer/index.tsx +31 -0
  271. package/src/mcp/canvas-app.ts +12 -2
  272. package/src/mcp/context-uri-template.ts +7 -4
  273. package/src/mcp/server.ts +63 -41
  274. package/src/mcp/workbench-app.ts +39 -6
  275. package/src/server/agent-presence.ts +128 -28
  276. package/src/server/ax-context.ts +27 -13
  277. package/src/server/board-export.ts +12 -1
  278. package/src/server/canvas-db.ts +331 -42
  279. package/src/server/canvas-operations.ts +195 -83
  280. package/src/server/canvas-provenance.ts +53 -22
  281. package/src/server/canvas-serialization.ts +2 -0
  282. package/src/server/canvas-state.ts +950 -122
  283. package/src/server/canvas-validation.ts +9 -2
  284. package/src/server/context-brief.ts +345 -38
  285. package/src/server/context-reads.ts +178 -30
  286. package/src/server/demo-state.json +355 -24
  287. package/src/server/edit-summary.ts +74 -0
  288. package/src/server/html-surface.ts +8 -1
  289. package/src/server/index.ts +181 -11
  290. package/src/server/knowledge-lifecycle.ts +304 -0
  291. package/src/server/lifecycle-status.ts +232 -0
  292. package/src/server/memory-graph.ts +166 -0
  293. package/src/server/mutation-history.ts +18 -6
  294. package/src/server/operations/composites.ts +32 -11
  295. package/src/server/operations/http.ts +7 -2
  296. package/src/server/operations/index.ts +4 -0
  297. package/src/server/operations/mcp.ts +26 -1
  298. package/src/server/operations/ops/ax-presence.ts +1 -1
  299. package/src/server/operations/ops/ax-read.ts +57 -6
  300. package/src/server/operations/ops/ax-timeline.ts +28 -2
  301. package/src/server/operations/ops/backup.ts +5 -3
  302. package/src/server/operations/ops/batch.ts +35 -4
  303. package/src/server/operations/ops/boards.ts +188 -47
  304. package/src/server/operations/ops/canvas-wire.ts +31 -1
  305. package/src/server/operations/ops/edges.ts +36 -2
  306. package/src/server/operations/ops/groups.ts +6 -1
  307. package/src/server/operations/ops/lifecycle.ts +262 -0
  308. package/src/server/operations/ops/memory-graph.ts +45 -0
  309. package/src/server/operations/ops/nodes.ts +107 -24
  310. package/src/server/operations/ops/query.ts +281 -68
  311. package/src/server/operations/ops/snapshots.ts +73 -1
  312. package/src/server/operations/ops/validate.ts +5 -1
  313. package/src/server/operations/ops/viewport.ts +97 -3
  314. package/src/server/operations/registry.ts +74 -25
  315. package/src/server/placement.ts +29 -0
  316. package/src/server/scope-fence.ts +78 -6
  317. package/src/server/server.ts +115 -16
  318. package/src/server/spatial-analysis.ts +48 -23
  319. package/src/server/tour-control.ts +82 -0
  320. package/src/server/wiki-links.ts +137 -0
  321. package/src/server/workbench-auth.ts +11 -0
  322. package/src/shared/agent-context.ts +3 -0
  323. package/src/shared/agent-presence.ts +15 -15
  324. package/src/shared/auto-arrange.ts +2 -0
  325. package/src/shared/boards.ts +4 -4
  326. package/src/shared/knowledge-lifecycle.ts +22 -0
  327. package/src/shared/knowledge-presets.ts +80 -0
  328. package/src/shared/lifecycle-status.ts +26 -0
  329. package/src/shared/memory-graph.ts +74 -0
  330. package/src/shared/relations.ts +98 -0
  331. package/src/shared/semantic-attention.ts +15 -1
  332. package/src/shared/tour.ts +23 -5
  333. package/src/shared/workspace-settings.ts +13 -0
  334. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  335. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  336. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  337. package/dist/types/server/agent-context.d.ts +0 -22
  338. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import { basename, resolve } from 'node:path';
3
4
  import { agentPresence } from './agent-presence.js';
4
5
  import { recomputeCodeGraph } from './code-graph.js';
@@ -22,12 +23,14 @@ import {
22
23
  } from './mcp-app-runtime.js';
23
24
  import { mutationHistory } from './mutation-history.js';
24
25
  import { intentRegistry } from './intent-registry.js';
26
+ import { OperationError } from './operations/types.js';
25
27
  import { clampCreateNodeSize } from './canvas-validation.js';
26
28
  import { validCanvasNodeUpdates } from './canvas-geometry.js';
27
29
  import { computeGroupBounds, findOpenCanvasPosition } from './placement.js';
28
30
  import { searchNodes } from './spatial-analysis.js';
29
31
  import { getCanvasNodeTitle } from './canvas-serialization.js';
30
32
  import { computeAutoArrange } from '../shared/auto-arrange.js';
33
+ import { relationKindSchema, relationReasonSchema } from '../shared/relations.js';
31
34
  import {
32
35
  applyJsonRenderStreamPatches,
33
36
  buildGraphSpec,
@@ -93,6 +96,7 @@ export interface CanvasStructuredNodeUpdateInput extends Omit<CanvasGraphNodeUpd
93
96
  }
94
97
 
95
98
  interface CanvasAddNodeInput {
99
+ prepareOnlyNodes?: CanvasNodeState[];
96
100
  type: CanvasNodeState['type'];
97
101
  title?: string;
98
102
  content?: string;
@@ -150,6 +154,10 @@ export function describeCreateSize(
150
154
  // surface's sizeWarnings.
151
155
 
152
156
  interface CanvasCreateGroupInput {
157
+ destinationBoardId?: string;
158
+ requestId?: string;
159
+ expectedBoardId?: string | null;
160
+ intentId?: string;
153
161
  title?: string;
154
162
  childIds?: string[];
155
163
  x?: number;
@@ -163,6 +171,7 @@ interface CanvasCreateGroupInput {
163
171
  interface CanvasNodeLookupInput {
164
172
  id?: string;
165
173
  search?: string;
174
+ board?: string;
166
175
  }
167
176
 
168
177
  const MAX_CONTEXT_PINS = 20;
@@ -424,6 +433,7 @@ export function buildGraphNodeUpdate(
424
433
  const source = isGraphPayloadLike(input.spec)
425
434
  ? input.spec
426
435
  : Object.fromEntries(Object.entries(input).filter(([key, value]) => key !== 'spec' && value !== undefined));
436
+ const specVersion = (typeof node.data.specVersion === 'number' ? node.data.specVersion : 0) + 1;
427
437
 
428
438
  if (input.spec !== undefined && !isGraphPayloadLike(input.spec)) {
429
439
  const spec = normalizeAndValidateJsonRenderSpec(input.spec);
@@ -436,6 +446,7 @@ export function buildGraphNodeUpdate(
436
446
  ...createJsonRenderNodeData(node.id, title, spec, {
437
447
  viewerType: 'graph',
438
448
  graphConfig: currentConfig,
449
+ specVersion,
439
450
  }),
440
451
  },
441
452
  };
@@ -453,6 +464,7 @@ export function buildGraphNodeUpdate(
453
464
  ...createJsonRenderNodeData(node.id, title, spec, {
454
465
  viewerType: 'graph',
455
466
  graphConfig,
467
+ specVersion,
456
468
  }),
457
469
  },
458
470
  };
@@ -791,7 +803,7 @@ function buildImageNodeData(input: CanvasAddNodeInput): Record<string, unknown>
791
803
  }
792
804
 
793
805
  if (!isDataUri && !isUrl && src) {
794
- const resolved = resolve(src);
806
+ const resolved = resolve(canvasState.getWorkspaceRoot(), src);
795
807
  const fileName = basename(resolved) || src;
796
808
  const { mimeType } = validateLocalImageFile(resolved);
797
809
  return {
@@ -949,7 +961,7 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
949
961
  const position =
950
962
  input.x !== undefined && input.y !== undefined
951
963
  ? { x: input.x, y: input.y }
952
- : findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
964
+ : findOpenCanvasPosition(input.prepareOnlyNodes ?? canvasState.getLayout().nodes, width, height);
953
965
  const id = `node-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
954
966
  const data = buildNodeData(input);
955
967
  const node: CanvasNodeState = {
@@ -963,6 +975,7 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
963
975
  data,
964
976
  };
965
977
 
978
+ if (input.prepareOnlyNodes) return { id, node, needsCodeGraphRecompute: false };
966
979
  canvasState.addNode(node);
967
980
  const storedNode = canvasState.getNode(id) ?? node;
968
981
 
@@ -992,8 +1005,12 @@ export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput):
992
1005
  ok: false;
993
1006
  error: string;
994
1007
  } {
1008
+ const remote = nodeRef.board ? canvasState.readBoard(nodeRef.board, true) : null;
1009
+ if (nodeRef.board && !remote) return { ok: false, error: `Board "${nodeRef.board}" not found.` };
1010
+ const getNode = (id: string) =>
1011
+ remote ? remote.layout.nodes.find((node) => node.id === id) : canvasState.getNode(id);
995
1012
  if (typeof nodeRef.id === 'string' && nodeRef.id.trim().length > 0) {
996
- const node = canvasState.getNode(nodeRef.id.trim());
1013
+ const node = getNode(nodeRef.id.trim());
997
1014
  if (!node) {
998
1015
  return { ok: false, error: `Node "${nodeRef.id}" not found.` };
999
1016
  }
@@ -1002,7 +1019,7 @@ export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput):
1002
1019
 
1003
1020
  if (typeof nodeRef.search === 'string' && nodeRef.search.trim().length > 0) {
1004
1021
  const query = nodeRef.search.trim();
1005
- const layout = canvasState.getLayout();
1022
+ const layout = remote?.layout ?? canvasState.getLayout();
1006
1023
  const exactTitleMatches = layout.nodes.filter((node) => {
1007
1024
  const title = getCanvasNodeTitle(node);
1008
1025
  return title !== null && title.toLowerCase() === query.toLowerCase();
@@ -1030,7 +1047,7 @@ export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput):
1030
1047
  .join(', ')}`,
1031
1048
  };
1032
1049
  }
1033
- const node = canvasState.getNode(matches[0]!.id);
1050
+ const node = getNode(matches[0]!.id);
1034
1051
  if (!node) {
1035
1052
  return { ok: false, error: `Resolved node "${matches[0]!.id}" disappeared.` };
1036
1053
  }
@@ -1406,7 +1423,8 @@ export function setCanvasContextPins(
1406
1423
  canvasState.setContextPins(normalizePins([...current, ...normalizedNodeIds]), options);
1407
1424
  } else {
1408
1425
  const current = Array.from(canvasState.contextPinnedNodeIds);
1409
- canvasState.setContextPins(current.filter((id) => !normalizedNodeIds.includes(id)));
1426
+ // Limit live pins, not a merged receipt's list of IDs to remove.
1427
+ canvasState.setContextPins(current.filter((id) => !nodeIds.includes(id)));
1410
1428
  }
1411
1429
 
1412
1430
  return {
@@ -1522,50 +1540,76 @@ export function gcCanvasSnapshots(
1522
1540
  }
1523
1541
 
1524
1542
  export function addCanvasEdge(input: {
1543
+ destinationBoardId?: string;
1544
+ requestId?: string;
1545
+ expectedBoardId?: string | null;
1546
+ intentId?: string;
1525
1547
  from?: string;
1526
1548
  to?: string;
1549
+ toBoardId?: string;
1527
1550
  fromSearch?: string;
1528
1551
  toSearch?: string;
1529
1552
  type: CanvasEdge['type'];
1553
+ kind?: string;
1554
+ reason?: string;
1530
1555
  label?: string;
1531
1556
  style?: CanvasEdge['style'];
1532
1557
  animated?: boolean;
1533
1558
  }): CanvasEdge {
1534
- const fromResult = resolveCanvasNode({
1535
- ...(typeof input.from === 'string' ? { id: input.from } : {}),
1536
- ...(typeof input.fromSearch === 'string' ? { search: input.fromSearch } : {}),
1537
- });
1538
- if (!fromResult.ok) {
1539
- throw new Error(fromResult.error);
1540
- }
1541
- const toResult = resolveCanvasNode({
1542
- ...(typeof input.to === 'string' ? { id: input.to } : {}),
1543
- ...(typeof input.toSearch === 'string' ? { search: input.toSearch } : {}),
1544
- });
1545
- if (!toResult.ok) {
1546
- throw new Error(toResult.error);
1547
- }
1559
+ const stored =
1560
+ input.destinationBoardId !== undefined || input.requestId !== undefined
1561
+ ? storedCreationTarget(input, 'edge.add')
1562
+ : undefined;
1563
+ const build = (): CanvasEdge => {
1564
+ const kind = input.kind === undefined ? undefined : relationKindSchema.parse(input.kind);
1565
+ const reason = input.reason === undefined ? undefined : relationReasonSchema.parse(input.reason);
1566
+ const fromResult = resolveCanvasNode({
1567
+ ...(typeof input.from === 'string' ? { id: input.from } : {}),
1568
+ ...(typeof input.fromSearch === 'string' ? { search: input.fromSearch } : {}),
1569
+ ...(stored ? { board: stored.boardId } : {}),
1570
+ });
1571
+ if (!fromResult.ok) {
1572
+ throw new Error(fromResult.error);
1573
+ }
1574
+ const toResult = resolveCanvasNode({
1575
+ ...(typeof input.to === 'string' ? { id: input.to } : {}),
1576
+ ...(typeof input.toSearch === 'string' ? { search: input.toSearch } : {}),
1577
+ ...(input.toBoardId || stored ? { board: input.toBoardId ?? stored!.boardId } : {}),
1578
+ });
1579
+ if (!toResult.ok) {
1580
+ throw new Error(toResult.error);
1581
+ }
1548
1582
 
1549
- const id = `edge-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
1550
- const edge: CanvasEdge = {
1551
- id,
1552
- from: fromResult.node.id,
1553
- to: toResult.node.id,
1554
- type: input.type,
1555
- ...(input.label ? { label: input.label } : {}),
1556
- ...(input.style ? { style: input.style } : {}),
1557
- ...(input.animated !== undefined ? { animated: input.animated } : {}),
1583
+ const id = `edge-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
1584
+ const edge: CanvasEdge = {
1585
+ id,
1586
+ from: fromResult.node.id,
1587
+ to: toResult.node.id,
1588
+ ...(input.toBoardId && input.toBoardId !== (stored?.boardId ?? canvasState.activeBoardId)
1589
+ ? { toBoardId: input.toBoardId }
1590
+ : {}),
1591
+ type: input.type,
1592
+ ...(kind ? { kind } : {}),
1593
+ ...(reason ? { reason } : {}),
1594
+ ...(input.label ? { label: input.label } : {}),
1595
+ ...(input.style ? { style: input.style } : {}),
1596
+ ...(input.animated !== undefined ? { animated: input.animated } : {}),
1597
+ };
1598
+ return edge;
1558
1599
  };
1600
+ if (stored) return canvasState.createStoredEdge(stored.boardId, stored.requestId, stored.fingerprint, build);
1601
+ const edge = build();
1559
1602
  const added = canvasState.addEdge(edge);
1560
1603
  if (!added) {
1561
1604
  throw new Error('Duplicate or self-edge.');
1562
1605
  }
1563
- return edge;
1606
+ // The stored link, stamped with its author and revision — the browser replaces its copy with this.
1607
+ return canvasState.getEdges().find((stored) => stored.id === edge.id) ?? edge;
1564
1608
  }
1565
1609
 
1566
1610
  export function updateCanvasEdge(
1567
1611
  id: string,
1568
- patch: Partial<Pick<CanvasEdge, 'type' | 'label' | 'style' | 'animated'>>,
1612
+ patch: Partial<Pick<CanvasEdge, 'type' | 'kind' | 'reason' | 'label' | 'style' | 'animated'>>,
1569
1613
  ): CanvasEdge {
1570
1614
  if (!canvasState.getEdges().some((edge) => edge.id === id)) {
1571
1615
  throw new Error(`Edge "${id}" not found.`);
@@ -1586,52 +1630,73 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): {
1586
1630
  node: CanvasNodeState;
1587
1631
  sizeAdjustment?: CanvasSizeAdjustment;
1588
1632
  } {
1589
- let x = input.x;
1590
- let y = input.y;
1591
- let width = input.width ?? 600;
1592
- let height = input.height ?? 400;
1633
+ const stored =
1634
+ input.destinationBoardId !== undefined || input.requestId !== undefined
1635
+ ? storedCreationTarget(
1636
+ { ...input, title: input.title ?? 'Group', childIds: input.childIds ?? [] },
1637
+ 'group.create',
1638
+ )
1639
+ : undefined;
1593
1640
  const explicitFrame =
1594
1641
  input.x !== undefined || input.y !== undefined || input.width !== undefined || input.height !== undefined;
1595
-
1596
- const childIds = input.childIds ?? [];
1597
- if (childIds.length > 0 && x === undefined && y === undefined) {
1598
- const childRects = childIds
1599
- .map((cid) => canvasState.getNode(cid))
1600
- .filter((node): node is CanvasNodeState => node !== undefined);
1601
- const bounds = computeGroupBounds(childRects);
1602
- if (bounds) {
1603
- x = bounds.x;
1604
- y = bounds.y;
1605
- width = bounds.width;
1606
- height = bounds.height;
1642
+ const childIds = [...new Set(input.childIds ?? [])];
1643
+ const build = (nodes: CanvasNodeState[]): CanvasNodeState => {
1644
+ const missing = childIds.filter((id) => !nodes.some((node) => node.id === id));
1645
+ if (stored && missing.length)
1646
+ throw new OperationError(`Cannot create group: missing child node IDs: ${missing.join(', ')}.`);
1647
+ // Selecting both a frame and its descendant would create conflicting memberships.
1648
+ for (const id of stored ? childIds : []) {
1649
+ let parent = nodes.find((node) => node.id === id)?.data.parentGroup;
1650
+ while (typeof parent === 'string') {
1651
+ if (childIds.includes(parent)) throw new OperationError('Cannot group both an ancestor and its descendant.');
1652
+ parent = nodes.find((node) => node.id === parent)?.data.parentGroup;
1653
+ }
1654
+ }
1655
+ let x = input.x;
1656
+ let y = input.y;
1657
+ let width = input.width ?? 600;
1658
+ let height = input.height ?? 400;
1659
+ if (childIds.length > 0 && x === undefined && y === undefined) {
1660
+ const childRects = childIds
1661
+ .map((cid) => nodes.find((node) => node.id === cid))
1662
+ .filter((node): node is CanvasNodeState => node !== undefined);
1663
+ const bounds = computeGroupBounds(childRects);
1664
+ if (bounds) {
1665
+ x = bounds.x;
1666
+ y = bounds.y;
1667
+ width = bounds.width;
1668
+ height = bounds.height;
1669
+ }
1607
1670
  }
1608
- }
1609
1671
 
1610
- const position =
1611
- x !== undefined && y !== undefined
1612
- ? { x, y }
1613
- : findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
1672
+ const position = x !== undefined && y !== undefined ? { x, y } : findOpenCanvasPosition(nodes, width, height);
1614
1673
 
1615
- const id = `group-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
1616
- const data: Record<string, unknown> = {
1617
- title: input.title ?? 'Group',
1618
- children: [],
1619
- frameMode: explicitFrame ? 'manual' : 'fit',
1620
- ...(input.color ? { color: input.color } : {}),
1621
- };
1674
+ const id = `group-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
1675
+ const data: Record<string, unknown> = {
1676
+ title: input.title ?? 'Group',
1677
+ children: [],
1678
+ frameMode: explicitFrame ? 'manual' : 'fit',
1679
+ ...(input.color ? { color: input.color } : {}),
1680
+ };
1622
1681
 
1623
- canvasState.addNode({
1624
- id,
1625
- type: 'group',
1626
- position,
1627
- size: { width, height },
1628
- zIndex: 0,
1629
- collapsed: false,
1630
- pinned: false,
1631
- data,
1632
- });
1682
+ return {
1683
+ id,
1684
+ type: 'group',
1685
+ position,
1686
+ size: { width, height },
1687
+ zIndex: 0,
1688
+ collapsed: false,
1689
+ pinned: false,
1690
+ data: { ...data, children: stored ? childIds : [] },
1691
+ };
1692
+ };
1693
+ const created = stored
1694
+ ? canvasState.createStoredGroup(stored.boardId, stored.requestId, stored.fingerprint, build, input.childLayout)
1695
+ : build(canvasState.getLayout().nodes);
1696
+ const id = created.id;
1697
+ if (!stored) canvasState.addNode(created);
1633
1698
 
1634
- if (childIds.length > 0) {
1699
+ if (!stored && childIds.length > 0) {
1635
1700
  canvasState.groupNodes(id, childIds, {
1636
1701
  preservePositions: input.childLayout === undefined,
1637
1702
  ...(input.childLayout ? { layout: input.childLayout } : {}),
@@ -1639,7 +1704,7 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): {
1639
1704
  });
1640
1705
  }
1641
1706
 
1642
- const node = canvasState.getNode(id);
1707
+ const node = stored ? created : canvasState.getNode(id);
1643
1708
  if (!node) {
1644
1709
  throw new Error(`Group "${id}" was not created.`);
1645
1710
  }
@@ -1660,6 +1725,41 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): {
1660
1725
  };
1661
1726
  }
1662
1727
 
1728
+ /** One logical creation identity across node, frame and link operations. */
1729
+ export function storedCreationTarget(input: object, operation: string) {
1730
+ const body = input as Record<string, unknown>;
1731
+ for (const key of ['destinationBoardId', 'requestId']) {
1732
+ if (body[key] !== undefined && (typeof body[key] !== 'string' || !body[key].trim()))
1733
+ throw new OperationError(`${key} must be a non-empty string.`);
1734
+ }
1735
+ if (body.expectedBoardId !== undefined && body.expectedBoardId !== canvasState.activeBoardId)
1736
+ throw new OperationError('The active board changed. Refresh before retrying this action.', 409);
1737
+ if (body.intentId !== undefined)
1738
+ throw new OperationError('Destination/retry creation does not support ghost intents.');
1739
+ const boardId = typeof body.destinationBoardId === 'string' ? body.destinationBoardId : canvasState.activeBoardId;
1740
+ if (!boardId) throw new OperationError('Open a board or provide destinationBoardId.');
1741
+ const ignored = new Set(['requestId', 'destinationBoardId', 'expectedBoardId', 'full', 'verbose', 'agentId']);
1742
+ const canonical = (value: unknown): unknown =>
1743
+ Array.isArray(value)
1744
+ ? value.map(canonical)
1745
+ : isRecord(value)
1746
+ ? Object.fromEntries(
1747
+ Object.keys(value)
1748
+ .sort()
1749
+ .filter((key) => value[key] !== undefined)
1750
+ .map((key) => [key, canonical(value[key])]),
1751
+ )
1752
+ : value;
1753
+ const fingerprint = createHash('sha256')
1754
+ .update(JSON.stringify(canonical(Object.fromEntries(Object.entries(body).filter(([key]) => !ignored.has(key))))))
1755
+ .digest('hex');
1756
+ return {
1757
+ boardId,
1758
+ requestId: typeof body.requestId === 'string' ? body.requestId : undefined,
1759
+ fingerprint: operation === 'node.add' ? fingerprint : `${operation}:${fingerprint}`,
1760
+ };
1761
+ }
1762
+
1663
1763
  export function groupCanvasNodes(
1664
1764
  groupId: string,
1665
1765
  childIds: string[],
@@ -1893,16 +1993,21 @@ export function createCanvasGraphNode(input: GraphNodeInput): {
1893
1993
  * fit call still win. In-memory only (a live client fact, never persisted);
1894
1994
  * with several clients connected, the most recent reporter wins.
1895
1995
  */
1896
- let lastClientViewportSize: { width: number; height: number } | null = null;
1897
-
1898
- export function setClientViewportSize(width: unknown, height: unknown): void {
1996
+ let lastClientViewportSize: {
1997
+ width: number;
1998
+ height: number;
1999
+ boardId: string | null;
2000
+ insets?: { top: number; bottom: number };
2001
+ } | null = null;
2002
+
2003
+ export function setClientViewportSize(width: unknown, height: unknown, insets?: { top: number; bottom: number }): void {
1899
2004
  if (typeof width !== 'number' || typeof height !== 'number') return;
1900
2005
  if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return;
1901
- lastClientViewportSize = { width, height };
2006
+ lastClientViewportSize = { width, height, boardId: canvasState.activeBoardId, insets };
1902
2007
  }
1903
2008
 
1904
2009
  export function getClientViewportSize(): { width: number; height: number } | null {
1905
- return lastClientViewportSize ? { ...lastClientViewportSize } : null;
2010
+ return lastClientViewportSize ? { width: lastClientViewportSize.width, height: lastClientViewportSize.height } : null;
1906
2011
  }
1907
2012
 
1908
2013
  export function fitCanvasView(options: CanvasFitViewOptions = {}): CanvasFitViewResult {
@@ -1911,7 +2016,8 @@ export function fitCanvasView(options: CanvasFitViewOptions = {}): CanvasFitView
1911
2016
  const padding = positiveNumber(options.padding, 24); // screen px now, not world units
1912
2017
  const maxScale = positiveNumber(options.maxScale, 1);
1913
2018
  const nodeIdFilter = options.nodeIds && options.nodeIds.length > 0 ? new Set(options.nodeIds) : null;
1914
- const targetNodes = canvasState.getLayout().nodes.filter((node) => !nodeIdFilter || nodeIdFilter.has(node.id));
2019
+ const layout = canvasState.getLayout();
2020
+ const targetNodes = layout.nodes.filter((node) => !nodeIdFilter || nodeIdFilter.has(node.id));
1915
2021
 
1916
2022
  if (targetNodes.length === 0) {
1917
2023
  const viewport = canvasState.viewport;
@@ -1933,12 +2039,18 @@ export function fitCanvasView(options: CanvasFitViewOptions = {}): CanvasFitView
1933
2039
  // Screen-space fit (architecture rule 9): `padding` and the chrome reserve
1934
2040
  // are SCREEN pixels — world-space padding shrank with the fit zoom and
1935
2041
  // parked nodes under the command bar and minimap (0.5.0 Amp report,
1936
- // finding A). The server cannot measure the client's chrome, so it reserves
1937
- // nominal bottom space: the minimap is always there; the command bar only
1938
- // while a session is attached.
1939
- const bottomReserve = agentPresence.snapshot().sessionActive ? 128 : 96;
2042
+ // finding A). The workbench reports its measured chrome with its size;
2043
+ // without a matching board and viewport size, retain nominal chrome reserves.
2044
+ const insets =
2045
+ lastClientViewportSize?.boardId === canvasState.activeBoardId &&
2046
+ lastClientViewportSize.width === width &&
2047
+ lastClientViewportSize.height === height
2048
+ ? lastClientViewportSize.insets
2049
+ : undefined;
2050
+ const bottomReserve = insets?.bottom ?? (agentPresence.snapshot().sessionActive ? 128 : 96);
2051
+ const topReserve = Math.max(padding, insets?.top ?? (layout.edges.length ? 90 : 48));
1940
2052
  const usableWidth = Math.max(80, width - padding * 2);
1941
- const usableHeight = Math.max(80, height - padding - bottomReserve);
2053
+ const usableHeight = Math.max(80, height - topReserve - bottomReserve);
1942
2054
  const worldWidth = Math.max(1, bounds.width);
1943
2055
  const worldHeight = Math.max(1, bounds.height);
1944
2056
  const scale = Math.min(maxScale, usableWidth / worldWidth, usableHeight / worldHeight);
@@ -1946,7 +2058,7 @@ export function fitCanvasView(options: CanvasFitViewOptions = {}): CanvasFitView
1946
2058
  const centerY = minY + bounds.height / 2;
1947
2059
  const viewport = {
1948
2060
  x: padding + usableWidth / 2 - centerX * scale,
1949
- y: padding + usableHeight / 2 - centerY * scale,
2061
+ y: topReserve + usableHeight / 2 - centerY * scale,
1950
2062
  scale,
1951
2063
  };
1952
2064
 
@@ -1,5 +1,7 @@
1
1
  import { pathToFileURL } from 'node:url';
2
+ import { z } from 'zod';
2
3
  import { normalizeHtmlNodeSemanticData } from './html-node-summary.js';
4
+ import { OperationError } from './operations/types.js';
3
5
 
4
6
  export type CanvasNodeType =
5
7
  | 'markdown'
@@ -26,7 +28,8 @@ export type CanvasNodeProvenanceSourceKind =
26
28
  | 'webpage-url'
27
29
  | 'mcp-tool'
28
30
  | 'artifact-file'
29
- | 'image-url';
31
+ | 'image-url'
32
+ | 'recipe';
30
33
 
31
34
  export type CanvasNodeRefreshStrategy =
32
35
  | 'file-watch'
@@ -34,7 +37,21 @@ export type CanvasNodeRefreshStrategy =
34
37
  | 'image-reload'
35
38
  | 'webpage-refresh'
36
39
  | 'mcp-app-rehydrate'
37
- | 'artifact-reopen';
40
+ | 'artifact-reopen'
41
+ | 'agent-request';
42
+
43
+ const observedSourceSchema = z.object({
44
+ boardId: z.string().min(1),
45
+ cardId: z.string().min(1),
46
+ contentRevision: z.number().int().nonnegative().optional(),
47
+ observedAt: z.iso.datetime().optional(),
48
+ availability: z.enum(['live', 'cached', 'unavailable']).optional(),
49
+ });
50
+ const recipeSchema = z.object({
51
+ tool: z.string().max(200),
52
+ input: z.unknown().optional(),
53
+ prompt: z.string().max(20000),
54
+ });
38
55
 
39
56
  export interface CanvasNodeProvenance {
40
57
  sourceKind: CanvasNodeProvenanceSourceKind;
@@ -42,6 +59,8 @@ export interface CanvasNodeProvenance {
42
59
  refreshStrategy: CanvasNodeRefreshStrategy;
43
60
  snapshotContent: boolean;
44
61
  syncedAt?: string;
62
+ sources?: Array<z.infer<typeof observedSourceSchema>>;
63
+ recipe?: z.infer<typeof recipeSchema>;
45
64
  details?: Record<string, unknown>;
46
65
  }
47
66
 
@@ -65,28 +84,40 @@ function buildMcpToolUri(serverName: string, toolName: string): string {
65
84
  return `mcp-tool://${encodeURIComponent(serverName)}/${encodeURIComponent(toolName)}`;
66
85
  }
67
86
 
68
- function normalizeExistingProvenance(value: unknown): CanvasNodeProvenance | null {
69
- if (!isRecord(value)) return null;
70
-
71
- const sourceKind = pickString(value.sourceKind);
72
- const sourceUri = pickString(value.sourceUri);
73
- const refreshStrategy = pickString(value.refreshStrategy);
74
- if (!sourceKind || !sourceUri || !refreshStrategy) return null;
75
-
76
- const normalized: CanvasNodeProvenance = {
77
- sourceKind: sourceKind as CanvasNodeProvenanceSourceKind,
78
- sourceUri,
79
- refreshStrategy: refreshStrategy as CanvasNodeRefreshStrategy,
80
- snapshotContent: value.snapshotContent !== false,
81
- };
87
+ const provenanceSchema = z.object({
88
+ sourceKind: z.enum(['workspace-file', 'webpage-url', 'mcp-tool', 'artifact-file', 'image-url', 'recipe']),
89
+ sourceUri: z.string().trim().min(1),
90
+ refreshStrategy: z.enum([
91
+ 'file-watch',
92
+ 'file-read-write',
93
+ 'image-reload',
94
+ 'webpage-refresh',
95
+ 'mcp-app-rehydrate',
96
+ 'artifact-reopen',
97
+ 'agent-request',
98
+ ]),
99
+ snapshotContent: z.boolean().default(true),
100
+ syncedAt: z.string().trim().min(1).optional(),
101
+ sources: z
102
+ .array(observedSourceSchema)
103
+ .max(100)
104
+ .refine(
105
+ (sources) =>
106
+ new Set(sources.map((source) => JSON.stringify([source.boardId, source.cardId]))).size === sources.length,
107
+ 'Source targets must be unique',
108
+ )
109
+ .optional(),
110
+ recipe: recipeSchema.optional(),
111
+ details: z.record(z.string(), z.unknown()).optional(),
112
+ });
82
113
 
83
- const syncedAt = pickString(value.syncedAt);
84
- if (syncedAt) normalized.syncedAt = syncedAt;
85
- if (isRecord(value.details) && Object.keys(value.details).length > 0) {
86
- normalized.details = { ...value.details };
114
+ function normalizeExistingProvenance(value: unknown): CanvasNodeProvenance | null {
115
+ if (value === undefined) return null;
116
+ const result = z.object({ provenance: provenanceSchema }).safeParse({ provenance: value });
117
+ if (!result.success) {
118
+ throw new OperationError(`Invalid node data: ${z.prettifyError(result.error)}`);
87
119
  }
88
-
89
- return normalized;
120
+ return result.data.provenance;
90
121
  }
91
122
 
92
123
  function mergeProvenance(
@@ -4,8 +4,10 @@ import type { CanvasAnnotation, CanvasLayout, CanvasNodeState, ViewportState } f
4
4
  import { normalizeCanvasNodeData, type CanvasNodeProvenance } from './canvas-provenance.js';
5
5
  import { getCanvasNodeKind as getSharedCanvasNodeKind } from '../shared/canvas-node-kind.js';
6
6
  import { canOpenNodeAsSurface } from '../shared/surface.js';
7
+ import type { LifecycleQualification } from '../shared/lifecycle-status.js';
7
8
 
8
9
  export interface SerializedCanvasNode extends CanvasNodeState {
10
+ qualification?: LifecycleQualification;
9
11
  kind: string;
10
12
  title: string | null;
11
13
  content: string | null;