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
@@ -1,9 +1,15 @@
1
- import { useEffect, useRef, useState } from 'preact/hooks';
2
- import { cameraSchema, interpolateCamera, resolveStop, type Camera, type Tour } from '../../shared/tour.js';
1
+ import { useEffect } from 'preact/hooks';
2
+ import { cameraSchema, type Camera } from '../../shared/tour.js';
3
3
  import { canvasArea } from './canvas-area';
4
- import { cancelViewportAnimation, hasInitialServerLayout, nodes, viewport } from '../state/canvas-store';
5
- import { presenting, recordingCamera } from '../state/presentation';
6
- import { requestJson } from '../state/intent-bridge';
4
+ import { cancelViewportAnimation, hasInitialServerLayout, viewport } from '../state/canvas-store';
5
+ import {
6
+ endPresentation,
7
+ presentationError,
8
+ presenting,
9
+ recordingCamera,
10
+ startPresentation,
11
+ stepTour,
12
+ } from '../state/presentation';
7
13
  import { usePanZoom } from './use-pan-zoom';
8
14
 
9
15
  declare global {
@@ -18,14 +24,10 @@ declare global {
18
24
  }
19
25
 
20
26
  export function Presentation() {
21
- const [tour, setTour] = useState<Tour>({ stops: [] });
22
- const [index, setIndex] = useState(0);
23
- const [error, setError] = useState('');
24
27
  const active = presenting.value;
25
28
  const ready = hasInitialServerLayout.value;
26
- const frame = useRef(0);
27
29
  const moveCamera = (camera: Camera) => {
28
- cancelAnimationFrame(frame.current);
30
+ cancelViewportAnimation();
29
31
  viewport.value = camera;
30
32
  };
31
33
  // A separate gesture surface keeps cards read-only and camera changes local.
@@ -58,45 +60,10 @@ export function Presentation() {
58
60
  recordingCamera.value = false;
59
61
  };
60
62
  }, []);
