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
@@ -146,12 +146,23 @@ export function takeOverNode(nodeId: string, title: string): void {
146
146
  }
147
147
 
148
148
  let heartbeat: ReturnType<typeof setInterval> | null = null;
149
+ let leave: (() => void) | null = null;
150
+
151
+ export function stopHumanPresence(): void {
152
+ if (heartbeat) clearInterval(heartbeat);
153
+ heartbeat = null;
154
+ if (pending) clearTimeout(pending);
155
+ pending = null;
156
+ if (leave) window.removeEventListener('pagehide', leave);
157
+ leave = null;
158
+ }
149
159
 
150
160
  /** Announce this tab and keep it alive; renews a held grab. Call once per connection. */
151
- export function startHumanPresence(): () => void {
161
+ export function startHumanPresence(): void {
162
+ stopHumanPresence();
152
163
  post({ cursor: lastCursor, grabbingNodeId: grabbingNodeId.value });
153
164
  heartbeat = setInterval(() => post({ grabbingNodeId: grabbingNodeId.value }), 3000);
154
- const leave = () => {
165
+ leave = () => {
155
166
  try {
156
167
  navigator.sendBeacon?.(
157
168
  '/api/canvas/human-presence',
@@ -162,11 +173,6 @@ export function startHumanPresence(): () => void {
162
173
  }
163
174
  };
164
175
  window.addEventListener('pagehide', leave);
165
- return () => {
166
- if (heartbeat) clearInterval(heartbeat);
167
- heartbeat = null;
168
- window.removeEventListener('pagehide', leave);
169
- };
170
176
  }
171
177
 
172
178
  export function resetHumanPresence(): void {
@@ -1,10 +1,11 @@
1
- import { canvasArea } from '../canvas/canvas-area';
1
+ import { canvasArea, canvasFitInsets } from '../canvas/canvas-area';
2
2
  import { showToast } from './attention-bridge';
3
3
  import { isHostedWorkbench, workbenchFetch } from './workbench-transport';
4
4
  import type { AgentPresenceSnapshot } from '../../shared/agent-presence.js';
5
- import type { CanvasEdge } from '../types';
5
+ import type { CanvasEdge, CanvasNodeState } from '../types';
6
6
  import { addEdge, canvasInteractionEpoch, nodes } from './canvas-store';
7
- import { boardsLoaded } from './boards-store';
7
+ import { activeBoardId, boardsLoaded } from './boards-store';
8
+ import { humanClientId } from './human-store';
8
9
  function logRequestError(action: string, error: unknown): void {
9
10
  console.error(`[intent-bridge] ${action} failed`, error);
10
11
  }
@@ -141,7 +142,11 @@ export async function vetoGhostIntent(intent: {
141
142
  }
142
143
 
143
144
  /** Fetch rendered markdown HTML from the server. */
144
- export async function renderMarkdown(markdown: string): Promise<string> {
145
+ export async function renderMarkdown(
146
+ markdown: string,
147
+ nodeId?: string,
148
+ expectedBoardId: string | null = activeBoardId.value,
149
+ ): Promise<string | null> {
145
150
  const data = await requestJson<{ html?: string }>(
146
151
  'renderMarkdown',
147
152
  '/api/render',
@@ -149,10 +154,10 @@ export async function renderMarkdown(markdown: string): Promise<string> {
149
154
  {
150
155
  method: 'POST',
151
156
  headers: { 'Content-Type': 'application/json' },
152
- body: JSON.stringify({ markdown }),
157
+ body: JSON.stringify({ markdown, ...(nodeId ? { nodeId, expectedBoardId } : {}) }),
153
158
  },
154
159
  );
155
- return data.html ?? '';
160
+ return data.html ?? null;
156
161
  }
157
162
 
158
163
  /** Fetch file content from the server. */
@@ -163,20 +168,6 @@ export async function fetchFile(path: string): Promise<{
163
168
  return requestJson('fetchFile', `/api/file?path=${encodeURIComponent(path)}`, { content: '' });
164
169
  }
165
170
 
166
- /** Save file content to the server. */
167
- export async function saveFile(path: string, content: string): Promise<{ ok: boolean; updatedAt?: string }> {
168
- return requestJson(
169
- 'saveFile',
170
- '/api/file/save',
171
- { ok: false },
172
- {
173
- method: 'POST',
174
- headers: { 'Content-Type': 'application/json' },
175
- body: JSON.stringify({ path, content }),
176
- },
177
- );
178
- }
179
-
180
171
  /** Fetch current workbench state. */
181
172
  export async function fetchWorkbenchState(): Promise<Record<string, unknown>> {
182
173
  return requestJson('fetchWorkbenchState', '/api/workbench/state', {});
@@ -192,8 +183,12 @@ export async function openWorkbenchFile(path: string): Promise<{ ok: boolean }>
192
183
  }
193
184
 
194
185
  /** Fetch canvas state from server. */
195
- export async function fetchCanvasState(): Promise<Record<string, unknown>> {
196
- return requestJson('fetchCanvasState', '/api/canvas/state?includeBlobs=true', {});
186
+ export async function fetchCanvasState(expectedBoardId: string | null): Promise<Record<string, unknown>> {
187
+ return requestJson(
188
+ 'fetchCanvasState',
189
+ `/api/canvas/state?includeBlobs=true&expectedBoardId=${encodeURIComponent(expectedBoardId ?? 'null')}`,
190
+ {},
191
+ );
197
192
  }
198
193
 
199
194
  export async function saveCanvasTheme(theme: string): Promise<{ ok: boolean; theme?: string }> {
@@ -262,7 +257,11 @@ export async function pushCanvasUpdate(
262
257
  await requestBestEffort('pushCanvasUpdate', '/api/canvas/update', {
263
258
  method: 'POST',
264
259
  headers: { 'Content-Type': 'application/json' },
265
- body: JSON.stringify({ updates, ...(options.recordHistory === false ? { recordHistory: false } : {}) }),
260
+ body: JSON.stringify({
261
+ updates,
262
+ ...(boardsLoaded.value ? { expectedBoardId: activeBoardId.value } : {}),
263
+ ...(options.recordHistory === false ? { recordHistory: false } : {}),
264
+ }),
266
265
  });
267
266
  }
268
267
 
@@ -272,6 +271,7 @@ export async function createEdgeFromClient(
272
271
  to: string,
273
272
  type: string,
274
273
  label?: string,
274
+ options: { toBoardId?: string; expectedBoardId?: string | null } = {},
275
275
  ): Promise<{ ok: boolean; id?: string }> {
276
276
  const epoch = canvasInteractionEpoch.value;
277
277
  const result = await requestJson<({ ok: true } & CanvasEdge) | { ok: false; error?: string }>(
@@ -281,10 +281,10 @@ export async function createEdgeFromClient(
281
281
  {
282
282
  method: 'POST',
283
283
  headers: { 'Content-Type': 'application/json' },
284
- body: JSON.stringify({ from, to, type, label }),
284
+ body: JSON.stringify({ from, to, type, label, ...options }),
285
285
  },
286
286
  );
287
- if (epoch === canvasInteractionEpoch.value && nodes.value.has(from) && nodes.value.has(to)) {
287
+ if (epoch === canvasInteractionEpoch.value && nodes.value.has(from) && (options.toBoardId || nodes.value.has(to))) {
288
288
  if (result.ok) {
289
289
  const { ok, ...edge } = result;
290
290
  addEdge(edge);
@@ -333,8 +333,9 @@ export async function updateNodeFromClient(
333
333
  children?: string[];
334
334
  expectedContentRevision?: number;
335
335
  expectedBoardId?: string | null;
336
+ persistFile?: boolean;
336
337
  },
337
- ): Promise<{ ok: boolean; id?: string; contentRevision?: number }> {
338
+ ): Promise<{ ok: boolean; id?: string; contentRevision?: number; lastEditedBy?: CanvasNodeState['lastEditedBy'] }> {
338
339
  return requestJson(
339
340
  'updateNodeFromClient',
340
341
  `/api/canvas/node/${encodeURIComponent(id)}`,
@@ -451,6 +452,8 @@ export async function updateViewportFromClient(
451
452
  body: JSON.stringify({
452
453
  ...viewport,
453
454
  ...clientWindowSize(),
455
+ clientId: humanClientId,
456
+ ...(boardsLoaded.value ? { expectedBoardId: activeBoardId.value } : {}),
454
457
  ...(options.recordHistory === false ? { recordHistory: false } : {}),
455
458
  }),
456
459
  },
@@ -462,7 +465,13 @@ export async function updateViewportFromClient(
462
465
  * `fit` computes against the human's REAL window instead of a hardcoded
463
466
  * 1440x900 (0.4.6 orb feedback #2: fit clipped nodes on both sides).
464
467
  */
465
- function clientWindowSize(): { clientWidth: number; clientHeight: number } | Record<string, never> {
468
+ function clientWindowSize():
469
+ | {
470
+ clientWidth: number;
471
+ clientHeight: number;
472
+ clientInsets: { top: number; bottom: number };
473
+ }
474
+ | Record<string, never> {
466
475
  if (typeof window === 'undefined') return {};
467
476
  // The CANVAS REGION, not the window — with the rail + top-bar shell a
468
477
  // window-sized fit would hide nodes under the chrome.
@@ -470,7 +479,8 @@ function clientWindowSize(): { clientWidth: number; clientHeight: number } | Rec
470
479
  const clientWidth = Math.round(area.width);
471
480
  const clientHeight = Math.round(area.height);
472
481
  if (!clientWidth || !clientHeight) return {};
473
- return { clientWidth, clientHeight };
482
+ const { top, bottom } = canvasFitInsets();
483
+ return { clientWidth, clientHeight, clientInsets: { top, bottom } };
474
484
  }
475
485
 
476
486
  /**
@@ -488,7 +498,11 @@ export async function reportClientViewportSize(): Promise<void> {
488
498
  await requestBestEffort('reportClientViewportSize', '/api/canvas/viewport', {
489
499
  method: 'POST',
490
500
  headers: { 'Content-Type': 'application/json' },
491
- body: JSON.stringify({ ...size, recordHistory: false }),
501
+ body: JSON.stringify({
502
+ ...size,
503
+ ...(boardsLoaded.value ? { expectedBoardId: activeBoardId.value } : {}),
504
+ recordHistory: false,
505
+ }),
492
506
  });
493
507
  }
494
508
 
@@ -595,7 +609,14 @@ export async function deleteSnapshot(id: string): Promise<{ ok: boolean }> {
595
609
  /** Patch an edge in place (label/type/style/animated) via the server. */
596
610
  export async function updateEdgeFromClient(
597
611
  edgeId: string,
598
- patch: { label?: string; type?: string; style?: string; animated?: boolean },
612
+ patch: {
613
+ kind?: string | null;
614
+ reason?: string | null;
615
+ label?: string;
616
+ type?: string;
617
+ style?: string;
618
+ animated?: boolean;
619
+ },
599
620
  ): Promise<{ ok: boolean }> {
600
621
  return requestJson(
601
622
  'updateEdgeFromClient',
@@ -0,0 +1,43 @@
1
+ import { computed, signal } from '@preact/signals';
2
+ import { knowledgeLifecycleEnabled } from './workspace-settings';
3
+ import type { KnowledgeResults } from '../../server/knowledge-lifecycle.js';
4
+ import { memoryKey } from '../../shared/memory-graph.js';
5
+ import { activeBoardId, boardIncarnation, lifecycleFilter } from './boards-store';
6
+ import { contextPinnedNodeIds } from './canvas-store';
7
+ import { requestJson } from './intent-bridge';
8
+
9
+ import { lifecycleTarget } from './boards-store';
10
+ export { lifecycleFilter, lifecycleTarget } from './boards-store';
11
+
12
+ export const knowledge = signal<KnowledgeResults['list'] | null>(null);
13
+ export const knowledgeUnavailable = signal(false);
14
+ export const effectiveLifecycleFilter = computed(() =>
15
+ knowledgeLifecycleEnabled.value ? lifecycleFilter.value : 'All',
16
+ );
17
+ export const retirementUndo = signal<{ boardId: string; nodeId: string; incarnation: number } | null>(null);
18
+ let sequence = 0;
19
+
20
+ export async function refreshKnowledge(): Promise<void> {
21
+ const request = ++sequence;
22
+ const incarnation = boardIncarnation;
23
+ const result = await requestJson<KnowledgeResults['list'] | null>('knowledge', '/api/canvas/knowledge', null);
24
+ if (request !== sequence || incarnation !== boardIncarnation) return;
25
+ knowledgeUnavailable.value = result?.ok !== true;
26
+ if (result?.ok === true) knowledge.value = result;
27
+ }
28
+
29
+ export function cardQualification(nodeId: string) {
30
+ return activeBoardId.value
31
+ ? knowledge.value?.cards[memoryKey({ boardId: activeBoardId.value, cardId: nodeId })]
32
+ : undefined;
33
+ }
34
+
35
+ /** Raw pin membership survives retirement, but retired cards are not delivered. */
36
+ export const activeContextPinnedNodeIds = computed(
37
+ () => new Set([...contextPinnedNodeIds.value].filter((id) => !cardQualification(id)?.retired)),
38
+ );
39
+
40
+ export function openLifecycle(nodeId: string): void {
41
+ if (knowledgeLifecycleEnabled.value && activeBoardId.value)
42
+ lifecycleTarget.value = { boardId: activeBoardId.value, nodeId, incarnation: boardIncarnation };
43
+ }
@@ -1,10 +1,12 @@
1
- import { computed } from '@preact/signals';
2
- import { findNeighborhoods } from '../../server/spatial-analysis.js';
3
- import { contextPinnedNodeIds, nodes } from './canvas-store';
1
+ import { computed, signal } from '@preact/signals';
2
+ import { findNeighborhoods, gapDistance } from '../../server/spatial-analysis.js';
3
+ import { contextPinnedNodeIds, nodes, selectedNodeIds } from './canvas-store';
4
+ import { grabbingNodeId } from './human-store';
5
+ import { activeContextPinnedNodeIds, cardQualification } from './knowledge-store';
4
6
 
5
7
  /**
6
8
  * Near a pin (docs/design/NearPin.dc.html): each pinned node pulls up to five
7
- * unpinned nodes within 600 px into the agent's brief as title + short
9
+ * unpinned nodes within a 600 px edge-to-edge gap into the agent's brief as title + short
8
10
  * summary. Same rule as the server's brief (`findNeighborhoods`), so the mark
9
11
  * claims exactly what the agent receives.
10
12
  */
@@ -22,11 +24,12 @@ let lastNear = new Map<string, NearPin[]>();
22
24
  * changes: the chips subscribed to it re-render only when a neighbourhood does.
23
25
  */
24
26
  export const nearPins = computed<Map<string, NearPin[]>>(() => {
25
- const pins = contextPinnedNodeIds.value;
27
+ const pins = activeContextPinnedNodeIds.value;
26
28
  const near = new Map<string, NearPin[]>();
27
29
  if (pins.size > 0) {
28
30
  const nearest = new Map<string, Array<NearPin & { distance: number }>>();
29
- for (const neighborhood of findNeighborhoods([...nodes.value.values()], pins)) {
31
+ const activeNodes = [...nodes.value.values()].filter((node) => !cardQualification(node.id)?.retired);
32
+ for (const neighborhood of findNeighborhoods(activeNodes, pins)) {
30
33
  const pinTitle = neighborhood.pinnedNodeTitle ?? neighborhood.pinnedNodeId;
31
34
  for (const neighbor of neighborhood.neighbors) {
32
35
  const list = nearest.get(neighbor.id) ?? [];
@@ -51,3 +54,109 @@ export const nearPins = computed<Map<string, NearPin[]>>(() => {
51
54
  lastNear = near;
52
55
  return near;
53
56
  });
57
+
58
+ /** Pin id → its neighbours' ids: the tether layer looks a pin up, never scans the near map. */
59
+ export const neighboursByPin = computed<Map<string, string[]>>(() => {
60
+ const byPin = new Map<string, string[]>();
61
+ for (const [nodeId, pins] of nearPins.value) {
62
+ for (const pin of pins) {
63
+ const list = byPin.get(pin.pinNodeId) ?? [];
64
+ list.push(nodeId);
65
+ byPin.set(pin.pinNodeId, list);
66
+ }
67
+ }
68
+ return byPin;
69
+ });
70
+
71
+ /** The node under the pointer: a hovered pin draws its neighbours (NearPin.dc.html §2). */
72
+ export const hoveredNodeId = signal<string | null>(null);
73
+ /** A near node whose pins the human asked to see from its chip ("Show pins"). */
74
+ export const shownPinsFor = signal<string | null>(null);
75
+
76
+ /**
77
+ * Pins drawn with tethers and the 600 px boundary around their edges: the pin being dragged,
78
+ * hovered or selected, or the pins of the near node whose chip said "Show
79
+ * pins". Dragging an unpinned node rings only its nearest pin (§3).
80
+ */
81
+ export const tetherFocus = computed<{ pinIds: string[]; ringOnly: boolean }>(() => {
82
+ const pins = contextPinnedNodeIds.value;
83
+ if (pins.size === 0) return { pinIds: [], ringOnly: false };
84
+ const grabbed = grabbingNodeId.value;
85
+ if (grabbed && !pins.has(grabbed)) {
86
+ const dragged = nodes.value.get(grabbed);
87
+ let nearest: string | null = null;
88
+ let best = Number.POSITIVE_INFINITY;
89
+ for (const id of pins) {
90
+ const pin = nodes.value.get(id);
91
+ if (!dragged || !pin) continue;
92
+ const distance = gapDistance(dragged, pin);
93
+ if (distance < best) {
94
+ best = distance;
95
+ nearest = id;
96
+ }
97
+ }
98
+ return { pinIds: nearest ? [nearest] : [], ringOnly: true };
99
+ }
100
+ const ids = new Set<string>();
101
+ if (grabbed) ids.add(grabbed);
102
+ const hovered = hoveredNodeId.value;
103
+ if (hovered && pins.has(hovered)) ids.add(hovered);
104
+ for (const id of selectedNodeIds.value) if (pins.has(id)) ids.add(id);
105
+ const shown = shownPinsFor.value;
106
+ for (const pin of (shown && nearPins.value.get(shown)) || []) ids.add(pin.pinNodeId);
107
+ return { pinIds: [...ids], ringOnly: false };
108
+ });
109
+
110
+ /** The moment it changes (§3): the chip pulses once on a drop inside, fades on a drop outside. */
111
+ export const nearChange = signal<{ nodeId: string; kind: 'in' | 'out'; pins: NearPin[] } | null>(null);
112
+ let nearAtGrab: NearPin[] | undefined;
113
+
114
+ export function noteNearAtGrab(nodeId: string): void {
115
+ clearNearChange(nodeId);
116
+ nearAtGrab = nearPins.value.get(nodeId);
117
+ }
118
+
119
+ export function settleNearAfterDrop(nodeId: string): void {
120
+ const before = nearAtGrab;
121
+ nearAtGrab = undefined;
122
+ const after = nearPins.value.get(nodeId);
123
+ if (after && !before) nearChange.value = { nodeId, kind: 'in', pins: after };
124
+ else if (before && !after) nearChange.value = { nodeId, kind: 'out', pins: before };
125
+ }
126
+
127
+ export function clearNearChange(nodeId: string): void {
128
+ if (nearChange.value?.nodeId === nodeId) nearChange.value = null;
129
+ }
130
+
131
+ /**
132
+ * The open near chip's card (§2), drawn over the canvas because a node paints
133
+ * contained. `at` is the chip's screen rect when it opened; the card closes
134
+ * when the view moves rather than chase a stale anchor.
135
+ */
136
+ export const nearCard = signal<{
137
+ nodeId: string;
138
+ at: { left: number; right: number; top: number; bottom: number };
139
+ view: string;
140
+ } | null>(null);
141
+ let closeTimer: ReturnType<typeof setTimeout> | null = null;
142
+
143
+ export function openNearCard(nodeId: string, chip: Element, view: string): void {
144
+ if (closeTimer) clearTimeout(closeTimer);
145
+ closeTimer = null;
146
+ const rect = chip.getBoundingClientRect();
147
+ nearCard.value = { nodeId, at: { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom }, view };
148
+ }
149
+
150
+ /** Leaving the chip for the card must not close it: close after a short grace. */
151
+ export function closeNearCardSoon(): void {
152
+ if (closeTimer) clearTimeout(closeTimer);
153
+ closeTimer = setTimeout(() => {
154
+ closeTimer = null;
155
+ nearCard.value = null;
156
+ }, 160);
157
+ }
158
+
159
+ export function keepNearCard(): void {
160
+ if (closeTimer) clearTimeout(closeTimer);
161
+ closeTimer = null;
162
+ }
@@ -1,13 +1,12 @@
1
1
  import { computed, signal } from '@preact/signals';
2
2
  import type { PmxAxIntent } from '../../shared/ax-intent.js';
3
3
  import { intents } from './intent-store';
4
+ import { agentSessionsEnabled } from './workspace-settings';
4
5
  import {
5
6
  HUMAN_STARTED_SESSION_LABEL,
6
7
  type AgentActivityEntry,
7
8
  type AgentPresence,
8
9
  type AgentPresenceSnapshot,
9
- CONTEXT_BUDGET_DEFAULT_TOKENS,
10
- type ContextBudget,
11
10
  externalWriters,
12
11
  isSessionActive,
13
12
  } from '../../shared/agent-presence.js';
@@ -21,7 +20,6 @@ import {
21
20
  * panel, command bar, presence layer, and top-bar chip all mount on it.
22
21
  */
23
22
  export const agentPresences = signal<AgentPresence[]>([]);
24
- export const contextBudget = signal<ContextBudget>({ used: 0, total: CONTEXT_BUDGET_DEFAULT_TOKENS });
25
23
  /** Recent agent writes, newest first — the External Steering activity feed. */
26
24
  export const agentActivity = signal<AgentActivityEntry[]>([]);
27
25
 
@@ -59,11 +57,14 @@ export function relativeAge(iso: string, now = Date.now()): string {
59
57
  return `${Math.floor(ms / 3_600_000)}h`;
60
58
  }
61
59
 
62
- /** Master gate for every agent surface: an attached session exists. */
60
+ /** Attachment state is independent of whether the human opted into its UI. */
63
61
  export const sessionActive = computed(() => isSessionActive(agentPresences.value));
62
+ export const sessionChromeActive = computed(() => agentSessionsEnabled.value && sessionActive.value);
64
63
 
65
- /** Live writers with no attached session — the External Steering mode. */
66
- export const externalWriterPresences = computed(() => externalWriters(agentPresences.value));
64
+ /** With session UI off, attached agents still appear as ordinary live writers. */
65
+ export const externalWriterPresences = computed(() =>
66
+ agentSessionsEnabled.value ? externalWriters(agentPresences.value) : agentPresences.value,
67
+ );
67
68
 
68
69
  /**
69
70
  * Connected agents the human can ADDRESS from the composer: every live
@@ -147,15 +148,11 @@ export function presenceWorldPosition(
147
148
  export function applyPresenceSnapshot(snapshot: Partial<AgentPresenceSnapshot> | null | undefined): void {
148
149
  if (!snapshot) return;
149
150
  if (Array.isArray(snapshot.presences)) agentPresences.value = snapshot.presences;
150
- if (snapshot.budget && Number.isFinite(snapshot.budget.used) && Number.isFinite(snapshot.budget.total)) {
151
- contextBudget.value = { used: snapshot.budget.used, total: snapshot.budget.total };
152
- }
153
151
  if (Array.isArray(snapshot.activity)) agentActivity.value = snapshot.activity;
154
152
  }
155
153
 
156
154
  export function resetPresence(): void {
157
155
  agentPresences.value = [];
158
- contextBudget.value = { used: 0, total: CONTEXT_BUDGET_DEFAULT_TOKENS };
159
156
  agentActivity.value = [];
160
157
  activityFeedOpen.value = false;
161
158
  writersSheetOpen.value = false;
@@ -1,7 +1,98 @@
1
1
  import { signal } from '@preact/signals';
2
+ import { interpolateCamera, resolveStop, type TourStop } from '../../shared/tour.js';
3
+ import { canvasArea } from '../canvas/canvas-area';
4
+ import type { ViewportState } from '../types';
5
+ import { animateCameraPath, nodes, viewport } from './canvas-store';
6
+ import { requestBestEffort, requestJson } from './intent-bridge';
2
7
 
3
8
  export const presenting = signal(false);
4
9
  export const recordingCamera = signal(false);
10
+ /** Why the current stop cannot be shown (missing target, no stops, server unreachable). */
11
+ export const presentationError = signal('');
12
+
5
13
  export function ownsCamera(): boolean {
6
14
  return presenting.value || recordingCamera.value;
7
15
  }
16
+
17
+ function prefersReducedMotion(): boolean {
18
+ return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
19
+ }
20
+
21
+ /**
22
+ * Ease this viewer's camera to a stop: log-space zoom, optional pull-back.
23
+ * Reduced motion jumps straight to the end. Throws when the target is missing.
24
+ */
25
+ export function playStop(stop: TourStop, onDone?: (end: ViewportState) => void): void {
26
+ const area = canvasArea();
27
+ const target = resolveStop(stop, [...nodes.value.values()], area.width, area.height);
28
+ const from = { ...viewport.value };
29
+ const duration = prefersReducedMotion() ? 0 : (stop.duration ?? 1) * 1000;
30
+ animateCameraPath(
31
+ (t) => interpolateCamera(from, target, t, area.width, area.height, stop.easing, stop.pullback),
32
+ duration,
33
+ onDone,
34
+ );
35
+ }
36
+
37
+ let stepSeq = 0;
38
+
39
+ /**
40
+ * Show a tour stop, entering presentation if needed. Entering hides the
41
+ * chrome, so the canvas area is measured only after that layout has
42
+ * committed (two frames); the latest step always wins.
43
+ */
44
+ export function presentStop(stop: TourStop): void {
45
+ const seq = ++stepSeq;
46
+ const entering = !presenting.value;
47
+ presenting.value = true;
48
+ presentationError.value = '';
49
+ const play = () => {
50
+ if (seq !== stepSeq) return;
51
+ try {
52
+ playStop(stop);
53
+ } catch (error) {
54
+ presentationError.value = error instanceof Error ? error.message : String(error);
55
+ }
56
+ };
57
+ if (entering) requestAnimationFrame(() => requestAnimationFrame(play));
58
+ else play();
59
+ }
60
+
61
+ /**
62
+ * Move the server's shared tour cursor. The `canvas-tour-step` echo does the
63
+ * animating, here and in every other presenting viewer, so a human's keys and
64
+ * an agent's `tour.go` always agree on where the tour is.
65
+ */
66
+ export async function stepTour(to: { stop: number } | { step: 'next' | 'previous' }): Promise<void> {
67
+ const result = await requestJson<{ ok: boolean; error?: string }>(
68
+ 'step tour',
69
+ '/api/canvas/tour/go',
70
+ { ok: false, error: 'Could not reach the canvas server.' },
71
+ {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({ ...to, present: false }),
75
+ },
76
+ );
77
+ if (!result.ok) presentationError.value = result.error ?? 'Could not change the tour stop.';
78
+ }
79
+
80
+ /** Present from the first stop, or resume where the tour already is (`?present=1`). */
81
+ export async function startPresentation(resume = false): Promise<void> {
82
+ presenting.value = true;
83
+ presentationError.value = '';
84
+ const at = resume
85
+ ? (await requestJson<{ position?: number | null }>('read tour', '/api/canvas/tour', {})).position
86
+ : null;
87
+ await stepTour({ stop: typeof at === 'number' ? at : 0 });
88
+ }
89
+
90
+ export function endPresentation(): void {
91
+ presenting.value = false;
92
+ presentationError.value = '';
93
+ void requestBestEffort('exit tour', '/api/canvas/tour/exit', {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: '{}',
97
+ });
98
+ }
@@ -0,0 +1,81 @@
1
+ import { computed, effect, signal } from '@preact/signals';
2
+ import { relationKind, relationPreset, relationSourceEnd } from '../../shared/relations.js';
3
+ import { activeBoard, activeBoardId } from './boards-store';
4
+ import { activeNodeId, edges, nodes, selectedNodeIds } from './canvas-store';
5
+
6
+ export const relationFilter = signal<{ boardId: string; kind: string } | null>(null);
7
+
8
+ effect(() => {
9
+ const filter = relationFilter.value;
10
+ if (
11
+ filter?.boardId === activeBoardId.value &&
12
+ ![...edges.value.values()].some((edge) => relationKind(edge) === filter.kind)
13
+ ) {
14
+ relationFilter.value = null;
15
+ }
16
+ });
17
+
18
+ // Hover is for hints, not for changing which cards remain readable.
19
+ export const relationFocus = computed(() => activeNodeId.value);
20
+
21
+ /** View-only highlights. They never infer evidence, change a decision or execute a flow. */
22
+ export const relationHighlight = computed(() => {
23
+ const preset = relationPreset(activeBoard()?.relationVocabulary);
24
+ const focus = relationFocus.value;
25
+ const kind = relationFilter.value?.boardId === activeBoardId.value ? relationFilter.value.kind : null;
26
+ const list = [...edges.value.values()].filter((edge) => !edge.toBoardId || edge.toBoardId === activeBoardId.value);
27
+ const focusedCards = new Set(selectedNodeIds.value);
28
+ if (focus) focusedCards.add(focus);
29
+ if (focusedCards.size === 0 && !kind) return null;
30
+ // Selecting a frame focuses its contents too, including nested groups.
31
+ for (const id of focusedCards) {
32
+ const node = nodes.value.get(id);
33
+ if (node?.type === 'group' && Array.isArray(node.data.children))
34
+ for (const child of node.data.children) if (typeof child === 'string') focusedCards.add(child);
35
+ }
36
+ const cards = new Set(focusedCards);
37
+ const links = new Set<string>();
38
+ const add = (edge: (typeof list)[number]) => {
39
+ links.add(edge.id);
40
+ cards.add(edge.from);
41
+ cards.add(edge.to);
42
+ };
43
+ if (!kind) for (const edge of list) if (focusedCards.has(edge.from) && focusedCards.has(edge.to)) add(edge);
44
+ if (kind) {
45
+ for (const edge of list) if (relationKind(edge) === kind) add(edge);
46
+ } else if (preset === 'Dependencies' || preset === 'Decision') {
47
+ let changed = true;
48
+ while (changed) {
49
+ changed = false;
50
+ for (const edge of list) {
51
+ if (links.has(edge.id)) continue;
52
+ const source = relationSourceEnd(edge);
53
+ const follows =
54
+ preset === 'Dependencies'
55
+ ? source && cards.has(source === 'from' ? edge.from : edge.to)
56
+ : ['option', 'if', 'chosen'].includes(relationKind(edge)) && cards.has(edge.to);
57
+ if (follows) {
58
+ add(edge);
59
+ changed = true;
60
+ }
61
+ }
62
+ }
63
+ } else if (preset === 'Flow') {
64
+ const progressed = new Set<string>();
65
+ for (const node of nodes.value.values())
66
+ if (['done', 'in-progress', 'blocked'].includes(String(node.data.axWorkStatus))) {
67
+ progressed.add(node.id);
68
+ cards.add(node.id);
69
+ }
70
+ for (const edge of list)
71
+ if (['flow', 'next', 'if-fails', 'loop'].includes(relationKind(edge)) && progressed.has(edge.from)) add(edge);
72
+ } else {
73
+ for (const edge of list)
74
+ if (
75
+ (focusedCards.has(edge.from) || focusedCards.has(edge.to)) &&
76
+ (preset !== 'Research' || ['supports', 'contradicts', 'cites', 'derived-from'].includes(relationKind(edge)))
77
+ )
78
+ add(edge);
79
+ }
80
+ return { cards, links };
81
+ });