pmx-canvas 0.8.2 → 0.9.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 (335) 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 +49 -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/intent-bridge.d.ts +11 -8
  53. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  54. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  55. package/dist/types/client/state/presence-store.d.ts +4 -4
  56. package/dist/types/client/state/presentation.d.ts +28 -0
  57. package/dist/types/client/state/relation-store.d.ts +10 -0
  58. package/dist/types/client/state/session-store.d.ts +41 -2
  59. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  60. package/dist/types/client/types.d.ts +13 -1
  61. package/dist/types/client/utils/keyboard.d.ts +2 -0
  62. package/dist/types/client/utils/time-ago.d.ts +4 -0
  63. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  64. package/dist/types/server/agent-presence.d.ts +10 -3
  65. package/dist/types/server/ax-context.d.ts +1 -1
  66. package/dist/types/server/canvas-db.d.ts +30 -7
  67. package/dist/types/server/canvas-operations.d.ts +24 -2
  68. package/dist/types/server/canvas-provenance.d.ts +22 -2
  69. package/dist/types/server/canvas-serialization.d.ts +2 -0
  70. package/dist/types/server/canvas-state.d.ts +93 -15
  71. package/dist/types/server/canvas-validation.d.ts +1 -1
  72. package/dist/types/server/context-brief.d.ts +94 -6
  73. package/dist/types/server/context-reads.d.ts +33 -7
  74. package/dist/types/server/edit-summary.d.ts +20 -0
  75. package/dist/types/server/index.d.ts +71 -10
  76. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  77. package/dist/types/server/lifecycle-status.d.ts +10 -0
  78. package/dist/types/server/memory-graph.d.ts +9 -0
  79. package/dist/types/server/mutation-history.d.ts +2 -0
  80. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  81. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  82. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  83. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  84. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  85. package/dist/types/server/operations/ops/query.d.ts +23 -0
  86. package/dist/types/server/operations/registry.d.ts +1 -0
  87. package/dist/types/server/placement.d.ts +7 -0
  88. package/dist/types/server/scope-fence.d.ts +2 -1
  89. package/dist/types/server/spatial-analysis.d.ts +6 -3
  90. package/dist/types/server/tour-control.d.ts +32 -0
  91. package/dist/types/server/wiki-links.d.ts +14 -0
  92. package/dist/types/server/workbench-auth.d.ts +2 -0
  93. package/dist/types/shared/agent-context.d.ts +2 -0
  94. package/dist/types/shared/agent-presence.d.ts +15 -12
  95. package/dist/types/shared/auto-arrange.d.ts +1 -0
  96. package/dist/types/shared/boards.d.ts +4 -4
  97. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  98. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  99. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  100. package/dist/types/shared/memory-graph.d.ts +53 -0
  101. package/dist/types/shared/relations.d.ts +68 -0
  102. package/dist/types/shared/semantic-attention.d.ts +2 -0
  103. package/dist/types/shared/tour.d.ts +38 -0
  104. package/dist/types/shared/workspace-settings.d.ts +11 -0
  105. package/docs/RELEASE.md +21 -3
  106. package/docs/adr-001-bun-only-runtime.md +7 -1
  107. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  108. package/docs/api-reference.md +109 -0
  109. package/docs/ax-host-adapter-contract.md +1 -1
  110. package/docs/ax-state-contract.md +5 -4
  111. package/docs/cli.md +153 -15
  112. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  113. package/docs/design/AgentContext.dc.html +3 -3
  114. package/docs/design/BoardPins.dc.html +37 -22
  115. package/docs/design/Categories.dc.html +208 -0
  116. package/docs/design/CategoryEditor.dc.html +33 -0
  117. package/docs/design/Chrome.dc.html +52 -9
  118. package/docs/design/ContextChip.dc.html +224 -0
  119. package/docs/design/ContextChip600.dc.html +136 -0
  120. package/docs/design/EmptyState.dc.html +45 -0
  121. package/docs/design/Flows.dc.html +23 -1
  122. package/docs/design/HeaderMarks.dc.html +183 -0
  123. package/docs/design/Home.dc.html +19 -1
  124. package/docs/design/HomeGraph.dc.html +86 -202
  125. package/docs/design/HomeMap.dc.html +23 -1
  126. package/docs/design/HomeTeam.dc.html +32 -14
  127. package/docs/design/Lifecycle.dc.html +22 -0
  128. package/docs/design/Lifecycle600.dc.html +18 -0
  129. package/docs/design/LifecycleVariants.dc.html +157 -0
  130. package/docs/design/LinkAuthorship.dc.html +139 -0
  131. package/docs/design/LinksChanged.dc.html +203 -0
  132. package/docs/design/LinksOptions.dc.html +137 -0
  133. package/docs/design/LinksOptions600.dc.html +134 -0
  134. package/docs/design/NearPin.dc.html +9 -10
  135. package/docs/design/Pane600.dc.html +4 -3
  136. package/docs/design/Relations.dc.html +3 -2
  137. package/docs/design/Restore.dc.html +152 -0
  138. package/docs/design/SeeChange.dc.html +267 -0
  139. package/docs/design/Uses.dc.html +12 -6
  140. package/docs/design/canvas.json +146 -19
  141. package/docs/design.md +449 -16
  142. package/docs/environment.md +0 -1
  143. package/docs/evals/curation-effect.md +128 -0
  144. package/docs/http-api.md +459 -37
  145. package/docs/mcp.md +146 -18
  146. package/docs/node-types.md +58 -3
  147. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  148. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  149. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  150. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  151. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  152. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  153. package/docs/product-context-vision-2026-09.md +17 -1
  154. package/docs/product-vision-2026-09.md +1082 -43
  155. package/docs/product-vision-review-2026-09.md +13 -1
  156. package/docs/screenshots/workbench-live-dark.png +0 -0
  157. package/docs/screenshots/workbench-live-light.png +0 -0
  158. package/docs/sdk.md +62 -2
  159. package/package.json +1 -1
  160. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  161. package/plugins/pmx-canvas/plugin.json +1 -1
  162. package/plugins/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/tools.json +601 -22
  164. package/skills/pmx-canvas/SKILL.md +216 -42
  165. package/skills/pmx-canvas/references/full-reference.md +120 -21
  166. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  167. package/skills/pmx-canvas/references/importing-context.md +132 -0
  168. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  169. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  170. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  171. package/src/cli/agent.ts +2 -0
  172. package/src/cli/commands/ax.ts +12 -0
  173. package/src/cli/commands/boards.ts +87 -11
  174. package/src/cli/commands/edges.ts +28 -0
  175. package/src/cli/commands/groups.ts +7 -0
  176. package/src/cli/commands/nodes.ts +1 -0
  177. package/src/cli/commands/query.ts +34 -2
  178. package/src/cli/commands/record.ts +199 -48
  179. package/src/cli/index.ts +6 -1
  180. package/src/client/App.tsx +177 -70
  181. package/src/client/canvas/AttentionHistory.tsx +0 -2
  182. package/src/client/canvas/BarHint.tsx +40 -2
  183. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  184. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  185. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  186. package/src/client/canvas/BoardPinButton.tsx +33 -0
  187. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  188. package/src/client/canvas/CanvasNode.tsx +360 -237
  189. package/src/client/canvas/CanvasViewport.tsx +45 -7
  190. package/src/client/canvas/CommandBar.tsx +29 -23
  191. package/src/client/canvas/CommandPalette.tsx +5 -12
  192. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  193. package/src/client/canvas/ContextChip.tsx +296 -0
  194. package/src/client/canvas/ContextMenu.tsx +63 -5
  195. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  196. package/src/client/canvas/EdgeLayer.tsx +339 -76
  197. package/src/client/canvas/EmptyState.tsx +33 -18
  198. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  199. package/src/client/canvas/ExternalWriters.tsx +57 -42
  200. package/src/client/canvas/HomeGraphView.tsx +942 -0
  201. package/src/client/canvas/HomeMapView.tsx +641 -0
  202. package/src/client/canvas/HomeReviewView.tsx +167 -0
  203. package/src/client/canvas/HomeView.tsx +687 -160
  204. package/src/client/canvas/IntentLayer.tsx +19 -12
  205. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  206. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  207. package/src/client/canvas/LifecycleFields.tsx +91 -0
  208. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  209. package/src/client/canvas/MemoryInspector.tsx +104 -0
  210. package/src/client/canvas/Minimap.tsx +3 -3
  211. package/src/client/canvas/NearPinLayer.tsx +187 -0
  212. package/src/client/canvas/NodeContextMark.tsx +175 -97
  213. package/src/client/canvas/Presentation.tsx +23 -57
  214. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  215. package/src/client/canvas/RelationsPanel.tsx +82 -0
  216. package/src/client/canvas/SelectionBar.tsx +3 -6
  217. package/src/client/canvas/SessionPanel.tsx +90 -50
  218. package/src/client/canvas/SessionReceipt.tsx +311 -39
  219. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  220. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  221. package/src/client/canvas/ToolRail.tsx +367 -192
  222. package/src/client/canvas/TopBar.tsx +354 -124
  223. package/src/client/canvas/canvas-area.ts +10 -3
  224. package/src/client/canvas/graph-geometry.ts +195 -0
  225. package/src/client/canvas/graph-model.ts +254 -0
  226. package/src/client/canvas/memory-layout.ts +106 -0
  227. package/src/client/canvas/use-chrome-height.ts +20 -0
  228. package/src/client/canvas/use-focus-trap.ts +31 -5
  229. package/src/client/canvas/use-memory-graph.ts +58 -0
  230. package/src/client/canvas/use-node-drag.ts +2 -1
  231. package/src/client/canvas/use-pan-zoom.ts +64 -7
  232. package/src/client/icons.tsx +0 -63
  233. package/src/client/mcp-app/style.css +0 -6
  234. package/src/client/nodes/AxStepControls.tsx +74 -50
  235. package/src/client/nodes/BoardNode.tsx +5 -1
  236. package/src/client/nodes/ContextNode.tsx +18 -11
  237. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  238. package/src/client/nodes/FileNode.tsx +46 -42
  239. package/src/client/nodes/HtmlNode.tsx +32 -0
  240. package/src/client/nodes/ImageNode.tsx +1 -1
  241. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  242. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  243. package/src/client/nodes/MarkdownNode.tsx +377 -109
  244. package/src/client/nodes/McpAppNode.tsx +50 -20
  245. package/src/client/nodes/MdFormatBar.tsx +21 -18
  246. package/src/client/nodes/PromptNode.tsx +1 -1
  247. package/src/client/nodes/ResponseNode.tsx +1 -1
  248. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  249. package/src/client/state/attention-bridge.ts +64 -15
  250. package/src/client/state/boards-store.ts +193 -39
  251. package/src/client/state/canvas-store.ts +99 -12
  252. package/src/client/state/context-chip-store.ts +70 -0
  253. package/src/client/state/context-status-store.ts +53 -0
  254. package/src/client/state/intent-bridge.ts +51 -30
  255. package/src/client/state/knowledge-store.ts +43 -0
  256. package/src/client/state/near-pin-store.ts +115 -6
  257. package/src/client/state/presence-store.ts +7 -10
  258. package/src/client/state/presentation.ts +91 -0
  259. package/src/client/state/relation-store.ts +81 -0
  260. package/src/client/state/session-store.ts +144 -18
  261. package/src/client/state/sse-bridge.ts +206 -29
  262. package/src/client/state/workspace-settings.ts +11 -0
  263. package/src/client/theme/global.css +2724 -476
  264. package/src/client/types.ts +12 -0
  265. package/src/client/utils/keyboard.ts +10 -0
  266. package/src/client/utils/time-ago.ts +14 -0
  267. package/src/json-render/renderer/index.tsx +31 -0
  268. package/src/mcp/canvas-app.ts +12 -2
  269. package/src/mcp/context-uri-template.ts +7 -4
  270. package/src/mcp/server.ts +63 -41
  271. package/src/mcp/workbench-app.ts +39 -6
  272. package/src/server/agent-presence.ts +128 -28
  273. package/src/server/ax-context.ts +27 -13
  274. package/src/server/board-export.ts +12 -1
  275. package/src/server/canvas-db.ts +331 -42
  276. package/src/server/canvas-operations.ts +195 -83
  277. package/src/server/canvas-provenance.ts +53 -22
  278. package/src/server/canvas-serialization.ts +2 -0
  279. package/src/server/canvas-state.ts +950 -122
  280. package/src/server/canvas-validation.ts +9 -2
  281. package/src/server/context-brief.ts +345 -38
  282. package/src/server/context-reads.ts +178 -30
  283. package/src/server/demo-state.json +355 -24
  284. package/src/server/edit-summary.ts +74 -0
  285. package/src/server/html-surface.ts +8 -1
  286. package/src/server/index.ts +181 -11
  287. package/src/server/knowledge-lifecycle.ts +304 -0
  288. package/src/server/lifecycle-status.ts +232 -0
  289. package/src/server/memory-graph.ts +166 -0
  290. package/src/server/mutation-history.ts +18 -6
  291. package/src/server/operations/composites.ts +32 -11
  292. package/src/server/operations/http.ts +7 -2
  293. package/src/server/operations/index.ts +4 -0
  294. package/src/server/operations/mcp.ts +26 -1
  295. package/src/server/operations/ops/ax-presence.ts +1 -1
  296. package/src/server/operations/ops/ax-read.ts +57 -6
  297. package/src/server/operations/ops/ax-timeline.ts +28 -2
  298. package/src/server/operations/ops/backup.ts +5 -3
  299. package/src/server/operations/ops/batch.ts +35 -4
  300. package/src/server/operations/ops/boards.ts +188 -47
  301. package/src/server/operations/ops/canvas-wire.ts +31 -1
  302. package/src/server/operations/ops/edges.ts +36 -2
  303. package/src/server/operations/ops/groups.ts +6 -1
  304. package/src/server/operations/ops/lifecycle.ts +262 -0
  305. package/src/server/operations/ops/memory-graph.ts +45 -0
  306. package/src/server/operations/ops/nodes.ts +107 -24
  307. package/src/server/operations/ops/query.ts +281 -68
  308. package/src/server/operations/ops/snapshots.ts +73 -1
  309. package/src/server/operations/ops/validate.ts +5 -1
  310. package/src/server/operations/ops/viewport.ts +97 -3
  311. package/src/server/operations/registry.ts +74 -25
  312. package/src/server/placement.ts +29 -0
  313. package/src/server/scope-fence.ts +78 -6
  314. package/src/server/server.ts +115 -16
  315. package/src/server/spatial-analysis.ts +48 -23
  316. package/src/server/tour-control.ts +82 -0
  317. package/src/server/wiki-links.ts +137 -0
  318. package/src/server/workbench-auth.ts +11 -0
  319. package/src/shared/agent-context.ts +3 -0
  320. package/src/shared/agent-presence.ts +15 -15
  321. package/src/shared/auto-arrange.ts +2 -0
  322. package/src/shared/boards.ts +4 -4
  323. package/src/shared/knowledge-lifecycle.ts +22 -0
  324. package/src/shared/knowledge-presets.ts +80 -0
  325. package/src/shared/lifecycle-status.ts +26 -0
  326. package/src/shared/memory-graph.ts +74 -0
  327. package/src/shared/relations.ts +98 -0
  328. package/src/shared/semantic-attention.ts +15 -1
  329. package/src/shared/tour.ts +23 -5
  330. package/src/shared/workspace-settings.ts +13 -0
  331. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  332. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  333. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  334. package/dist/types/server/agent-context.d.ts +0 -22
  335. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -30,6 +30,7 @@ import { executeOperation, runWithSuppressedEmits } from '../registry.js';
