pmx-canvas 0.8.0 → 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 (341) 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 +66 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2759 -510
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2936 -632
  8. package/dist/json-render/index.css +81 -1
  9. package/dist/json-render/index.js +20 -20
  10. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  11. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  12. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  13. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  15. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  17. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  18. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  19. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  20. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  21. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  22. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  23. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  24. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  25. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  26. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  27. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  28. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  29. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  30. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  31. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  32. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  33. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  34. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  35. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  36. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  37. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  38. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  39. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  40. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  41. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  42. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  43. package/dist/types/client/icons.d.ts +0 -10
  44. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  45. package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
  46. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  47. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  48. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  49. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  50. package/dist/types/client/state/boards-store.d.ts +81 -9
  51. package/dist/types/client/state/canvas-store.d.ts +14 -1
  52. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  53. package/dist/types/client/state/context-status-store.d.ts +35 -0
  54. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  55. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  56. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  57. package/dist/types/client/state/presence-store.d.ts +4 -4
  58. package/dist/types/client/state/presentation.d.ts +28 -0
  59. package/dist/types/client/state/relation-store.d.ts +10 -0
  60. package/dist/types/client/state/session-store.d.ts +41 -2
  61. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  62. package/dist/types/client/types.d.ts +13 -1
  63. package/dist/types/client/utils/keyboard.d.ts +2 -0
  64. package/dist/types/client/utils/time-ago.d.ts +4 -0
  65. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  66. package/dist/types/server/agent-presence.d.ts +10 -3
  67. package/dist/types/server/ax-context.d.ts +1 -1
  68. package/dist/types/server/canvas-db.d.ts +30 -7
  69. package/dist/types/server/canvas-operations.d.ts +24 -2
  70. package/dist/types/server/canvas-provenance.d.ts +22 -2
  71. package/dist/types/server/canvas-serialization.d.ts +2 -0
  72. package/dist/types/server/canvas-state.d.ts +93 -15
  73. package/dist/types/server/canvas-validation.d.ts +1 -1
  74. package/dist/types/server/context-brief.d.ts +94 -6
  75. package/dist/types/server/context-reads.d.ts +33 -7
  76. package/dist/types/server/edit-summary.d.ts +20 -0
  77. package/dist/types/server/index.d.ts +71 -10
  78. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  79. package/dist/types/server/lifecycle-status.d.ts +10 -0
  80. package/dist/types/server/memory-graph.d.ts +9 -0
  81. package/dist/types/server/mutation-history.d.ts +2 -0
  82. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  83. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  84. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  85. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  86. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  87. package/dist/types/server/operations/ops/query.d.ts +23 -0
  88. package/dist/types/server/operations/registry.d.ts +1 -0
  89. package/dist/types/server/placement.d.ts +7 -0
  90. package/dist/types/server/scope-fence.d.ts +2 -1
  91. package/dist/types/server/spatial-analysis.d.ts +6 -3
  92. package/dist/types/server/tour-control.d.ts +32 -0
  93. package/dist/types/server/wiki-links.d.ts +14 -0
  94. package/dist/types/server/workbench-auth.d.ts +2 -0
  95. package/dist/types/shared/agent-context.d.ts +2 -0
  96. package/dist/types/shared/agent-presence.d.ts +15 -12
  97. package/dist/types/shared/auto-arrange.d.ts +1 -0
  98. package/dist/types/shared/boards.d.ts +4 -4
  99. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  100. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  101. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  102. package/dist/types/shared/memory-graph.d.ts +53 -0
  103. package/dist/types/shared/relations.d.ts +68 -0
  104. package/dist/types/shared/semantic-attention.d.ts +2 -0
  105. package/dist/types/shared/tour.d.ts +38 -0
  106. package/dist/types/shared/workspace-settings.d.ts +11 -0
  107. package/docs/RELEASE.md +21 -3
  108. package/docs/adr-001-bun-only-runtime.md +7 -1
  109. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  110. package/docs/api-reference.md +109 -0
  111. package/docs/ax-host-adapter-contract.md +1 -1
  112. package/docs/ax-state-contract.md +5 -4
  113. package/docs/cli.md +153 -15
  114. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  115. package/docs/design/AgentContext.dc.html +3 -3
  116. package/docs/design/BoardPins.dc.html +37 -22
  117. package/docs/design/Categories.dc.html +208 -0
  118. package/docs/design/CategoryEditor.dc.html +33 -0
  119. package/docs/design/Chrome.dc.html +55 -9
  120. package/docs/design/ContextChip.dc.html +224 -0
  121. package/docs/design/ContextChip600.dc.html +136 -0
  122. package/docs/design/EmptyState.dc.html +45 -0
  123. package/docs/design/Flows.dc.html +23 -1
  124. package/docs/design/HeaderMarks.dc.html +183 -0
  125. package/docs/design/Home.dc.html +19 -1
  126. package/docs/design/HomeGraph.dc.html +86 -202
  127. package/docs/design/HomeMap.dc.html +23 -1
  128. package/docs/design/HomeTeam.dc.html +32 -14
  129. package/docs/design/Lifecycle.dc.html +22 -0
  130. package/docs/design/Lifecycle600.dc.html +18 -0
  131. package/docs/design/LifecycleVariants.dc.html +157 -0
  132. package/docs/design/LinkAuthorship.dc.html +139 -0
  133. package/docs/design/LinksChanged.dc.html +203 -0
  134. package/docs/design/LinksOptions.dc.html +137 -0
  135. package/docs/design/LinksOptions600.dc.html +134 -0
  136. package/docs/design/NearPin.dc.html +9 -10
  137. package/docs/design/Pane600.dc.html +4 -3
  138. package/docs/design/Relations.dc.html +3 -2
  139. package/docs/design/Restore.dc.html +152 -0
  140. package/docs/design/SeeChange.dc.html +267 -0
  141. package/docs/design/Uses.dc.html +12 -6
  142. package/docs/design/canvas.json +146 -19
  143. package/docs/design.md +454 -18
  144. package/docs/environment.md +0 -1
  145. package/docs/evals/curation-effect.md +128 -0
  146. package/docs/http-api.md +465 -38
  147. package/docs/mcp.md +150 -19
  148. package/docs/node-types.md +58 -3
  149. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  150. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  151. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  152. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  153. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  154. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  155. package/docs/product-context-vision-2026-09.md +17 -1
  156. package/docs/product-vision-2026-09.md +1082 -43
  157. package/docs/product-vision-review-2026-09.md +13 -1
  158. package/docs/screenshots/workbench-live-dark.png +0 -0
  159. package/docs/screenshots/workbench-live-light.png +0 -0
  160. package/docs/sdk.md +62 -2
  161. package/package.json +1 -1
  162. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/plugin.json +1 -1
  164. package/plugins/pmx-canvas/runtime.json +1 -1
  165. package/plugins/pmx-canvas/tools.json +601 -22
  166. package/skills/pmx-canvas/SKILL.md +234 -43
  167. package/skills/pmx-canvas/evals/evals.json +62 -0
  168. package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
  169. package/skills/pmx-canvas/references/full-reference.md +120 -21
  170. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  171. package/skills/pmx-canvas/references/importing-context.md +132 -0
  172. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  173. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  174. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  175. package/src/cli/agent.ts +2 -0
  176. package/src/cli/commands/ax.ts +12 -0
  177. package/src/cli/commands/boards.ts +87 -11
  178. package/src/cli/commands/edges.ts +28 -0
  179. package/src/cli/commands/groups.ts +7 -0
  180. package/src/cli/commands/nodes.ts +4 -2
  181. package/src/cli/commands/query.ts +34 -2
  182. package/src/cli/commands/record.ts +199 -48
  183. package/src/cli/index.ts +7 -1
  184. package/src/client/App.tsx +177 -70
  185. package/src/client/canvas/AttentionHistory.tsx +0 -2
  186. package/src/client/canvas/BarHint.tsx +40 -2
  187. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  188. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  189. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  190. package/src/client/canvas/BoardPinButton.tsx +33 -0
  191. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  192. package/src/client/canvas/CanvasNode.tsx +360 -237
  193. package/src/client/canvas/CanvasViewport.tsx +45 -7
  194. package/src/client/canvas/CommandBar.tsx +29 -23
  195. package/src/client/canvas/CommandPalette.tsx +5 -12
  196. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  197. package/src/client/canvas/ContextChip.tsx +296 -0
  198. package/src/client/canvas/ContextMenu.tsx +63 -5
  199. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  200. package/src/client/canvas/EdgeLayer.tsx +339 -76
  201. package/src/client/canvas/EmptyState.tsx +33 -18
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  203. package/src/client/canvas/ExternalWriters.tsx +57 -42
  204. package/src/client/canvas/HomeGraphView.tsx +942 -0
  205. package/src/client/canvas/HomeMapView.tsx +641 -0
  206. package/src/client/canvas/HomeReviewView.tsx +167 -0
  207. package/src/client/canvas/HomeView.tsx +687 -160
  208. package/src/client/canvas/IntentLayer.tsx +19 -12
  209. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  210. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  211. package/src/client/canvas/LifecycleFields.tsx +91 -0
  212. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  213. package/src/client/canvas/MemoryInspector.tsx +104 -0
  214. package/src/client/canvas/Minimap.tsx +3 -3
  215. package/src/client/canvas/NearPinLayer.tsx +187 -0
  216. package/src/client/canvas/NodeContextMark.tsx +175 -97
  217. package/src/client/canvas/Presentation.tsx +23 -57
  218. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  219. package/src/client/canvas/RelationsPanel.tsx +82 -0
  220. package/src/client/canvas/SelectionBar.tsx +3 -6
  221. package/src/client/canvas/SessionPanel.tsx +90 -50
  222. package/src/client/canvas/SessionReceipt.tsx +311 -39
  223. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  224. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  225. package/src/client/canvas/ToolRail.tsx +367 -192
  226. package/src/client/canvas/TopBar.tsx +354 -124
  227. package/src/client/canvas/canvas-area.ts +10 -3
  228. package/src/client/canvas/graph-geometry.ts +195 -0
  229. package/src/client/canvas/graph-model.ts +254 -0
  230. package/src/client/canvas/memory-layout.ts +106 -0
  231. package/src/client/canvas/use-chrome-height.ts +20 -0
  232. package/src/client/canvas/use-focus-trap.ts +31 -5
  233. package/src/client/canvas/use-memory-graph.ts +58 -0
  234. package/src/client/canvas/use-node-drag.ts +2 -1
  235. package/src/client/canvas/use-pan-zoom.ts +64 -7
  236. package/src/client/icons.tsx +0 -63
  237. package/src/client/mcp-app/style.css +0 -6
  238. package/src/client/nodes/AxStepControls.tsx +74 -50
  239. package/src/client/nodes/BoardNode.tsx +5 -1
  240. package/src/client/nodes/ContextNode.tsx +18 -11
  241. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  242. package/src/client/nodes/FileNode.tsx +46 -42
  243. package/src/client/nodes/HtmlNode.tsx +44 -2
  244. package/src/client/nodes/ImageNode.tsx +1 -1
  245. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  246. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  247. package/src/client/nodes/MarkdownNode.tsx +377 -109
  248. package/src/client/nodes/McpAppNode.tsx +50 -20
  249. package/src/client/nodes/MdFormatBar.tsx +21 -18
  250. package/src/client/nodes/PromptNode.tsx +1 -1
  251. package/src/client/nodes/ResponseNode.tsx +1 -1
  252. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  253. package/src/client/state/attention-bridge.ts +64 -15
  254. package/src/client/state/boards-store.ts +193 -39
  255. package/src/client/state/canvas-store.ts +99 -12
  256. package/src/client/state/context-chip-store.ts +70 -0
  257. package/src/client/state/context-status-store.ts +53 -0
  258. package/src/client/state/intent-bridge.ts +51 -30
  259. package/src/client/state/knowledge-store.ts +43 -0
  260. package/src/client/state/near-pin-store.ts +115 -6
  261. package/src/client/state/presence-store.ts +7 -10
  262. package/src/client/state/presentation.ts +91 -0
  263. package/src/client/state/relation-store.ts +81 -0
  264. package/src/client/state/session-store.ts +144 -18
  265. package/src/client/state/sse-bridge.ts +206 -29
  266. package/src/client/state/workspace-settings.ts +11 -0
  267. package/src/client/theme/global.css +2759 -510
  268. package/src/client/types.ts +12 -0
  269. package/src/client/utils/keyboard.ts +10 -0
  270. package/src/client/utils/time-ago.ts +14 -0
  271. package/src/json-render/renderer/index.css +6 -1
  272. package/src/json-render/renderer/index.tsx +31 -1
  273. package/src/mcp/canvas-app.ts +12 -2
  274. package/src/mcp/context-uri-template.ts +7 -4
  275. package/src/mcp/server.ts +63 -41
  276. package/src/mcp/workbench-app.ts +39 -6
  277. package/src/server/agent-presence.ts +128 -28
  278. package/src/server/ax-context.ts +27 -13
  279. package/src/server/board-export.ts +12 -1
  280. package/src/server/canvas-db.ts +331 -42
  281. package/src/server/canvas-operations.ts +196 -84
  282. package/src/server/canvas-provenance.ts +53 -22
  283. package/src/server/canvas-serialization.ts +2 -0
  284. package/src/server/canvas-state.ts +950 -122
  285. package/src/server/canvas-validation.ts +9 -2
  286. package/src/server/context-brief.ts +345 -38
  287. package/src/server/context-reads.ts +179 -30
  288. package/src/server/demo-state.json +355 -24
  289. package/src/server/edit-summary.ts +74 -0
  290. package/src/server/html-surface.ts +8 -1
  291. package/src/server/index.ts +203 -13
  292. package/src/server/knowledge-lifecycle.ts +304 -0
  293. package/src/server/lifecycle-status.ts +232 -0
  294. package/src/server/memory-graph.ts +166 -0
  295. package/src/server/mutation-history.ts +18 -6
  296. package/src/server/operations/composites.ts +32 -11
  297. package/src/server/operations/http.ts +7 -2
  298. package/src/server/operations/index.ts +4 -0
  299. package/src/server/operations/mcp.ts +32 -1
  300. package/src/server/operations/ops/ax-presence.ts +1 -1
  301. package/src/server/operations/ops/ax-read.ts +57 -6
  302. package/src/server/operations/ops/ax-timeline.ts +28 -2
  303. package/src/server/operations/ops/backup.ts +5 -3
  304. package/src/server/operations/ops/batch.ts +35 -4
  305. package/src/server/operations/ops/boards.ts +188 -47
  306. package/src/server/operations/ops/canvas-wire.ts +31 -1
  307. package/src/server/operations/ops/edges.ts +36 -2
  308. package/src/server/operations/ops/groups.ts +6 -1
  309. package/src/server/operations/ops/json-render.ts +5 -1
  310. package/src/server/operations/ops/lifecycle.ts +262 -0
  311. package/src/server/operations/ops/memory-graph.ts +45 -0
  312. package/src/server/operations/ops/nodes.ts +107 -24
  313. package/src/server/operations/ops/query.ts +281 -68
  314. package/src/server/operations/ops/snapshots.ts +73 -1
  315. package/src/server/operations/ops/validate.ts +5 -1
  316. package/src/server/operations/ops/viewport.ts +97 -3
  317. package/src/server/operations/registry.ts +77 -26
  318. package/src/server/placement.ts +29 -0
  319. package/src/server/scope-fence.ts +78 -6
  320. package/src/server/server.ts +115 -16
  321. package/src/server/spatial-analysis.ts +48 -23
  322. package/src/server/tour-control.ts +82 -0
  323. package/src/server/wiki-links.ts +137 -0
  324. package/src/server/workbench-auth.ts +11 -0
  325. package/src/shared/agent-context.ts +3 -0
  326. package/src/shared/agent-presence.ts +15 -15
  327. package/src/shared/auto-arrange.ts +2 -0
  328. package/src/shared/boards.ts +4 -4
  329. package/src/shared/knowledge-lifecycle.ts +22 -0
  330. package/src/shared/knowledge-presets.ts +80 -0
  331. package/src/shared/lifecycle-status.ts +26 -0
  332. package/src/shared/memory-graph.ts +74 -0
  333. package/src/shared/relations.ts +98 -0
  334. package/src/shared/semantic-attention.ts +15 -1
  335. package/src/shared/tour.ts +23 -5
  336. package/src/shared/workspace-settings.ts +13 -0
  337. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  338. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  339. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  340. package/dist/types/server/agent-context.d.ts +0 -22
  341. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -1,10 +1,12 @@