63
+ // `?present=1` joins the tour where it is (an agent may already be driving it).
61
64
  useEffect(() => {
62
- if (!active || !ready) return;
63
- let cancelled = false;
64
- void requestJson('read tour', '/api/canvas/tour', { tour: { stops: [] } as Tour }).then((result) => {
65
- if (!cancelled) {
66
- setTour(result.tour);
67
- setIndex(0);
68
- }
69
- });
70
- return () => {
71
- cancelled = true;
72
- };
73
- }, [active, ready]);
74
- useEffect(() => {
75
- if (!active || recordingCamera.value) return;
76
- const stop = tour.stops[index];
77
- if (!stop) return;
78
- const area = canvasArea();
79
- let target: Camera;
80
- try {
81
- target = resolveStop(stop, [...nodes.value.values()], area.width, area.height);
82
- setError('');
83
- } catch (error) {
84
- setError(error instanceof Error ? error.message : String(error));
85
- return;
86
- }
87
- cancelViewportAnimation();
88
- const from = { ...viewport.value };
89
- const start = performance.now();
90
- const duration = (stop.duration ?? 1) * 1000;
91
- const tick = (now: number) => {
92
- if (recordingCamera.value) return;
93
- const t = duration === 0 ? 1 : Math.min(1, (now - start) / duration);
94
- viewport.value = interpolateCamera(from, target, t, area.width, area.height, stop.easing, stop.pullback);
95
- if (t < 1) frame.current = requestAnimationFrame(tick);
96
- };
97
- frame.current = requestAnimationFrame(tick);
98
- return () => cancelAnimationFrame(frame.current);
99
- }, [active, index, tour]);
65
+ if (ready && presenting.value && !recordingCamera.value) void startPresentation(true);
66
+ }, [ready]);
100
67
  useEffect(() => {
101
68
  if (!active) return;
102
69
  const key = (event: KeyboardEvent) => {
@@ -104,14 +71,13 @@ export function Presentation() {
104
71
  if (event.target instanceof HTMLButtonElement && ['Enter', ' '].includes(event.key)) return;
105
72
  if (['Escape', 'ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp', ' '].includes(event.key))
106
73
  event.preventDefault();
107
- if (event.key === 'Escape') presenting.value = false;
108
- if (['ArrowRight', 'ArrowDown', ' '].includes(event.key))
109
- setIndex((i) => Math.max(0, Math.min(tour.stops.length - 1, i + 1)));
110
- if (['ArrowLeft', 'ArrowUp'].includes(event.key)) setIndex((i) => Math.max(0, i - 1));
74
+ if (event.key === 'Escape') endPresentation();
75
+ if (['ArrowRight', 'ArrowDown', ' '].includes(event.key)) void stepTour({ step: 'next' });
76
+ if (['ArrowLeft', 'ArrowUp'].includes(event.key)) void stepTour({ step: 'previous' });
111
77
  };
112
78
  document.addEventListener('keydown', key, true);
113
79
  return () => document.removeEventListener('keydown', key, true);
114
- }, [active, tour]);
80
+ }, [active]);
115
81
  return active ? (
116
82
  <>
117
83
  {!recordingCamera.value && (
@@ -119,18 +85,18 @@ export function Presentation() {
119
85
  class="presentation-navigation"
120
86
  ref={navigationRef}
121
87
  onPointerDown={(event) => {
122
- if (event.button === 1 || event.button === 2) cancelAnimationFrame(frame.current);
88
+ if (event.button === 1 || event.button === 2) cancelViewportAnimation();
123
89
  }}
124
90
  onContextMenu={(event) => event.preventDefault()}
125
91
  >
126
- <button type="button" class="presentation-exit" onClick={() => (presenting.value = false)}>
92
+ <button type="button" class="presentation-exit" onClick={endPresentation}>
127
93
  Exit presentation
128
94
  </button>
129
95
  </div>
130
96
  )}
131
- {error && (
97
+ {presentationError.value && (
132
98
  <div class="presentation-error" role="alert">
133
- {error}
99
+ {presentationError.value}
134
100
  </div>
135
101
  )}
136
102
  </>
@@ -0,0 +1,413 @@
1
+ import { useEffect, useRef, useState } from 'preact/hooks';
2
+ import type { RelationDefinition, RelationPreset } from '../../shared/relations.js';
3
+ import { boardList, updateBoard } from '../state/boards-store';
4
+ import { requestJson } from '../state/intent-bridge';
5
+ import { knowledge, refreshKnowledge } from '../state/knowledge-store';
6
+ import { useFocusTrap } from './use-focus-trap';
7
+
8
+ /** Shared templates live in Home; applying one never rewrites connection kinds. */
9
+ export function RelationPresetsDialog({ onClose }: { onClose: () => void }) {
10
+ const ref = useRef<HTMLDivElement>(null);
11
+ const presets = knowledge.value?.relationPresets;
12
+ // Until the user chooses, the first saved preset follows catalog availability.
13
+ // Explicit null (new/deleted preset) must not silently select another template.
14
+ const [selection, setSelected] = useState<RelationPreset | null | undefined>();
15
+ const selected = selection === undefined ? (presets?.[0] ?? null) : selection;
16
+ const [editing, setEditing] = useState<RelationPreset | null>(null);
17
+ const [baseline, setBaseline] = useState<RelationPreset | null>(null);
18
+ const [pending, setPending] = useState<(() => void) | null>(null);
19
+ const [boardId, setBoardId] = useState('');
20
+ const [busy, setBusy] = useState(false);
21
+ const [message, setMessage] = useState('');
22
+ const [confirmDelete, setConfirmDelete] = useState(false);
23
+ const guardRef = useRef<HTMLDivElement>(null);
24
+ useEffect(() => {
25
+ if (!pending) return;
26
+ const previous = document.activeElement;
27
+ guardRef.current?.querySelector('button')?.focus();
28
+ return () => {
29
+ if (previous instanceof HTMLElement && previous.isConnected) previous.focus();
30
+ };
31
+ }, [pending]);
32
+ const existing = selected;
33
+ const dirty = editing !== null && JSON.stringify(editing) !== JSON.stringify(baseline);
34
+ const leave = (action: () => void) => {
35
+ if (busy) return;
36
+ if (dirty) setPending(() => action);
37
+ else action();
38
+ };
39
+ const handleEscape = () => {
40
+ if (pending) setPending(null);
41
+ else if (confirmDelete) setConfirmDelete(false);
42
+ else leave(onClose);
43
+ };
44
+ useFocusTrap(ref, true, { onEscape: handleEscape });
45
+ const choose = (preset: RelationPreset | null) => {
46
+ setSelected(preset ? structuredClone(preset) : null);
47
+ setEditing(null);
48
+ setBaseline(null);
49
+ setConfirmDelete(false);
50
+ setMessage('');
51
+ };
52
+ const changeKind = (index: number, change: Partial<RelationDefinition>) => {
53
+ if (editing)
54
+ setEditing({
55
+ ...editing,
56
+ vocabulary: editing.vocabulary.map((kind, i) => (i === index ? { ...kind, ...change } : kind)),
57
+ });
58
+ };
59
+ return (
60
+ <div
61
+ class="knowledge-backdrop"
62
+ onKeyDown={(event) => {
63
+ if (event.key === 'Escape') {
64
+ event.preventDefault();
65
+ event.stopPropagation();
66
+ handleEscape();
67
+ }
68
+ }}
69
+ >
70
+ <div
71
+ ref={ref}
72
+ class="knowledge-dialog is-category-editor relation-presets-dialog"
73
+ role="dialog"
74
+ aria-modal="true"
75
+ aria-label="Workspace relation presets"
76
+ >
77
+ <header>
78
+ <h2>Workspace relation presets</h2>
79
+ <button
80
+ type="button"
81
+ disabled={busy || !!pending}
82
+ aria-label="Close relation presets"
83
+ onClick={() => leave(onClose)}
84
+ >
85
+ ×
86
+ </button>
87
+ </header>
88
+ <p>Saved templates are shared across the workspace. Saving does not apply a preset to any board.</p>
89
+ {message && <p role="status">{message}</p>}
90
+ {pending && (
91
+ <div
92
+ ref={guardRef}
93
+ role="alertdialog"
94
+ aria-label="Discard preset changes"
95
+ aria-modal="true"
96
+ class="relation-dirty-guard"
97
+ >
98
+ <p>Discard unsaved preset changes?</p>
99
+ <button type="button" onClick={() => setPending(null)}>
100
+ Keep editing
101
+ </button>
102
+ <button
103
+ type="button"
104
+ onClick={() => {
105
+ const action = pending;
106
+ setPending(null);
107
+ action();
108
+ }}
109
+ >
110
+ Discard changes
111
+ </button>
112
+ </div>
113
+ )}
114
+ <div inert={pending ? true : undefined}>
115
+ {!presets ? (
116
+ <p>Relation presets unavailable. Close and retry.</p>
117
+ ) : (
118
+ <div class="category-editor-columns">
119
+ <section>
120
+ <h3>{presets.length} presets</h3>
121
+ <button
122
+ type="button"
123
+ disabled={busy}
124
+ onClick={() =>
125
+ leave(() => {
126
+ choose(null);
127
+ const draft = { id: crypto.randomUUID(), name: '', vocabulary: [] };
128
+ setBaseline(structuredClone(draft));
129
+ setEditing(draft);
130
+ })
131
+ }
132
+ >
133
+ Add preset
134
+ </button>
135
+ <div class="category-library" role="group" aria-label="Workspace relation presets">
136
+ {presets.map((preset) => (
137
+ <button
138
+ type="button"
139
+ class="category-choice"
140
+ key={preset.id}
141
+ disabled={busy}
142
+ aria-pressed={(editing?.id ?? selected?.id) === preset.id}
143
+ onClick={() => leave(() => choose(preset))}
144
+ >
145
+ <strong>{preset.name}</strong>
146
+ <span>{preset.vocabulary.map((kind) => kind.kind).join(' · ') || 'No relation kinds'}</span>
147
+ </button>
148
+ ))}
149
+ </div>
150
+ </section>
151
+ <section class="relation-preset-detail">
152
+ {editing ? (
153
+ <form
154
+ class="relation-vocabulary"
155
+ onSubmit={async (event) => {
156
+ event.preventDefault();
157
+ if (busy) return;
158
+ if (
159
+ new Set(editing.vocabulary.map((kind) => kind.kind.trim())).size !== editing.vocabulary.length
160
+ ) {
161
+ setMessage('Give each relation kind a unique name.');
162
+ return;
163
+ }
164
+ setBusy(true);
165
+ const result = await requestJson<{ ok: boolean; preset: RelationPreset } | null>(
166
+ 'relationPresetWrite',
167
+ '/api/canvas/knowledge/relation-preset',
168
+ null,
169
+ {
170
+ method: 'POST',
171
+ headers: { 'Content-Type': 'application/json' },
172
+ body: JSON.stringify({ preset: editing }),
173
+ },
174
+ );
175
+ await refreshKnowledge();
176
+ setBusy(false);
177
+ if (result?.ok) choose(result.preset);
178
+ setMessage(
179
+ result?.ok
180
+ ? 'Preset saved for the workspace. Existing boards are unchanged.'
181
+ : 'Preset was not saved. Retry.',
182
+ );
183
+ }}
184
+ >
185
+ <h3>{selected ? 'Customize shared preset' : 'New shared preset'}</h3>
186
+ <label>
187
+ Name
188
+ <input
189
+ required
190
+ pattern={'.*\\S.*'}
191
+ maxLength={80}
192
+ disabled={busy}
193
+ value={editing.name}
194
+ onInput={(event) => setEditing({ ...editing, name: event.currentTarget.value })}
195
+ />
196
+ </label>
197
+ <p>Kinds stay open. Choose meaning ink and direction, not pin, agent or warning colours.</p>
198
+ {editing.vocabulary.map((kind, index) => (
199
+ <div class="vocabulary-row" key={index}>
200
+ <input
201
+ required
202
+ pattern={'.*\\S.*'}
203
+ aria-label={`Kind ${index + 1}`}
204
+ maxLength={80}
205
+ disabled={busy}
206
+ value={kind.kind}
207
+ onInput={(event) => changeKind(index, { kind: event.currentTarget.value })}
208
+ />
209
+ <select
210
+ aria-label={`Ink ${index + 1}`}
211
+ disabled={busy}
212
+ value={kind.ink}
213
+ onChange={(event) =>
214
+ changeKind(index, { ink: event.currentTarget.value as RelationDefinition['ink'] })
215
+ }
216
+ >
217
+ {['supports', 'contradicts', 'cites', 'derived', 'informs', 'related'].map((ink) => (
218
+ <option key={ink}>{ink}</option>
219
+ ))}
220
+ </select>
221
+ <label>
222
+ <input
223
+ type="checkbox"
224
+ disabled={busy}
225
+ checked={kind.directed}
226
+ onChange={(event) => changeKind(index, { directed: event.currentTarget.checked })}
227
+ />
228
+ Arrow
229
+ </label>
230
+ <button
231
+ type="button"
232
+ aria-label={`Remove kind ${index + 1}`}
233
+ disabled={busy}
234
+ onClick={() =>
235
+ setEditing({ ...editing, vocabulary: editing.vocabulary.filter((_, i) => i !== index) })
236
+ }
237
+ >
238
+ ×
239
+ </button>
240
+ </div>
241
+ ))}
242
+ <button
243
+ type="button"
244
+ disabled={busy || editing.vocabulary.length >= 64}
245
+ onClick={() =>
246
+ setEditing({
247
+ ...editing,
248
+ vocabulary: [...editing.vocabulary, { kind: '', ink: 'related', directed: true }],
249
+ })
250
+ }
251
+ >
252
+ Add kind
253
+ </button>
254
+ <div class="relation-preset-footer">
255
+ <button type="submit" disabled={busy}>
256
+ Save preset
257
+ </button>
258
+ <button type="button" disabled={busy} onClick={() => leave(() => choose(selected))}>
259
+ Cancel customization
260
+ </button>
261
+ </div>
262
+ </form>
263
+ ) : existing ? (
264
+ <>
265
+ <h3>{existing.name}</h3>
266
+ <p>Saved relations · read-only example</p>
267
+ <p>
268
+ Use on board replaces only the chosen board’s vocabulary. Matching connections use its ink and
269
+ direction; connection records are not rewritten.
270
+ </p>
271
+ <ul class="relation-preset-preview" aria-label="Saved relation preview">
272
+ {existing.vocabulary.map((kind) => (
273
+ <li key={kind.kind} style={{ '--relation-ink': `var(--c-rel-${kind.ink})` }}>
274
+ <strong>{kind.kind}</strong>
275
+ <div class="relation-preset-example">
276
+ <span>Example A</span>
277
+ <svg viewBox="0 0 120 24" aria-hidden="true">
278
+ <line
279
+ x1="8"
280
+ y1="12"
281
+ x2="112"
282
+ y2="12"
283
+ stroke="currentColor"
284
+ stroke-width="2"
285
+ stroke-dasharray={kind.ink === 'related' ? '3 5' : undefined}
286
+ />
287
+ {kind.directed && (
288
+ <path
289
+ d="M 102 6 L 112 12 L 102 18"
290
+ fill="none"
291
+ stroke="currentColor"
292
+ stroke-width="2"
293
+ />
294
+ )}
295
+ </svg>
296
+ <span>Example B</span>
297
+ </div>
298
+ <span>
299
+ {kind.ink} · {kind.directed ? 'Directed: A → B' : 'Undirected: A — B'}
300
+ </span>
301
+ </li>
302
+ ))}
303
+ </ul>
304
+ {!existing.vocabulary.length && (
305
+ <p>No relation kinds. This empty preset can still be used on a board.</p>
306
+ )}
307
+ <div class="relation-preset-footer">
308
+ {existing && (
309
+ <>
310
+ <label>
311
+ Apply saved preset to board
312
+ <select
313
+ aria-label="Apply saved preset to board"
314
+ disabled={busy}
315
+ value={boardId}
316
+ onChange={(event) => setBoardId(event.currentTarget.value)}
317
+ >
318
+ <option value="">Choose a board</option>
319
+ {boardList.value.map((board) => (
320
+ <option key={board.id} value={board.id}>
321
+ {board.name}
322
+ </option>
323
+ ))}
324
+ </select>
325
+ </label>
326
+ <button
327
+ type="button"
328
+ class="relation-preset-use"
329
+ disabled={busy || !boardId}
330
+ onClick={async () => {
331
+ if (busy || !boardId) return;
332
+ setBusy(true);
333
+ const saved = await updateBoard(boardId, { relationVocabulary: existing.vocabulary });
334
+ setBusy(false);
335
+ setMessage(
336
+ saved
337
+ ? 'Preset applied. Connection records and other boards are unchanged.'
338
+ : 'Preset was not applied. Retry.',
339
+ );
340
+ }}
341
+ >
342
+ Use on board
343
+ </button>
344
+ <button
345
+ type="button"
346
+ disabled={busy}
347
+ onClick={() => {
348
+ setBaseline(structuredClone(existing));
349
+ setEditing(structuredClone(existing));
350
+ setConfirmDelete(false);
351
+ setMessage('');
352
+ }}
353
+ >
354
+ Customize preset
355
+ </button>
356
+ {confirmDelete ? (
357
+ <div role="group" aria-label="Confirm preset deletion">
358
+ <p>Delete {existing.name}? Boards that used it keep their existing vocabulary.</p>
359
+ <button type="button" disabled={busy} onClick={() => setConfirmDelete(false)}>
360
+ Cancel deletion
361
+ </button>
362
+ <button
363
+ type="button"
364
+ class="is-danger"
365
+ disabled={busy}
366
+ onClick={async () => {
367
+ if (busy) return;
368
+ setBusy(true);
369
+ const result = await requestJson<{ ok: boolean } | null>(
370
+ 'relationPresetRemove',
371
+ `/api/canvas/knowledge/relation-preset/${encodeURIComponent(existing.id)}`,
372
+ null,
373
+ { method: 'DELETE' },
374
+ );
375
+ await refreshKnowledge();
376
+ setBusy(false);
377
+ setConfirmDelete(false);
378
+ if (result?.ok) choose(null);
379
+ setMessage(
380
+ result?.ok
381
+ ? 'Preset deleted. Existing boards are unchanged.'
382
+ : 'Preset was not deleted. Retry.',
383
+ );
384
+ }}
385
+ >
386
+ Confirm deletion
387
+ </button>
388
+ </div>
389
+ ) : (
390
+ <button
391
+ type="button"
392
+ class="is-danger"
393
+ disabled={busy}
394
+ onClick={() => setConfirmDelete(true)}
395
+ >
396
+ Delete preset
397
+ </button>
398
+ )}
399
+ </>
400
+ )}
401
+ </div>
402
+ </>
403
+ ) : (
404
+ <p>Choose a saved preset or add a new one.</p>
405
+ )}
406
+ </section>
407
+ </div>
408
+ )}
409
+ </div>
410
+ </div>
411
+ </div>
412
+ );
413
+ }
@@ -0,0 +1,82 @@
1
+ import { useEffect, useState } from 'preact/hooks';
2
+ import { memoryKey, type MemoryGraph } from '../../shared/memory-graph.js';
3
+ import { relationDefinition, relationSourceEnd } from '../../shared/relations.js';
4
+ import { activeBoardId, boardList, openMemoryTarget } from '../state/boards-store';
5
+ import { edges, nodes } from '../state/canvas-store';
6
+ import { requestJson } from '../state/intent-bridge';
7
+
8
+ /** The expanded card's evidence and uses, from the same graph agents navigate. */
9
+ export function RelationsPanel({ nodeId }: { nodeId: string }) {
10
+ const boardId = activeBoardId.value;
11
+ const [graph, setGraph] = useState<MemoryGraph | null>(null);
12
+ const [unavailable, setUnavailable] = useState(false);
13
+ const currentEdges = edges.value;
14
+ const currentNodes = nodes.value;
15
+ const catalog = boardList.value;
16
+ useEffect(() => {
17
+ let cancelled = false;
18
+ setGraph(null);
19
+ setUnavailable(false);
20
+ if (boardId)
21
+ void requestJson<MemoryGraph | null>(
22
+ 'relations',
23
+ `/api/canvas/memory-graph?focus=${encodeURIComponent(memoryKey({ boardId, cardId: nodeId }))}&depth=1`,
24
+ null,
25
+ ).then((result) => {
26
+ if (!cancelled) {
27
+ setGraph(result);
28
+ setUnavailable(!result);
29
+ }
30
+ });
31
+ return () => {
32
+ cancelled = true;
33
+ };
34
+ }, [boardId, nodeId, currentEdges, currentNodes, catalog]);
35
+ const key = boardId ? memoryKey({ boardId, cardId: nodeId }) : '';
36
+ const relations =
37
+ graph?.edges.filter((edge) => edge.kind !== 'member' && (edge.from === key || edge.to === key)) ?? [];
38
+ const group = (edge: (typeof relations)[number]) => {
39
+ const incoming = edge.to === key;
40
+ if (incoming && edge.relationKind === 'supports') return 'Evidence for';
41
+ if (incoming && edge.relationKind === 'contradicts') return 'Evidence against';
42
+ const source = relationSourceEnd({ kind: edge.relationKind, type: edge.type });
43
+ if (source) return (source === 'from' ? !incoming : incoming) ? 'Used by' : 'Sources';
44
+ if (edge.relationKind === 'cites' || edge.type === 'references') return incoming ? 'Used by' : 'Sources';
45
+ if (!incoming && edge.relationKind === 'supports') return 'Used by';
46
+ return 'Connections';
47
+ };
48
+ const groups = ['Evidence for', 'Evidence against', 'Sources', 'Used by', 'Connections'];
49
+ relations.sort((a, b) => groups.indexOf(group(a)) - groups.indexOf(group(b)));
50
+ return (
51
+ <aside class="relations-panel" aria-label="Relations">
52
+ <h3>Relations</h3>
53
+ {!graph && <p role="status">{unavailable ? 'Relations unavailable. Reopen to retry.' : 'Loading relations…'}</p>}
54
+ {graph && relations.length === 0 && <p>No connections yet.</p>}
55
+ {relations.map((edge, index) => {
56
+ const outgoing = edge.from === key;
57
+ const target = outgoing ? edge.target : edge.source;
58
+ const other = graph?.nodes.find((item) => item.key === memoryKey(target));
59
+ const owner = catalog.find((board) => board.id === edge.source.boardId);
60
+ const definition = relationDefinition({ kind: edge.relationKind, type: edge.type }, owner?.relationVocabulary);
61
+ return (
62
+ <div class="relation-row" key={edge.id}>
63
+ {(index === 0 || group(relations[index - 1]) !== group(edge)) && <h4>{group(edge)}</h4>}
64
+ <span class="relation-kind" style={{ color: `var(--c-rel-${definition.ink})` }}>
65
+ {outgoing ? '→' : '←'} {edge.relationKind ?? edge.type ?? edge.kind}
66
+ </span>
67
+ {edge.missing ? (
68
+ <span>Missing target</span>
69
+ ) : (
70
+ <button type="button" class="relation-target" onClick={() => void openMemoryTarget(target)}>
71
+ {other?.title ?? target.boardId}
72
+ </button>
73
+ )}
74
+ {edge.label && <span>{edge.label}</span>}
75
+ <p>{edge.reason ?? 'No reason recorded.'}</p>
76
+ </div>
77
+ );
78
+ })}
79
+ <p>Manage shared relation presets in Home.</p>
80
+ </aside>
81
+ );
82
+ }
@@ -1,6 +1,5 @@
1
1
  import { useCallback } from 'preact/hooks';
2
2
  import { IconArrange, IconClose } from '../icons';
3
- import { attentionHistoryOpen } from '../state/attention-store';
4
3
  import {
5
4
  addContextPins,
6
5
  alignSelection,
@@ -13,6 +12,7 @@ import {
13
12
  import { createEdgeFromClient, createGroupFromClient, removeNodeFromClient } from '../state/intent-bridge';
14
13
  import { sessionActive } from '../state/presence-store';
15
14
  import { BarHint } from './BarHint';
15
+ import { useChromeHeight } from './use-chrome-height';
16
16
 
17
17
  /**
18
18
  * Floating bottom-center bar for a multi-selection (rail-chrome-v2 phase 7,
@@ -22,6 +22,7 @@ import { BarHint } from './BarHint';
22
22
  */
23
23
  export function SelectionBar() {
24
24
  const count = selectedNodeIds.value.size;
25
+ const barRef = useChromeHeight('--selection-bar-height');
25
26
 
26
27
  const handlePinContext = useCallback(() => {
27
28
  const ids = Array.from(selectedNodeIds.value);
@@ -57,12 +58,8 @@ export function SelectionBar() {
57
58
 
58
59
  if (count === 0) return null;
59
60
  const many = count >= 2;
60
- // The quiet board's pin bar shares the bottom-center slot (same rule as
61
- // `ContextPinBar`'s own visibility) — sit above it rather than under it.
62
- const abovePinBar = !sessionActive.value && contextPinnedNodeIds.value.size > 0 && !attentionHistoryOpen.value;
63
-
64
61
  return (
65
- <div class={`selection-bar${abovePinBar ? ' is-above-pin-bar' : ''}`} role="toolbar" aria-label="Selection">
62
+ <div class="selection-bar" role="toolbar" aria-label="Selection" ref={barRef}>
66
63
  <span class="selection-bar-count">
67
64
  {count} node{count !== 1 ? 's' : ''} selected
68
65
  </span>