30
30
  import { defineOperation, type Operation, type OperationContext } from '../types.js';
31
31
  import { readJsonValue } from '../http.js';
32
32
  import { isRecord } from './nodes.js';
33
+ import { boardsPayload } from './boards.js';
33
34
 
34
35
  interface BatchEntry {
35
36
  op: string;
@@ -157,8 +158,10 @@ function createInternalBatchNode(args: Record<string, unknown>): Record<string,
157
158
  * entry shape. Most ops match the wire shape verbatim; the cases below diverge
158
159
  * because the legacy switch shaped them by hand.
159
160
  */
160
- function shapeBatchEntry(op: string, result: unknown): Record<string, unknown> {
161
+ function shapeBatchEntry(op: string, result: unknown, creationReceipt = false): Record<string, unknown> {
161
162
  const body = isRecord(result) ? result : {};
163
+ // A retry's refs must describe its original creation, not a later live edit.
164
+ if (creationReceipt) return body;
162
165
 
163
166
  // node.add / node.update / group.create / graph.add: the legacy switch pushed
164
167
  // { ok:true, ...serializeCanvasNodeCompact(node), <extras> }. The wire shape
@@ -237,7 +240,13 @@ async function runBatch(operations: BatchEntry[]): Promise<BatchEnvelope> {
237
240
  refs,
238
241
  };
239
242
  }
240
- if (operation.op === 'node.add' && typeof args.type === 'string' && isInternalBatchNodeType(args.type)) {
243
+ if (
244
+ operation.op === 'node.add' &&
245
+ typeof args.type === 'string' &&
246
+ isInternalBatchNodeType(args.type) &&
247
+ args.destinationBoardId === undefined &&
248
+ args.requestId === undefined
249
+ ) {
241
250
  const result = createInternalBatchNode(args);
242
251
  results.push(result);
243
252
  if (typeof operation.assign === 'string' && operation.assign.trim().length > 0) {
@@ -258,7 +267,12 @@ async function runBatch(operations: BatchEntry[]): Promise<BatchEnvelope> {
258
267
  if (canvasState.boardGeneration !== boardGeneration) {
259
268
  throw new Error('The board changed while the batch was running.');
260
269
  }
261
- const result = shapeBatchEntry(operation.op, raw);
270
+ const result = shapeBatchEntry(
271
+ operation.op,
272
+ raw,
273
+ ['node.add', 'group.create', 'edge.add'].includes(operation.op) &&
274
+ (args.requestId !== undefined || args.destinationBoardId !== undefined),
275
+ );
262
276
  results.push(result);
263
277
  if (typeof operation.assign === 'string' && operation.assign.trim().length > 0) {
264
278
  refs[operation.assign] = result;
@@ -391,8 +405,25 @@ const batchOperation = defineOperation<z.infer<typeof batchSchema>, BatchEnvelop
391
405
  const operations = normalizeOperations(input.operations);
392
406
  // Suppress every per-entry SSE frame (auto layout emit + each handler's
393
407
  // ctx.emit). After the loop completes, emit ONE final canvas-layout-update.
408
+ const revision = canvasState.getContentRevision().revision;
394
409
  const envelope = await runWithSuppressedEmits(() => runBatch(operations));
395
- ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
410
+ if (
411
+ revision !== canvasState.getContentRevision().revision ||
412
+ operations.some(
413
+ (entry) =>
414
+ !['node.add', 'group.create', 'edge.add'].includes(entry.op) ||
415
+ entry.args.destinationBoardId === undefined ||
416
+ entry.args.destinationBoardId === canvasState.activeBoardId,
417
+ )
418
+ )
419
+ ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
420
+ if (
421
+ operations.some(
422
+ (entry) =>
423
+ ['node.add', 'group.create', 'edge.add'].includes(entry.op) && entry.args.destinationBoardId !== undefined,
424
+ )
425
+ )
426
+ ctx.emit('boards-changed', boardsPayload());
396
427
  // The suppression window also swallows the pins op's context-pins-changed
397
428
  // frame, leaving the client's pin state (menus, indicators) stale until
398
429
  // reload (0.4.5 report Finding X). If the batch touched pins, follow the
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Many boards (plan 012): board.list / board.get / board.create /
3
- * board.update / board.open / board.delete.
3
+ * board.update / board.open / board.delete, plus board.pin / board.unpin
4
+ * (vision move 0a: the working set across boards).
4
5
  *
5
6
  * Agents can explicitly open boards as well as list, read and create them.
6
7
  * Deletion requires the human (HUMAN_ONLY_OPS in registry.ts).
@@ -11,46 +12,111 @@
11
12
  *
12
13
  * This module must never import server.ts or index.ts.
13
14
  */
15
+ import { randomUUID } from 'node:crypto';
14
16
  import { z } from 'zod';
15
- import { normalizeBoardCategory } from '../../../shared/boards.js';
17
+ import { normalizeBoardFolder } from '../../../shared/boards.js';
18
+ import { relationVocabularySchema } from '../../../shared/relations.js';
16
19
  import { canvasState, type CanvasLayout } from '../../canvas-state.js';
17
20
  import { openCanvasBoard } from '../../canvas-operations.js';
18
21
  import { summarizeNodeForAgentContext } from '../../../shared/agent-context.js';
22
+ import { BRIEF_SUMMARY_LENGTH } from '../../context-brief.js';
23
+ import { memoryGraph, boardConnections } from '../../memory-graph.js';
19
24
  import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
20
25
 
21
- export function boardsPayload(): Record<string, unknown> {
26
+ /** The most cards a Home thumbnail draws (the largest first); the rest are noise at that size. */
27
+ const PREVIEW_CARDS = 24;
28
+ export const boardCatalogEpoch = randomUUID();
29
+ let catalogRevision = 0;
30
+ let reloadRevision = 0;
31
+
32
+ /**
33
+ * A board's thumbnail on Home (docs/design/Home.dc.html): its cards as
34
+ * rectangles in 0–1 of the board's extent, pinned ones flagged.
35
+ */
36
+ function boardPreview(
37
+ nodes: CanvasLayout['nodes'],
38
+ pins: readonly string[],
39
+ ): Array<{ x: number; y: number; w: number; h: number; pinned?: true }> {
40
+ const cards = nodes.filter((node) => node.type !== 'group');
41
+ if (cards.length === 0) return [];
42
+ const left = Math.min(...cards.map((node) => node.position.x));
43
+ const top = Math.min(...cards.map((node) => node.position.y));
44
+ const width = Math.max(...cards.map((node) => node.position.x + node.size.width)) - left || 1;
45
+ const height = Math.max(...cards.map((node) => node.position.y + node.size.height)) - top || 1;
46
+ const round = (value: number) => Math.round(value * 1000) / 1000;
47
+ return [...cards]
48
+ .sort((a, b) => b.size.width * b.size.height - a.size.width * a.size.height)
49
+ .slice(0, PREVIEW_CARDS)
50
+ .map((node) => ({
51
+ x: round((node.position.x - left) / width),
52
+ y: round((node.position.y - top) / height),
53
+ w: round(node.size.width / width),
54
+ h: round(node.size.height / height),
55
+ ...(pins.includes(node.id) ? { pinned: true as const } : {}),
56
+ }));
57
+ }
58
+
59
+ export function boardsPayload(reloaded = false): Record<string, unknown> {
60
+ const revision = ++catalogRevision;
61
+ if (reloaded) reloadRevision = revision;
22
62
  const boards = canvasState.listBoards();
23
63
  const names = new Map(boards.map((board) => [board.id, board.name]));
24
- const links = new Map<string, Array<{ nodeId: string; boardId: string; title: string | null; missing: boolean }>>();
25
- for (const board of boards) {
26
- const nodes = canvasState.readBoard(board.id, false)?.layout.nodes ?? [];
27
- links.set(
28
- board.id,
29
- nodes
30
- .filter((node) => node.type === 'board')
31
- .map((node) => {
32
- const boardId = typeof node.data.boardId === 'string' ? node.data.boardId : '';
33
- return { nodeId: node.id, boardId, title: names.get(boardId) ?? null, missing: !names.has(boardId) };
34
- }),
35
- );
36
- }
64
+ const graph = memoryGraph();
65
+ const titles = new Map(graph.nodes.map((node) => [node.key, node.title]));
66
+ const connections = boardConnections(graph);
37
67
  return {
68
+ catalogEpoch: boardCatalogEpoch,
69
+ catalogRevision: revision,
70
+ reloadRevision,
71
+ boardGeneration: canvasState.boardGeneration,
38
72
  activeBoardId: canvasState.activeBoardId,
39
73
  boards: boards.map((board) => {
40
74
  const read = canvasState.readBoard(board.id, true);
41
75
  const readme = read?.layout.nodes.find((node) => node.id === board.readmeNodeId && node.type === 'markdown');
76
+ const pins = read?.state.contextPins ?? [];
77
+ const pinnedNodes = (read?.layout.nodes ?? []).filter((node) => pins.includes(node.id));
78
+ const summary = readme ? summarizeNodeForAgentContext(readme, { defaultTextLength: 1000 }) : null;
42
79
  return {
43
80
  ...board,
44
- summary: readme ? summarizeNodeForAgentContext(readme, { defaultTextLength: 1000 }) : null,
45
- pinnedTitles: (read?.layout.nodes ?? [])
46
- .filter((node) => read?.state.contextPins.includes(node.id))
47
- .map((node) => ({ nodeId: node.id, title: typeof node.data.title === 'string' ? node.data.title : node.id })),
48
- links: links.get(board.id) ?? [],
49
- backlinks: boards.flatMap((source) =>
50
- (links.get(source.id) ?? [])
51
- .filter((link) => link.boardId === board.id)
52
- .map((link) => ({ boardId: source.id, title: source.name, nodeId: link.nodeId })),
53
- ),
81
+ summary,
82
+ pinnedTitles: pinnedNodes.map((node) => ({
83
+ nodeId: node.id,
84
+ title: typeof node.data.title === 'string' ? node.data.title : node.id,
85
+ })),
86
+ preview: boardPreview(read?.layout.nodes ?? [], pins),
87
+ // What a pinned board's map roughly costs in the brief: its README and pinned cards as summaries.
88
+ mapChars:
89
+ (summary?.length ?? 0) +
90
+ pinnedNodes.reduce(
91
+ (sum, node) =>
92
+ sum +
93
+ summarizeNodeForAgentContext(node, {
94
+ defaultTextLength: BRIEF_SUMMARY_LENGTH,
95
+ webpageTextLength: BRIEF_SUMMARY_LENGTH,
96
+ }).length,
97
+ 0,
98
+ ),
99
+ links: connections
100
+ .filter((edge) => edge.source.boardId === board.id)
101
+ .map((edge) => ({
102
+ id: edge.id,
103
+ nodeId: edge.source.cardId ?? '',
104
+ boardId: edge.target.boardId,
105
+ ...(edge.target.cardId ? { cardId: edge.target.cardId, cardTitle: titles.get(edge.to) ?? null } : {}),
106
+ title: names.get(edge.target.boardId) ?? null,
107
+ missing: edge.missing,
108
+ kind: edge.kind,
109
+ })),
110
+ backlinks: connections
111
+ .filter((edge) => edge.target.boardId === board.id)
112
+ .map((edge) => ({
113
+ id: edge.id,
114
+ boardId: edge.source.boardId,
115
+ title: names.get(edge.source.boardId) ?? '',
116
+ nodeId: edge.source.cardId ?? '',
117
+ kind: edge.kind,
118
+ ...(edge.target.cardId ? { cardId: edge.target.cardId } : {}),
119
+ })),
54
120
  };
55
121
  }),
56
122
  };
@@ -82,19 +148,21 @@ export function readTargetBoard(
82
148
  };
83
149
  }
84
150
 
85
- function emitBoardsChanged(ctx: OperationContext): void {
86
- ctx.emit('boards-changed', boardsPayload());
151
+ function emitBoardsChanged(ctx: OperationContext): Record<string, unknown> {
152
+ const payload = boardsPayload();
153
+ ctx.emit('boards-changed', payload);
154
+ return payload;
87
155
  }
88
156
 
89
157
  const jsonResult = (result: unknown) => ({
90
158
  content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
91
159
  });
92
160
 
93
- /** '' or null clears the category; undefined leaves it alone. */
94
- function boardCategory(value: unknown): string | null | undefined {
161
+ /** '' or null clears the folder; undefined leaves it alone. */
162
+ function boardFolder(value: unknown): string | null | undefined {
95
163
  if (value === undefined) return undefined;
96
164
  try {
97
- return normalizeBoardCategory(typeof value === 'string' ? value : null);
165
+ return normalizeBoardFolder(typeof value === 'string' ? value : null);
98
166
  } catch (error) {
99
167
  throw new OperationError(error instanceof Error ? error.message : String(error));
100
168
  }
@@ -158,7 +226,7 @@ const boardGetOperation = defineOperation<z.infer<typeof getSchema>, Record<stri
158
226
 
159
227
  const createShape = {
160
228
  name: z.unknown().optional().describe('Board name'),
161
- category: z.unknown().optional().describe('Folder path on Home, e.g. Engineering/Canvas'),
229
+ folder: z.unknown().optional().describe('Folder path on Home, e.g. Engineering/Canvas'),
162
230
  };
163
231
  const createSchema = z.looseObject(createShape);
164
232
 
@@ -174,12 +242,12 @@ const boardCreateOperation = defineOperation<z.infer<typeof createSchema>, Recor
174
242
  'Create a new empty board without opening it. Use canvas_board action "open" with the returned board id before writing to it.',
175
243
  extraShape: {
176
244
  name: z.string().describe('Board name'),
177
- category: z.string().optional().describe('Folder path on Home, e.g. Engineering/Canvas'),
245
+ folder: z.string().optional().describe('Folder path on Home, e.g. Engineering/Canvas'),
178
246
  },
179
247
  formatResult: jsonResult,
180
248
  },
181
249
  handler: (input, ctx) => {
182
- const board = canvasState.createBoard(boardName(input.name), boardCategory(input.category) ?? null);
250
+ const board = canvasState.createBoard(boardName(input.name), boardFolder(input.folder) ?? null);
183
251
  if (!board) throw new OperationError('Boards need a workspace database.', 409);
184
252
  emitBoardsChanged(ctx);
185
253
  return { ok: true, board };
@@ -209,8 +277,7 @@ const boardOpenOperation = defineOperation<z.infer<typeof openSchema>, Record<st
209
277
  handler: (input, ctx) => {
210
278
  const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : null;
211
279
  if (!openCanvasBoard(id).ok) throw new OperationError(`Board "${id}" not found.`, 404);
212
- emitBoardsChanged(ctx);
213
- return { ok: true, ...boardsPayload() };
280
+ return { ok: true, ...emitBoardsChanged(ctx) };
214
281
  },
215
282
  });
216
283
 
@@ -219,8 +286,16 @@ const boardOpenOperation = defineOperation<z.infer<typeof openSchema>, Record<st
219
286
  const updateShape = {
220
287
  id: z.unknown().optional().describe('Board id'),
221
288
  name: z.unknown().optional().describe('New name'),
222
- category: z.unknown().optional().describe('Folder path on Home; "" or null unfiles it'),
289
+ relationVocabulary: relationVocabularySchema
290
+ .optional()
291
+ .describe('Board relation vocabulary: open kinds with a meaning ink and direction'),
292
+ folder: z.unknown().optional().describe('Folder path on Home; "" or null unfiles it'),
223
293
  readmeNodeId: z.unknown().optional().describe('Markdown node id to use as README; null clears it'),
294
+ mapPosition: z
295
+ .object({ x: z.number().finite(), y: z.number().finite() })
296
+ .nullable()
297
+ .optional()
298
+ .describe('Persist a Home Map position; null returns to the generated folder layout'),
224
299
  };
225
300
  const updateSchema = z.looseObject(updateShape);
226
301
 
@@ -233,11 +308,11 @@ const boardUpdateOperation = defineOperation<z.infer<typeof updateSchema>, Recor
233
308
  mcp: {
234
309
  toolName: 'canvas_update_board',
235
310
  description:
236
- 'Rename a board, move it into a nested folder on Home (e.g. Engineering/Canvas; category "" unfiles it), or designate its markdown introduction with readmeNodeId. During board authoring, set a useful README when none exists; preserve an existing designation unless the human requests a change. Moving preserves the board ID and content.',
311
+ 'Rename a board, move it into a nested folder on Home (e.g. Engineering/Canvas; folder "" unfiles it), or designate its markdown introduction with readmeNodeId. During board authoring, set a useful README when none exists; preserve an existing designation unless the human requests a change. Moving preserves the board ID and content.',
237
312
  extraShape: {
238
313
  id: z.string().describe('Board id'),
239
314
  name: z.string().optional().describe('New name'),
240
- category: z.string().optional().describe('Folder path, up to 8 levels of 1–60 characters; "" unfiles it'),
315
+ folder: z.string().optional().describe('Folder path, up to 8 levels of 1–60 characters; "" unfiles it'),
241
316
  readmeNodeId: z.string().nullable().optional().describe('Markdown node id used as the board README'),
242
317
  },
243
318
  formatResult: jsonResult,
@@ -246,7 +321,9 @@ const boardUpdateOperation = defineOperation<z.infer<typeof updateSchema>, Recor
246
321
  const id = typeof input.id === 'string' ? input.id : '';
247
322
  const patch = {
248
323
  ...(input.name !== undefined ? { name: boardName(input.name) } : {}),
249
- ...(input.category !== undefined ? { category: boardCategory(input.category) ?? null } : {}),
324
+ ...(input.relationVocabulary !== undefined ? { relationVocabulary: input.relationVocabulary } : {}),
325
+ ...(input.folder !== undefined ? { folder: boardFolder(input.folder) ?? null } : {}),
326
+ ...(input.mapPosition !== undefined ? { mapPosition: input.mapPosition } : {}),
250
327
  };
251
328
  if (input.readmeNodeId !== undefined) {
252
329
  const board = canvasState.readBoard(id, false);
@@ -267,15 +344,14 @@ const boardUpdateOperation = defineOperation<z.infer<typeof updateSchema>, Recor
267
344
  throw new OperationError('README must identify a markdown node on this board.');
268
345
  }
269
346
  }
270
- emitBoardsChanged(ctx);
271
- return { ok: true, ...boardsPayload() };
347
+ return { ok: true, ...emitBoardsChanged(ctx) };
272
348
  },
273
349
  });
274
350
 
275
351
  const fromShape = {
276
352
  sourceBoardId: z.string().describe('Source board id'),
277
353
  name: z.string().describe('New board name'),
278
- category: z.string().optional(),
354
+ folder: z.string().optional(),
279
355
  nodeIds: z.array(z.string()).optional(),
280
356
  includeReadme: z.boolean().optional(),
281
357
  includeStructure: z.boolean().optional(),
@@ -310,13 +386,19 @@ const boardCreateFromOperation = defineOperation<z.infer<typeof fromSchema>, Rec
310
386
  title: typeof node.data.title === 'string' ? node.data.title : null,
311
387
  reusable: !['prompt', 'response', 'trace', 'mcp-app'].includes(node.type),
312
388
  })),
313
- edges: source.layout.edges.map((edge) => ({ id: edge.id, from: edge.from, to: edge.to, type: edge.type })),
389
+ edges: source.layout.edges.map((edge) => ({
390
+ id: edge.id,
391
+ from: edge.from,
392
+ to: edge.to,
393
+ ...(edge.toBoardId !== undefined ? { toBoardId: edge.toBoardId } : {}),
394
+ type: edge.type,
395
+ })),
314
396
  };
315
397
  }
316
398
  const board = canvasState.createBoardFromBoard({
317
399
  ...input,
318
400
  name: boardName(input.name),
319
- category: boardCategory(input.category),
401
+ folder: boardFolder(input.folder),
320
402
  });
321
403
  if (!board) throw new OperationError('Could not create board from source.', 409);
322
404
  emitBoardsChanged(ctx);
@@ -324,6 +406,64 @@ const boardCreateFromOperation = defineOperation<z.infer<typeof fromSchema>, Rec
324
406
  },
325
407
  });