1
1
  import { exportDialogOpen } from './ExportDialog';
2
- import { activeBoardId } from '../state/boards-store';
2
+ import { activeBoard, activeBoardId, browsingMemory } from '../state/boards-store';
3
3
  import { BoardSwitcher } from './BoardSwitcher';
4
+ import { BoardPinButton } from './BoardPinButton';
5
+ import { ContextChip } from './ContextChip';
4
6
  import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
5
7
  import type { ComponentChildren } from 'preact';
6
- import { presenting } from '../state/presentation';
7
- import { useEffect, useState } from 'preact/hooks';
8
+ import { startPresentation } from '../state/presentation';
9
+ import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
8
10
  import { IconFitAll, IconZoomIn, IconZoomOut, IconClose } from '../icons';
9
11
  import {
10
12
  animateViewport,
@@ -20,14 +22,22 @@ import {
20
22
  } from '../state/canvas-store';
21
23
  import { modChord } from '../utils/platform';
22
24
  import { canvasArea } from './canvas-area';
23
- import { BarHint } from './BarHint';
25
+ import { BarHint, BarPortal } from './BarHint';
24
26
  import { degradedState } from './ConnectionBanner';
25
27
  import { ExternalWriterIndicator } from './ExternalWriters';
26
28
  import { useNow } from './use-now';
27
- import { agentPhaseLabel } from '../../shared/agent-presence.js';
28
- import { activeSession, agentPresences, attachedSessions, contextBudget, writerColor } from '../state/presence-store';
29
- import { endSession, pendingGates, startSession } from '../state/session-store';
29
+ import { type AgentPresence, agentPhaseLabel } from '../../shared/agent-presence.js';
30
+ import { activeSession, agentPresences, attachedSessions, writerColor, writerInitial } from '../state/presence-store';
31
+ import {
32
+ activityLens,
33
+ endSession,
34
+ pendingGates,
35
+ sessionPanelOpened,
36
+ setActivityLens,
37
+ startSession,
38
+ } from '../state/session-store';
30
39
  import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
40
+ import { agentSessionsEnabled } from '../state/workspace-settings';
31
41
 
32
42
  /**
33
43
  * The attached session's chip (rail-chrome-v2 phase 3): phase-colored, with a
@@ -39,6 +49,31 @@ const AGENT_CHIP_LIMIT = 3;
39
49
  const AGENT_PHASE_RANK: Record<string, number> = { 'waiting-approval': 0, thinking: 1, tooling: 2, idle: 3 };
40
50
 
41
51
  function AgentChip() {
52
+ const [opened, setOpened] = useState<{ id: string; button: HTMLButtonElement } | null>(null);
53
+ const popover = useRef<HTMLDivElement>(null);
54
+ useLayoutEffect(() => {
55
+ if (!opened) return;
56
+ const close = (event: PointerEvent) => {
57
+ if (!popover.current?.contains(event.target as Node) && !opened.button.contains(event.target as Node))
58
+ setOpened(null);
59
+ };
60
+ const onKey = (event: KeyboardEvent) => {
61
+ if (event.key === 'Escape' && !event.defaultPrevented) {
62
+ event.preventDefault();
63
+ setOpened(null);
64
+ opened.button.focus({ preventScroll: true });
65
+ }
66
+ };
67
+ const onResize = () => setOpened(null);
68
+ document.addEventListener('pointerdown', close, true);
69
+ document.addEventListener('keydown', onKey);
70
+ window.addEventListener('resize', onResize);
71
+ return () => {
72
+ document.removeEventListener('pointerdown', close, true);
73
+ document.removeEventListener('keydown', onKey);
74
+ window.removeEventListener('resize', onResize);
75
+ };
76
+ }, [opened]);
42
77
  // Every attached session gets its chip — with several agents on the board,
43
78
  // showing only the first hid the rest from the top bar entirely. But one
44
79
  // chip PER agent floods the bar on a many-agent board (user feedback,
@@ -47,7 +82,10 @@ function AgentChip() {
47
82
  // active few wear their own chips and the rest fold into a census chip
48
83
  // that always names the total.
49
84
  const allSessions = attachedSessions.value;
50
- if (allSessions.length === 0) return null;
85
+ useEffect(() => {
86
+ if (opened && !opened.button.isConnected) setOpened(null);
87
+ }, [allSessions, opened]);
88
+ if (!agentSessionsEnabled.value || allSessions.length === 0) return null;
51
89
  // Attached workers are represented by their orchestrator's roll-up, not a
52
90
  // second top-level chip of their own. Orphans remain visible.
53
91
  const sessions = allSessions.filter(
@@ -106,9 +144,26 @@ function AgentChip() {
106
144
  data-phase={session.phase}
107
145
  style={{ '--identity-color': writerColor(session.sessionId) }}
108
146
  >
147
+ <button
148
+ type="button"
149
+ class="agent-chip-initial"
150
+ aria-label={`${session.label} session details`}
151
+ aria-expanded={opened?.id === session.sessionId}
152
+ aria-haspopup="dialog"
153
+ onClick={(event) => {
154
+ event.stopPropagation();
155
+ setOpened(
156
+ opened?.id === session.sessionId ? null : { id: session.sessionId, button: event.currentTarget },
157
+ );
158
+ }}
159
+ >
160
+ {writerInitial(session.label)}
161
+ <span class="agent-chip-dot" aria-hidden="true" />
162
+ </button>
109
163
  <span class="agent-chip-dot" aria-hidden="true" />
110
164
  <span class="agent-chip-label">{agentPhaseLabel(session)}</span>
111
165
  <span class="agent-chip-who hud-collapsible-text">{session.label}</span>
166
+ <SessionTouches session={session} />
112
167
  {workers > 0 && (
113
168
  <span class="agent-chip-workers">
114
169
  +{workers} worker{workers === 1 ? '' : 's'}
@@ -118,7 +173,6 @@ function AgentChip() {
118
173
  type="button"
119
174
  class="agent-chip-end"
120
175
  aria-label={`End ${session.label} session`}
121
- title={`End ${session.label}'s session`}
122
176
  onClick={(e) => {
123
177
  // The chip sits inside a tap-to-open hint — ending a session
124
178
  // must not also open it.
@@ -144,17 +198,109 @@ function AgentChip() {
144
198
  </span>
145
199
  </BarHint>
146
200
  )}
201
+ {opened &&
202
+ visible
203
+ .filter((session) => session.sessionId === opened.id)
204
+ .map((session) => (
205
+ <BarPortal key={session.sessionId}>
206
+ <div
207
+ ref={popover}
208
+ class={`agent-session-popover phase-${session.phase}`}
209
+ role="dialog"
210
+ aria-label={`${session.label} session details`}
211
+ style={{
212
+ right: `${Math.max(12, window.innerWidth - opened.button.getBoundingClientRect().right)}px`,
213
+ top: `${opened.button.getBoundingClientRect().bottom + 8}px`,
214
+ }}
215
+ >
216
+ <div class="agent-session-popover-head">
217
+ <span class="agent-chip-dot" aria-hidden="true" />
218
+ <strong>
219
+ {session.label} · {agentPhaseLabel(session)}
220
+ </strong>
221
+ </div>
222
+ <SessionTouches session={session} popover />
223
+ <button
224
+ type="button"
225
+ class="session-receipt-link"
226
+ onClick={() => {
227
+ sessionPanelOpened.value = true;
228
+ setOpened(null);
229
+ }}
230
+ >
231
+ Open session panel
232
+ </button>
233
+ </div>
234
+ </BarPortal>
235
+ ))}
147
236
  </>
148
237
  );
149
238
  }
150
239
 
240
+ /**
241
+ * What the session has done so far (AgentContext.dc.html lens, Pane600
242
+ * "Claude · 4 read · 1 edited"): the counts, and a press dims every card it
243
+ * has not touched. Hidden until it has touched something.
244
+ */
245
+ function SessionTouches({ session, popover = false }: { session: AgentPresence; popover?: boolean }) {
246
+ const touches = session.session;
247
+ const parts = touches
248
+ ? (
249
+ [
250
+ ['read', touches.read.length],
251
+ ['created', touches.created.length],
252
+ ['edited', touches.edited.length],
253
+ ['pinned', touches.pinned.length],
254
+ ] as const
255
+ ).filter(([, count]) => count > 0)
256
+ : [];
257
+ const lens = activityLens.value;
258
+ const on = lens?.kind === 'live' && lens.sessionId === session.sessionId;
259
+ // While its lens is on the button stays, even at zero, so it can be turned off.
260
+ if (parts.length === 0 && !on && !popover) return null;
261
+ const counts = parts.map(([word, count]) => `${count} ${word}`).join(' · ') || 'nothing yet';
262
+ if (popover)
263
+ return (
264
+ <>
265
+ <span class="agent-session-counts">{counts}</span>
266
+ <button
267
+ type="button"
268
+ role="switch"
269
+ aria-checked={on}
270
+ class={`session-receipt-lens${on ? ' is-on' : ''}`}
271
+ onClick={() => setActivityLens(!on, { sessionId: session.sessionId })}
272
+ >
273
+ <span class="session-receipt-lens-track" aria-hidden="true">
274
+ <span class="session-receipt-lens-knob" />
275
+ </span>
276
+ Dim untouched nodes
277
+ </button>
278
+ </>
279
+ );
280
+ return (
281
+ <button
282
+ type="button"
283
+ class={`agent-chip-touches${on ? ' is-on' : ''}`}
284
+ aria-pressed={on}
285
+ aria-label={`This session: ${parts.map(([word, count]) => `${count} ${word}`).join(', ') || 'nothing yet'}. ${on ? 'Show all cards' : 'Dim untouched cards'}`}
286
+ onClick={(event) => {
287
+ // Inside the chip's tap-to-open hint: toggling the lens must not open it.
288
+ event.stopPropagation();
289
+ setActivityLens(!on, { sessionId: session.sessionId });
290
+ }}
291
+ >
292
+ {counts}
293
+ </button>
294
+ );
295
+ }
296
+
151
297
  /**
152
298
  * Amber escalation badge while any approval gate is pending in an attached
153
299
  * session: "1 gate · 4:31" — the countdown is the soonest auto-hold.
154
300
  */
155
301
  function GateBadge() {
156
302
  const count = pendingGates.value.length;
157
- const active = activeSession.value !== null && count > 0;
303
+ const active = count > 0;
158
304
  const now = useNow(active ? 1000 : 0);
159
305
  if (!active) return null;
160
306
  const soonest = pendingGates.value
@@ -164,65 +310,12 @@ function GateBadge() {
164
310
  return (
165
311
  <BarHint
166
312
  label="Approval gates waiting on you"
167
- tapToOpen
168
- body="The agent is blocked until you approve or reject in the session panel; unanswered gates auto-hold when the countdown runs out."
313
+ body="Open agent controls to approve or reject. Unanswered gates auto-hold when the countdown runs out, even with the session beta off."
169
314
  >
170
- <span class="gate-badge">
315
+ <button type="button" class="gate-badge" onClick={() => (sessionPanelOpened.value = true)}>
171
316
  {count} gate{count === 1 ? '' : 's'}
172
317
  {soonest !== undefined && ` · ${formatCountdown(soonest)}`}
173
- </span>
174
- </BarHint>
175
- );
176
- }
177
-
178
- function budgetTone(ratio: number): 'ok' | 'warn' | 'danger' {
179
- if (ratio > 0.9) return 'danger';
180
- if (ratio >= 0.7) return 'warn';
181
- return 'ok';
182
- }
183
-
184
- function formatTokens(value: number): string {
185
- return value >= 1000 ? `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k` : String(value);
186
- }
187
-
188
- /**
189
- * Context meter (rail-chrome-v2 phase 5). Two honest modes:
190
- * - the host reported the agent's REAL window (`contextUsage` on its presence)
191
- * → "Context": used / window, with the pinned payload as a note;
192
- * - nothing reported (no adapter does yet) → "Pins": the pinned-context
193
- * payload estimate against the configured budget
194
- * (PMX_CANVAS_CONTEXT_BUDGET_TOKENS). Never presented as the agent's window.
195
- * Session-only, like the chip; the estimate moves within a frame of a pin toggle.
196
- */
197
- function ContextBudget() {
198
- const session = activeSession.value;
199
- if (!session) return null;
200
- const budget = contextBudget.value;
201
- const real = session.contextUsage;
202
- const used = real ? real.used : budget.used;
203
- const total = real ? real.total : budget.total;
204
- const ratio = total > 0 ? Math.min(1, used / total) : 0;
205
- const pct = Math.round(ratio * 100);
206
- const hint = real
207
- ? {
208
- label: `Context window — reported by ${session.label}`,
209
- body: `${formatTokens(real.used)} of ${formatTokens(real.total)} tokens of the agent's actual context window are used. Your ✦-pinned nodes account for ≈ ${formatTokens(budget.used)} tokens of it.`,
210
- }
211
- : {
212
- label: 'Pins — pinned-context size (estimate)',
213
- body: `The nodes you pinned (✦) are what the agent is asked to carry as context: ≈ ${formatTokens(budget.used)} of a ${formatTokens(budget.total)}-token budget. When the agent's host reports its real context window, this meter switches to showing that instead.`,
214
- };
215
- return (
216
- <BarHint label={hint.label} body={hint.body} tapToOpen>
217
- <span class={`context-budget tone-${budgetTone(ratio)}`} data-mode={real ? 'window' : 'pins'}>
218
- <span class="context-budget-caption hud-collapsible-text">{real ? 'Context' : 'Pins'}</span>
219
- <span class="context-budget-track" aria-hidden="true">
220
- <span class="context-budget-fill" style={{ width: `${pct}%` }} />
221
- </span>
222
- <span class="context-budget-label" data-testid="budget-label">
223
- {used > 0 && pct === 0 ? '<1' : pct}%
224
- </span>
225
- </span>
318
+ </button>
226
319
  </BarHint>
227
320
  );
228
321
  }
@@ -234,7 +327,7 @@ function ContextBudget() {
234
327
  */
235
328
  function StartSessionButton() {
236
329
  const [busy, setBusy] = useState(false);
237
- if (activeSession.value) return null;
330
+ if (!agentSessionsEnabled.value || activeSession.value) return null;
238
331
  return (
239
332
  <BarHint
240
333
  label="Start an agent session"
@@ -243,6 +336,7 @@ function StartSessionButton() {
243
336
  <button
244
337
  type="button"
245
338
  class="start-session-btn"
339
+ aria-label="Start agent session beta"
246
340
  disabled={busy}
247
341
  onClick={() => {
248
342
  setBusy(true);
@@ -250,7 +344,7 @@ function StartSessionButton() {
250
344
  }}
251
345
  >
252
346
  <span class="start-session-dot" aria-hidden="true" />
253
- Start agent session
347
+ <span class="start-session-label">Start agent session</span>
254
348
  <span class="beta-tag">beta</span>
255
349
  </button>
256
350
  </BarHint>
@@ -264,6 +358,32 @@ function StartSessionButton() {
264
358
  * below 1180px, and nothing ever wraps to a second row.
265
359
  */
266
360
  export function TopBar() {
361
+ const barRef = useRef<HTMLDivElement>(null);
362
+ useLayoutEffect(() => {
363
+ const bar = barRef.current;
364
+ const controls = bar?.querySelector<HTMLElement>('.top-bar-right');
365
+ if (!bar || !controls) return;
366
+ // Fold by actual content, not only window width: session counts and agent
367
+ // names can fill a wide pane too. Re-evaluate from the full layout so a
368
+ // resize or ended session can unfold it again.
369
+ const fit = () => {
370
+ const menuOpen = bar.querySelector('.top-bar-more [aria-expanded="true"]');
371
+ bar.classList.remove('is-folded', 'is-compact');
372
+ const overflow = () => controls.getBoundingClientRect().right > bar.getBoundingClientRect().right - 14;
373
+ // A resize or updated count must not hide the menu under its pointer.
374
+ if (menuOpen || overflow()) bar.classList.add('is-folded');
375
+ if (overflow()) bar.classList.add('is-compact');
376
+ };
377
+ const resize = new ResizeObserver(fit);
378
+ resize.observe(bar);
379
+ const content = new MutationObserver(fit);
380
+ content.observe(bar, { childList: true, characterData: true, subtree: true });
381
+ fit();
382
+ return () => {
383
+ resize.disconnect();
384
+ content.disconnect();
385
+ };
386
+ }, []);
267
387
  const status = connectionStatus.value;
268
388
  const hasSynced = hasInitialServerLayout.value;
269
389
  const v = viewport.value;
@@ -295,6 +415,7 @@ export function TopBar() {
295
415
 
296
416
  const degraded = degradedState.value;
297
417
  const statusTitle = degraded ?? (status === 'connected' && !hasSynced ? 'syncing' : status);
418
+ const pinTarget = activeBoard();
298
419
  const countsLabel = hasSynced
299
420
  ? [
300
421
  `${nodeCount} node${nodeCount !== 1 ? 's' : ''}`,
@@ -313,7 +434,7 @@ export function TopBar() {
313
434
  };
314
435
 
315
436
  return (
316
- <div class="top-bar">
437
+ <div class="top-bar" ref={barRef}>
317
438
  <BarHint
318
439
  label={`Canvas status: ${statusTitle}`}
319
440
  tapToOpen
@@ -323,73 +444,182 @@ export function TopBar() {
323
444
  <span class={`connection-dot ${degraded ?? status}`} aria-label={`Canvas status: ${statusTitle}`} />
324
445
  </BarHint>
325
446
  <BoardSwitcher fallbackName={workspaceName || 'PMX Canvas'} />
447
+ {pinTarget && !browsingMemory.value && <BoardPinButton board={pinTarget} />}
326
448
  <span class="top-bar-meta hud-collapsible-text">{sessionId.value ? sessionId.value.slice(0, 12) : '…'}</span>
327
- <span class="top-bar-meta hud-collapsible-text">{countsLabel}</span>
449
+ {!browsingMemory.value && <span class="top-bar-meta hud-collapsible-text">{countsLabel}</span>}
328
450
 
329
451
  <span class="top-bar-spacer" />
330
452
 
331
- <AgentChip />
332
- <GateBadge />
333
- <ContextBudget />
334
- <ExternalWriterIndicator />
335
- {!isHostedWorkbench() && <StartSessionButton />}
453
+ {/* Keep the context summary; fold controls before compacting sessions
454
+ when the actual content fills the bar (ContextChip600.dc.html). */}
455
+ <div class="top-bar-right">
456
+ <AgentChip />
457
+ <GateBadge />
458
+ <ContextChip />
459
+ <ExternalWriterIndicator />
460
+ {!isHostedWorkbench() && <StartSessionButton />}
336
461
 
337
- <div class="top-bar-sep" />
462
+ {!browsingMemory.value && (
463
+ <>
464
+ <div class="top-bar-sep" />
465
+ <div class="top-bar-actions">
466
+ {activeBoardId.value && (
467
+ <BarHint
468
+ label="Export board"
469
+ body="One HTML file of this board that anyone can open, no install needed."
470
+ >
471
+ <button
472
+ type="button"
473
+ class="present-button"
474
+ onClick={() => {
475
+ exportDialogOpen.value = true;
476
+ }}
477
+ >
478
+ Export
479
+ </button>
480
+ </BarHint>
481
+ )}
482
+ {activeBoardId.value && (
483
+ <BarHint
484
+ label="Present board"
485
+ body="Arrow keys or Space step through stops; Esc exits. Without a saved tour, groups are read top-to-bottom, left-to-right."
486
+ >
487
+ <button
488
+ type="button"
489
+ class="present-button"
490
+ onClick={() => {
491
+ void startPresentation();
492
+ }}
493
+ >
494
+ Present
495
+ </button>
496
+ </BarHint>
497
+ )}
498
+ <BarHint label="Zoom out" shortcut={modChord('\u2212')}>
499
+ <button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1 / 1.25)} aria-label="Zoom out">
500
+ <IconZoomOut />
501
+ </button>
502
+ </BarHint>
503
+ <BarHint label="Reset zoom" shortcut={modChord('0')}>
504
+ <button
505
+ type="button"
506
+ class="top-bar-zoom-label"
507
+ onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)}
508
+ aria-label="Reset view"
509
+ >
510
+ {Math.round(v.scale * 100)}%
511
+ </button>
512
+ </BarHint>
513
+ <BarHint label="Zoom in" shortcut={modChord('+')}>
514
+ <button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1.25)} aria-label="Zoom in">
515
+ <IconZoomIn />
516
+ </button>
517
+ </BarHint>
518
+ <BarHint label="Fit all" shortcut="F" align="end">
519
+ <button type="button" class="top-bar-btn" onClick={handleFit} aria-label="Fit canvas">
520
+ <IconFitAll />
521
+ </button>
522
+ </BarHint>
523
+ </div>
524
+ <TopBarMore boardOpen={!!activeBoardId.value} onFit={handleFit} />
525
+ </>
526
+ )}
527
+ </div>
528
+ </div>
529
+ );
530
+ }
338
531
 
339
- {activeBoardId.value && (
340
- <BarHint label="Export board" body="One HTML file of this board that anyone can open, no install needed.">
341
- <button
342
- type="button"
343
- class="present-button"
344
- onClick={() => {
345
- exportDialogOpen.value = true;
346
- }}
347
- >
348
- Export
532
+ /** Narrow bars fold Export, Present, zoom and fit into one ⋯ menu (Pane600.dc.html). */
533
+ function TopBarMore({ boardOpen, onFit }: { boardOpen: boolean; onFit: () => void }) {
534
+ const [open, setOpen] = useState(false);
535
+ const root = useRef<HTMLDivElement>(null);
536
+ const trigger = useRef<HTMLButtonElement>(null);
537
+ useLayoutEffect(() => {
538
+ if (!open) return;
539
+ const items = () => [...(root.current?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [])];
540
+ items()[0]?.focus();
541
+ const onKey = (event: KeyboardEvent) => {
542
+ if (event.key === 'Escape') {
543
+ setOpen(false);
544
+ trigger.current?.focus();
545
+ } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
546
+ event.preventDefault();
547
+ const list = items();
548
+ const at = list.indexOf(document.activeElement as HTMLElement);
549
+ const next = event.key === 'ArrowDown' ? at + 1 : at - 1;
550
+ list[(next + list.length) % list.length]?.focus();
551
+ }
552
+ };
553
+ const onDown = (event: PointerEvent) => {
554
+ if (root.current && !root.current.contains(event.target as Node)) setOpen(false);
555
+ };
556
+ window.addEventListener('keydown', onKey);
557
+ window.addEventListener('pointerdown', onDown);
558
+ return () => {
559
+ window.removeEventListener('keydown', onKey);
560
+ window.removeEventListener('pointerdown', onDown);
561
+ };
562
+ }, [open]);
563
+ const run = (action: () => void) => () => {
564
+ setOpen(false);
565
+ action();
566
+ };
567
+ return (
568
+ <div class="top-bar-more" ref={root}>
569
+ <button
570
+ ref={trigger}
571
+ type="button"
572
+ class="top-bar-btn"
573
+ aria-label="More: export, present, zoom, fit"
574
+ aria-haspopup="menu"
575
+ aria-expanded={open}
576
+ onClick={() => setOpen(!open)}
577
+ >
578
+ ⋯
579
+ </button>
580
+ {open && (
581
+ <div class="toolbar-menu top-bar-more-menu" role="menu">
582
+ {boardOpen && (
583
+ <button
584
+ type="button"
585
+ role="menuitem"
586
+ class="toolbar-menu-item"
587
+ onClick={run(() => {
588
+ exportDialogOpen.value = true;
589
+ })}
590
+ >
591
+ Export board
592
+ </button>
593
+ )}
594
+ {boardOpen && (
595
+ <button
596
+ type="button"
597
+ role="menuitem"
598
+ class="toolbar-menu-item"
599
+ onClick={run(() => void startPresentation())}
600
+ >
601
+ Present board
602
+ </button>
603
+ )}
604
+ <button type="button" role="menuitem" class="toolbar-menu-item" onClick={run(() => zoomByFactor(1.25))}>
605
+ Zoom in
606
+ </button>
607
+ <button type="button" role="menuitem" class="toolbar-menu-item" onClick={run(() => zoomByFactor(1 / 1.25))}>
608
+ Zoom out
349
609
  </button>
350
- </BarHint>
351
- )}
352
- {activeBoardId.value && (
353
- <BarHint
354
- label="Present board"
355
- body="Arrow keys or Space step through stops; Esc exits. Without a saved tour, groups are read top-to-bottom, left-to-right."
356
- >
357
610
  <button
358
611
  type="button"
359
- class="present-button"
360
- onClick={() => {
361
- presenting.value = true;
362
- }}
612
+ role="menuitem"
613
+ class="toolbar-menu-item"
614
+ onClick={run(() => animateViewport({ x: 0, y: 0, scale: 1 }, 250))}
363
615
  >
364
- Present
616
+ Reset zoom
365
617
  </button>
366
- </BarHint>
618
+ <button type="button" role="menuitem" class="toolbar-menu-item" onClick={run(onFit)}>
619
+ Fit all
620
+ </button>
621
+ </div>
367
622
  )}
368
- <BarHint label="Zoom out" shortcut={modChord('\u2212')}>
369
- <button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1 / 1.25)} aria-label="Zoom out">
370
- <IconZoomOut />
371
- </button>
372
- </BarHint>
373
- <BarHint label="Reset zoom" shortcut={modChord('0')}>
374
- <button
375
- type="button"
376
- class="top-bar-zoom-label"
377
- onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)}
378
- aria-label="Reset view"
379
- >
380
- {Math.round(v.scale * 100)}%
381
- </button>
382
- </BarHint>
383
- <BarHint label="Zoom in" shortcut={modChord('+')}>
384
- <button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1.25)} aria-label="Zoom in">
385
- <IconZoomIn />
386
- </button>
387
- </BarHint>
388
- <BarHint label="Fit all" shortcut="F" align="end">
389
- <button type="button" class="top-bar-btn" onClick={handleFit} aria-label="Fit canvas">
390
- <IconFitAll />
391
- </button>
392
- </BarHint>
393
623
  </div>
394
624
  );
395
625
  }