326
408
 
409
+ // ── board.pin / board.unpin ───────────────────────────────────
410
+ // A pinned board joins the agent's working set: its README and pinned cards
411
+ // travel in the brief whichever board is open. Pinning is context, never a
412
+ // write target; writes still go to the open board.
413
+
414
+ const pinShape = {
415
+ id: z.unknown().optional().describe('Board id'),
416
+ reason: z.unknown().optional().describe('Why this board matters; shown to the human on hover'),
417
+ };
418
+ const pinSchema = z.looseObject(pinShape);
419
+
420
+ const boardPinOperation = defineOperation<z.infer<typeof pinSchema>, Record<string, unknown>>({
421
+ name: 'board.pin',
422
+ mutates: false,
423
+ input: pinSchema,
424
+ inputShape: pinShape,
425
+ http: { method: 'POST', path: '/api/canvas/boards/:id/pin' },
426
+ mcp: {
427
+ toolName: 'canvas_pin_board',
428
+ description:
429
+ "Pin a board into the agent's working set: its README and pinned cards reach the brief whichever board is open. Read-only context; writes still go to the open board.",
430
+ extraShape: {
431
+ id: z.string().describe('Board id'),
432
+ reason: z.string().optional().describe('Why this board matters; shown to the human on hover'),
433
+ },
434
+ formatResult: jsonResult,
435
+ },
436
+ handler: (input, ctx) => {
437
+ const id = typeof input.id === 'string' ? input.id.trim() : '';
438
+ const reason =
439
+ typeof input.reason === 'string' && input.reason.trim() ? input.reason.trim().slice(0, 280) : undefined;
440
+ if (!canvasState.pinBoard(id, reason)) throw new OperationError(`Board "${id}" not found.`, 404);
441
+ return { ok: true, ...emitBoardsChanged(ctx) };
442
+ },
443
+ });
444
+
445
+ const unpinShape = { id: z.unknown().optional().describe('Board id') };
446
+ const unpinSchema = z.looseObject(unpinShape);
447
+
448
+ const boardUnpinOperation = defineOperation<z.infer<typeof unpinSchema>, Record<string, unknown>>({
449
+ name: 'board.unpin',
450
+ mutates: false,
451
+ input: unpinSchema,
452
+ inputShape: unpinShape,
453
+ http: { method: 'POST', path: '/api/canvas/boards/:id/unpin' },
454
+ mcp: {
455
+ toolName: 'canvas_unpin_board',
456
+ description: "Remove a board from the agent's working set.",
457
+ extraShape: { id: z.string().describe('Board id') },
458
+ formatResult: jsonResult,
459
+ },
460
+ handler: (input, ctx) => {
461
+ const id = typeof input.id === 'string' ? input.id.trim() : '';
462
+ if (!canvasState.unpinBoard(id)) throw new OperationError(`Board "${id}" not found.`, 404);
463
+ return { ok: true, ...emitBoardsChanged(ctx) };
464
+ },
465
+ });
466
+
327
467
  // ── board.delete (human only) ─────────────────────────────────