@@ -49,17 +49,24 @@ export interface CanvasFitInsets {
49
49
 
50
50
  /**
51
51
  * Screen-space insets Fit must reserve for the floating chrome that overlays
52
- * the canvas region: the bottom command bar (whose pinned-chip row grows it)
52
+ * the canvas region: the bottom command bar or standalone context pill,
53
53
  * and the bottom-right minimap. Measured from the live elements — fit's old
54
54
  * world-space padding shrank with the fit zoom and parked nodes underneath
55
55
  * both (0.5.0 Amp report, finding A; architecture rule 9's trap).
56
56
  */
57
57
  export function canvasFitInsets(): CanvasFitInsets {
58
+ let top = 16;
58
59
  let bottom = 16;
59
60
  try {
60
61
  const area = canvasArea();
61
62
  const areaBottom = area.top + area.height;
62
- for (const selector of ['.command-bar', '.minimap']) {
63
+ for (const selector of ['.lifecycle-filters', '.relation-legend']) {
64
+ const el = document.querySelector(selector);
65
+ if (!(el instanceof HTMLElement) || !el.isConnected) continue;
66
+ const rect = el.getBoundingClientRect();
67
+ if (rect.height > 0) top = Math.max(top, rect.bottom - area.top + 12);
68
+ }
69
+ for (const selector of ['.command-bar', '.context-pin-bar', '.minimap']) {
63
70
  const el = document.querySelector(selector);
64
71
  if (!(el instanceof HTMLElement) || !el.isConnected) continue;
65
72
  const rect = el.getBoundingClientRect();
@@ -70,5 +77,5 @@ export function canvasFitInsets(): CanvasFitInsets {
70
77
  } catch {
71
78
  // Chromeless embeds and unit DOMs: the defaults below are the contract.
72
79
  }
73
- return { top: 16, right: 16, bottom, left: 16 };
80
+ return { top, right: 16, bottom, left: 16 };
74
81
  }