328
468
 
329
469
  const deleteShape = {
@@ -342,8 +482,7 @@ const boardDeleteOperation = defineOperation<z.infer<typeof deleteSchema>, Recor
342
482
  // Deleting the open board returns to Home through the same path as opening it.
343
483
  if (id === canvasState.activeBoardId) openCanvasBoard(null);
344
484
  if (!canvasState.deleteBoard(id)) throw new OperationError(`Board "${id}" not found.`, 404);
345
- emitBoardsChanged(ctx);
346
- return { ok: true, ...boardsPayload() };
485
+ return { ok: true, ...emitBoardsChanged(ctx) };
347
486
  },
348
487
  });
349
488
 
@@ -354,5 +493,7 @@ export const boardOperations: Operation[] = [
354
493
  boardCreateOperation,
355
494
  boardCreateFromOperation,
356
495
  boardUpdateOperation,
496
+ boardPinOperation,
497
+ boardUnpinOperation,
357
498
  boardDeleteOperation,
358
499
  ];
@@ -7,6 +7,7 @@
7
7
  * This module must never import server.ts or index.ts.
8
8
  */
9
9
  import { z } from 'zod';
10
+ import { workspaceSettingsPatchSchema } from '../../../shared/workspace-settings.js';
10
11
  import { normalizeCanvasTheme } from '../../canvas-db.js';
11
12
  import { applyCanvasNodeUpdates } from '../../canvas-operations.js';
12
13
  import { canvasState } from '../../canvas-state.js';
@@ -98,4 +99,33 @@ const applyUpdatesOperation = defineOperation<z.infer<typeof applyUpdatesSchema>
98
99
  },
99
100
  });
100
101
 
101
- export const canvasWireOperations: Operation[] = [themeGetOperation, themeSetOperation, applyUpdatesOperation];
102
+ const settingsGetOperation = defineOperation({
103
+ name: 'settings.get',
104
+ mutates: false,
105
+ input: emptySchema,
106
+ inputShape: emptyShape,
107
+ http: { method: 'GET', path: '/api/canvas/settings' },
108
+ handler: () => ({ ok: true, workspaceSettings: canvasState.getWorkspaceSettings() }),
109
+ });
110
+
111
+ const settingsSetOperation = defineOperation({
112
+ name: 'settings.set',
113
+ mutates: false,
114
+ // Strip transport attribution/precondition fields before storing the patch.
115
+ input: z.object(workspaceSettingsPatchSchema.shape),
116
+ inputShape: workspaceSettingsPatchSchema.shape,
117
+ http: { method: 'POST', path: '/api/canvas/settings' },
118
+ handler: (input, ctx) => {
119
+ const workspaceSettings = canvasState.updateWorkspaceSettings(input);
120
+ ctx.emit('settings-changed', { workspaceSettings });
121
+ return { ok: true, workspaceSettings };
122
+ },
123
+ });
124
+
125
+ export const canvasWireOperations: Operation[] = [
126
+ themeGetOperation,
127
+ themeSetOperation,
128
+ settingsGetOperation,
129
+ settingsSetOperation,
130
+ applyUpdatesOperation,
131
+ ];
@@ -4,6 +4,7 @@
4
4
  * This module must never import server.ts or index.ts.
5
5
  */
6
6
  import { z } from 'zod';
7
+ import { relationKindSchema, relationReasonSchema } from '../../../shared/relations.js';
7
8
  import type { CanvasEdge } from '../../canvas-state.js';
8
9
  import { addCanvasEdge, removeCanvasEdge, updateCanvasEdge } from '../../canvas-operations.js';
9
10
  import { readJsonValue } from '../http.js';
@@ -16,6 +17,8 @@ const VALID_EDGE_STYLES = new Set(['solid', 'dashed', 'dotted']);
16
17
  // ── edge.add ──────────────────────────────────────────────────
17
18
 
18
19
  const edgeAddShape = {
20
+ destinationBoardId: z.string().min(1).optional().describe('Board owning the source and edge; does not activate it'),
21
+ requestId: z.string().min(1).optional().describe('Logical creation identity for retry-safe edge creation'),
19
22
  intentId: z
20
23
  .string()
21
24
  .optional()
@@ -23,6 +26,11 @@ const edgeAddShape = {
23
26
  .describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
24
27
  from: z.string().optional().catch(undefined).describe('Source node ID'),
25
28
  to: z.string().optional().catch(undefined).describe('Target node ID'),
29
+ toBoardId: z
30
+ .string()
31
+ .min(1)
32
+ .optional()
33
+ .describe('Target board ID for a cross-board edge; independent of the source destinationBoardId'),
26
34
  fromSearch: z
27
35
  .string()
28
36
  .optional()
@@ -34,6 +42,10 @@ const edgeAddShape = {
34
42
  .catch(undefined)
35
43
  .describe('Resolve the target node by exact or fuzzy title/content search'),
36
44
  type: z.unknown().optional().describe('Edge type: flow, depends-on, relation, or references'),
45
+ kind: relationKindSchema
46
+ .optional()
47
+ .describe('Open semantic kind, e.g. supports, contradicts, derived-from, supersedes'),
48
+ reason: relationReasonSchema.optional().describe('Why this relation exists'),
37
49
  label: z.unknown().optional().describe('Edge label text'),
38
50
  style: z.unknown().optional().describe('Optional edge stroke style: solid, dashed, or dotted'),
39
51
  animated: z.unknown().optional().describe('Animate the edge stroke'),
@@ -80,7 +92,10 @@ const edgeAddOperation = defineOperation<z.infer<typeof edgeAddSchema>, CanvasEd
80
92
  id: body.id,
81
93
  from: body.from,
82
94
  to: body.to,
95
+ ...(body.toBoardId ? { toBoardId: body.toBoardId } : {}),
83
96
  type: body.type,
97
+ kind: body.kind,
98
+ reason: body.reason,
84
99
  label: body.label,
85
100
  style: body.style,
86
101
  animated: body.animated,
@@ -108,16 +123,23 @@ const edgeAddOperation = defineOperation<z.infer<typeof edgeAddSchema>, CanvasEd
108
123
  }
109
124
  try {
110
125
  return addCanvasEdge({
126
+ destinationBoardId: input.destinationBoardId,
127
+ requestId: input.requestId,
128
+ intentId: input.intentId,
111
129
  ...(typeof body.from === 'string' ? { from: body.from } : {}),
112
130
  ...(typeof body.to === 'string' ? { to: body.to } : {}),
131
+ ...(typeof body.toBoardId === 'string' ? { toBoardId: body.toBoardId } : {}),
113
132
  ...(typeof body.fromSearch === 'string' ? { fromSearch: body.fromSearch } : {}),
114
133
  ...(typeof body.toSearch === 'string' ? { toSearch: body.toSearch } : {}),
115
134
  type: rawType as CanvasEdge['type'],
135
+ ...(input.kind ? { kind: input.kind } : {}),
136
+ ...(input.reason ? { reason: input.reason } : {}),
116
137
  ...(body.label ? { label: String(body.label) } : {}),
117
138
  ...(style ? { style: style as CanvasEdge['style'] } : {}),
118
139
  ...(body.animated !== undefined ? { animated: Boolean(body.animated) } : {}),
119
140
  });
120
141
  } catch (error) {
142
+ if (error instanceof OperationError) throw error;
121
143
  throw new OperationError(error instanceof Error ? error.message : 'Duplicate or self-edge.');
122
144
  }
123
145
  },
@@ -129,6 +151,8 @@ const edgeAddOperation = defineOperation<z.infer<typeof edgeAddSchema>, CanvasEd
129
151
  const edgeUpdateShape = {
130
152
  id: z.string().optional().catch(undefined).describe('Edge ID to update'),
131
153
  type: z.unknown().optional().describe('New edge type: flow, depends-on, relation, or references'),
154
+ kind: relationKindSchema.nullable().optional().describe('Semantic kind; null clears it'),
155
+ reason: relationReasonSchema.nullable().optional().describe('Why this relation exists; null clears it'),
132
156
  label: z.unknown().optional().describe('New label text; empty string or null clears the label'),
133
157
  style: z.unknown().optional().describe('New stroke style: solid, dashed, or dotted'),
134
158
  animated: z.unknown().optional().describe('Animate the edge stroke'),
@@ -162,7 +186,15 @@ const edgeUpdateOperation = defineOperation<z.infer<typeof edgeUpdateSchema>, Ca
162
186
  {
163
187
  type: 'text' as const,
164
188
  text: JSON.stringify(
165
- { id: body.id, type: body.type, label: body.label, style: body.style, animated: body.animated },
189
+ {
190
+ id: body.id,
191
+ type: body.type,
192
+ kind: body.kind,
193
+ reason: body.reason,
194
+ label: body.label,
195
+ style: body.style,
196
+ animated: body.animated,
197
+ },
166
198
  null,
167
199
  2,
168
200
  ),
@@ -177,7 +209,9 @@ const edgeUpdateOperation = defineOperation<z.infer<typeof edgeUpdateSchema>, Ca
177
209
  if (!id) {
178
210
  throw new OperationError('Missing required field: id.');
179
211
  }
180
- const patch: Partial<Pick<CanvasEdge, 'type' | 'label' | 'style' | 'animated'>> = {};
212
+ const patch: Partial<Pick<CanvasEdge, 'type' | 'kind' | 'reason' | 'label' | 'style' | 'animated'>> = {};
213
+ if ('kind' in input) patch.kind = input.kind ?? undefined;
214
+ if ('reason' in input) patch.reason = input.reason || undefined;
181
215
  if ('type' in body && body.type !== undefined) {
182
216
  if (typeof body.type !== 'string' || !VALID_EDGE_TYPES.has(body.type)) {
183
217
  throw new OperationError(`Invalid edge type: "${String(body.type)}".`);
@@ -21,6 +21,8 @@ function pickChildLayout(value: unknown): 'grid' | 'column' | 'flow' | undefined
21
21
  // ── group.create ──────────────────────────────────────────────
22
22
 
23
23
  const groupCreateShape = {
24
+ destinationBoardId: z.string().min(1).optional().describe('Create the group on this board without activating it'),
25
+ requestId: z.string().min(1).optional().describe('Logical creation identity for retry-safe group creation'),
24
26
  intentId: z
25
27
  .string()
26
28
  .optional()
@@ -90,7 +92,7 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
90
92
  const width = typeof body.width === 'number' ? body.width : undefined;
91
93
  const height = typeof body.height === 'number' ? body.height : undefined;
92
94
  const childLayout = pickChildLayout(body.childLayout);
93
- if (childIds.length > 0) {
95
+ if (childIds.length > 0 && input.destinationBoardId === undefined && input.requestId === undefined) {
94
96
  const missingChildIds = childIds.filter((id) => !canvasState.getNode(id));
95
97
  if (missingChildIds.length > 0) {
96
98
  throw new OperationError(
@@ -99,6 +101,9 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
99
101
  }
100
102
  }
101
103
  return createCanvasGroup({
104
+ destinationBoardId: input.destinationBoardId,
105
+ requestId: input.requestId,
106
+ intentId: input.intentId,
102
107
  title,
103
108
  childIds,
104
109
  color,