pmx-canvas 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +59 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2724 -476
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2899 -596
  8. package/dist/json-render/index.js +20 -20
  9. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  10. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  11. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  12. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  13. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  14. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  15. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  16. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  17. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  19. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  20. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  21. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  22. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  23. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  24. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  25. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  26. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  27. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  28. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  29. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  30. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  31. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  32. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  33. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  34. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  35. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  36. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  37. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  38. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  39. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  40. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  41. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  42. package/dist/types/client/icons.d.ts +0 -10
  43. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  44. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  45. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  46. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  47. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  48. package/dist/types/client/state/boards-store.d.ts +81 -9
  49. package/dist/types/client/state/canvas-store.d.ts +14 -1
  50. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  51. package/dist/types/client/state/context-status-store.d.ts +35 -0
  52. package/dist/types/client/state/human-store.d.ts +2 -1
  53. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  54. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  55. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  56. package/dist/types/client/state/presence-store.d.ts +4 -4
  57. package/dist/types/client/state/presentation.d.ts +28 -0
  58. package/dist/types/client/state/relation-store.d.ts +10 -0
  59. package/dist/types/client/state/session-store.d.ts +41 -2
  60. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  61. package/dist/types/client/types.d.ts +13 -1
  62. package/dist/types/client/utils/keyboard.d.ts +2 -0
  63. package/dist/types/client/utils/time-ago.d.ts +4 -0
  64. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  65. package/dist/types/server/agent-presence.d.ts +10 -3
  66. package/dist/types/server/ax-context.d.ts +1 -1
  67. package/dist/types/server/canvas-db.d.ts +30 -7
  68. package/dist/types/server/canvas-operations.d.ts +24 -2
  69. package/dist/types/server/canvas-provenance.d.ts +22 -2
  70. package/dist/types/server/canvas-serialization.d.ts +2 -0
  71. package/dist/types/server/canvas-state.d.ts +93 -15
  72. package/dist/types/server/canvas-validation.d.ts +1 -1
  73. package/dist/types/server/context-brief.d.ts +94 -6
  74. package/dist/types/server/context-reads.d.ts +33 -7
  75. package/dist/types/server/edit-summary.d.ts +20 -0
  76. package/dist/types/server/index.d.ts +71 -10
  77. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  78. package/dist/types/server/lifecycle-status.d.ts +10 -0
  79. package/dist/types/server/memory-graph.d.ts +9 -0
  80. package/dist/types/server/mutation-history.d.ts +2 -0
  81. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  82. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  83. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  84. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  85. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  86. package/dist/types/server/operations/ops/query.d.ts +23 -0
  87. package/dist/types/server/operations/registry.d.ts +1 -0
  88. package/dist/types/server/placement.d.ts +7 -0
  89. package/dist/types/server/scope-fence.d.ts +2 -1
  90. package/dist/types/server/spatial-analysis.d.ts +6 -3
  91. package/dist/types/server/tour-control.d.ts +32 -0
  92. package/dist/types/server/wiki-links.d.ts +14 -0
  93. package/dist/types/server/workbench-auth.d.ts +2 -0
  94. package/dist/types/shared/agent-context.d.ts +2 -0
  95. package/dist/types/shared/agent-presence.d.ts +15 -12
  96. package/dist/types/shared/auto-arrange.d.ts +1 -0
  97. package/dist/types/shared/boards.d.ts +4 -4
  98. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  99. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  100. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  101. package/dist/types/shared/memory-graph.d.ts +53 -0
  102. package/dist/types/shared/relations.d.ts +68 -0
  103. package/dist/types/shared/semantic-attention.d.ts +2 -0
  104. package/dist/types/shared/tour.d.ts +38 -0
  105. package/dist/types/shared/workspace-settings.d.ts +11 -0
  106. package/docs/RELEASE.md +21 -3
  107. package/docs/adr-001-bun-only-runtime.md +7 -1
  108. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  109. package/docs/api-reference.md +109 -0
  110. package/docs/ax-host-adapter-contract.md +1 -1
  111. package/docs/ax-state-contract.md +5 -4
  112. package/docs/cli.md +153 -15
  113. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  114. package/docs/design/AgentContext.dc.html +3 -3
  115. package/docs/design/BoardPins.dc.html +37 -22
  116. package/docs/design/Categories.dc.html +208 -0
  117. package/docs/design/CategoryEditor.dc.html +33 -0
  118. package/docs/design/Chrome.dc.html +52 -9
  119. package/docs/design/ContextChip.dc.html +224 -0
  120. package/docs/design/ContextChip600.dc.html +136 -0
  121. package/docs/design/EmptyState.dc.html +45 -0
  122. package/docs/design/Flows.dc.html +23 -1
  123. package/docs/design/HeaderMarks.dc.html +183 -0
  124. package/docs/design/Home.dc.html +19 -1
  125. package/docs/design/HomeGraph.dc.html +86 -202
  126. package/docs/design/HomeMap.dc.html +23 -1
  127. package/docs/design/HomeTeam.dc.html +32 -14
  128. package/docs/design/Lifecycle.dc.html +22 -0
  129. package/docs/design/Lifecycle600.dc.html +18 -0
  130. package/docs/design/LifecycleVariants.dc.html +157 -0
  131. package/docs/design/LinkAuthorship.dc.html +139 -0
  132. package/docs/design/LinksChanged.dc.html +203 -0
  133. package/docs/design/LinksOptions.dc.html +137 -0
  134. package/docs/design/LinksOptions600.dc.html +134 -0
  135. package/docs/design/NearPin.dc.html +9 -10
  136. package/docs/design/Pane600.dc.html +4 -3
  137. package/docs/design/Relations.dc.html +3 -2
  138. package/docs/design/Restore.dc.html +152 -0
  139. package/docs/design/SeeChange.dc.html +267 -0
  140. package/docs/design/Uses.dc.html +12 -6
  141. package/docs/design/canvas.json +146 -19
  142. package/docs/design.md +449 -16
  143. package/docs/environment.md +0 -1
  144. package/docs/evals/curation-effect.md +128 -0
  145. package/docs/http-api.md +459 -37
  146. package/docs/mcp.md +146 -18
  147. package/docs/node-types.md +58 -3
  148. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  149. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  150. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  151. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  152. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  153. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  154. package/docs/product-context-vision-2026-09.md +17 -1
  155. package/docs/product-vision-2026-09.md +1082 -43
  156. package/docs/product-vision-review-2026-09.md +13 -1
  157. package/docs/screenshots/workbench-live-dark.png +0 -0
  158. package/docs/screenshots/workbench-live-light.png +0 -0
  159. package/docs/sdk.md +62 -2
  160. package/package.json +1 -1
  161. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  162. package/plugins/pmx-canvas/plugin.json +1 -1
  163. package/plugins/pmx-canvas/runtime.json +1 -1
  164. package/plugins/pmx-canvas/tools.json +601 -22
  165. package/skills/pmx-canvas/SKILL.md +216 -42
  166. package/skills/pmx-canvas/evals/evals.json +224 -0
  167. package/skills/pmx-canvas/references/full-reference.md +120 -21
  168. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  169. package/skills/pmx-canvas/references/importing-context.md +132 -0
  170. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  171. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  172. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  173. package/src/cli/agent.ts +2 -0
  174. package/src/cli/commands/ax.ts +12 -0
  175. package/src/cli/commands/boards.ts +88 -11
  176. package/src/cli/commands/edges.ts +28 -0
  177. package/src/cli/commands/groups.ts +7 -0
  178. package/src/cli/commands/nodes.ts +1 -0
  179. package/src/cli/commands/query.ts +34 -2
  180. package/src/cli/commands/record.ts +199 -48
  181. package/src/cli/index.ts +6 -1
  182. package/src/client/App.tsx +177 -70
  183. package/src/client/canvas/AttentionHistory.tsx +0 -2
  184. package/src/client/canvas/BarHint.tsx +40 -2
  185. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  186. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  187. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  188. package/src/client/canvas/BoardPinButton.tsx +33 -0
  189. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  190. package/src/client/canvas/CanvasNode.tsx +360 -237
  191. package/src/client/canvas/CanvasViewport.tsx +45 -7
  192. package/src/client/canvas/CommandBar.tsx +29 -23
  193. package/src/client/canvas/CommandPalette.tsx +5 -12
  194. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  195. package/src/client/canvas/ContextChip.tsx +296 -0
  196. package/src/client/canvas/ContextMenu.tsx +63 -5
  197. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  198. package/src/client/canvas/EdgeLayer.tsx +339 -76
  199. package/src/client/canvas/EmptyState.tsx +33 -18
  200. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  201. package/src/client/canvas/ExternalWriters.tsx +57 -42
  202. package/src/client/canvas/HomeGraphView.tsx +942 -0
  203. package/src/client/canvas/HomeMapView.tsx +641 -0
  204. package/src/client/canvas/HomeReviewView.tsx +167 -0
  205. package/src/client/canvas/HomeView.tsx +687 -160
  206. package/src/client/canvas/IntentLayer.tsx +19 -12
  207. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  208. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  209. package/src/client/canvas/LifecycleFields.tsx +91 -0
  210. package/src/client/canvas/LifecyclePanel.tsx +543 -0
  211. package/src/client/canvas/MemoryInspector.tsx +104 -0
  212. package/src/client/canvas/Minimap.tsx +3 -3
  213. package/src/client/canvas/NearPinLayer.tsx +187 -0
  214. package/src/client/canvas/NodeContextMark.tsx +175 -97
  215. package/src/client/canvas/Presentation.tsx +23 -57
  216. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  217. package/src/client/canvas/RelationsPanel.tsx +82 -0
  218. package/src/client/canvas/SelectionBar.tsx +3 -6
  219. package/src/client/canvas/SessionPanel.tsx +90 -50
  220. package/src/client/canvas/SessionReceipt.tsx +311 -39
  221. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  222. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  223. package/src/client/canvas/ToolRail.tsx +367 -192
  224. package/src/client/canvas/TopBar.tsx +354 -124
  225. package/src/client/canvas/canvas-area.ts +10 -3
  226. package/src/client/canvas/graph-geometry.ts +195 -0
  227. package/src/client/canvas/graph-model.ts +254 -0
  228. package/src/client/canvas/memory-layout.ts +106 -0
  229. package/src/client/canvas/use-chrome-height.ts +20 -0
  230. package/src/client/canvas/use-focus-trap.ts +31 -5
  231. package/src/client/canvas/use-memory-graph.ts +58 -0
  232. package/src/client/canvas/use-node-drag.ts +2 -1
  233. package/src/client/canvas/use-pan-zoom.ts +64 -7
  234. package/src/client/icons.tsx +0 -63
  235. package/src/client/mcp-app/style.css +0 -6
  236. package/src/client/nodes/AxStepControls.tsx +74 -50
  237. package/src/client/nodes/BoardNode.tsx +5 -1
  238. package/src/client/nodes/ContextNode.tsx +18 -11
  239. package/src/client/nodes/ExtAppFrame.tsx +6 -3
  240. package/src/client/nodes/FileNode.tsx +46 -42
  241. package/src/client/nodes/HtmlNode.tsx +32 -0
  242. package/src/client/nodes/ImageNode.tsx +1 -1
  243. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  244. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  245. package/src/client/nodes/MarkdownNode.tsx +377 -109
  246. package/src/client/nodes/McpAppNode.tsx +50 -20
  247. package/src/client/nodes/MdFormatBar.tsx +21 -18
  248. package/src/client/nodes/PromptNode.tsx +1 -1
  249. package/src/client/nodes/ResponseNode.tsx +1 -1
  250. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  251. package/src/client/state/attention-bridge.ts +64 -15
  252. package/src/client/state/boards-store.ts +193 -39
  253. package/src/client/state/canvas-store.ts +99 -12
  254. package/src/client/state/context-chip-store.ts +70 -0
  255. package/src/client/state/context-status-store.ts +53 -0
  256. package/src/client/state/human-store.ts +13 -7
  257. package/src/client/state/intent-bridge.ts +51 -30
  258. package/src/client/state/knowledge-store.ts +43 -0
  259. package/src/client/state/near-pin-store.ts +115 -6
  260. package/src/client/state/presence-store.ts +7 -10
  261. package/src/client/state/presentation.ts +91 -0
  262. package/src/client/state/relation-store.ts +81 -0
  263. package/src/client/state/session-store.ts +144 -18
  264. package/src/client/state/sse-bridge.ts +235 -32
  265. package/src/client/state/workspace-settings.ts +11 -0
  266. package/src/client/theme/global.css +2724 -476
  267. package/src/client/types.ts +12 -0
  268. package/src/client/utils/keyboard.ts +10 -0
  269. package/src/client/utils/time-ago.ts +14 -0
  270. package/src/json-render/renderer/index.tsx +31 -0
  271. package/src/mcp/canvas-app.ts +12 -2
  272. package/src/mcp/context-uri-template.ts +7 -4
  273. package/src/mcp/server.ts +63 -41
  274. package/src/mcp/workbench-app.ts +39 -6
  275. package/src/server/agent-presence.ts +128 -28
  276. package/src/server/ax-context.ts +27 -13
  277. package/src/server/board-export.ts +12 -1
  278. package/src/server/canvas-db.ts +331 -42
  279. package/src/server/canvas-operations.ts +195 -83
  280. package/src/server/canvas-provenance.ts +53 -22
  281. package/src/server/canvas-serialization.ts +2 -0
  282. package/src/server/canvas-state.ts +950 -122
  283. package/src/server/canvas-validation.ts +9 -2
  284. package/src/server/context-brief.ts +345 -38
  285. package/src/server/context-reads.ts +178 -30
  286. package/src/server/demo-state.json +355 -24
  287. package/src/server/edit-summary.ts +74 -0
  288. package/src/server/html-surface.ts +8 -1
  289. package/src/server/index.ts +181 -11
  290. package/src/server/knowledge-lifecycle.ts +304 -0
  291. package/src/server/lifecycle-status.ts +232 -0
  292. package/src/server/memory-graph.ts +166 -0
  293. package/src/server/mutation-history.ts +18 -6
  294. package/src/server/operations/composites.ts +32 -11
  295. package/src/server/operations/http.ts +7 -2
  296. package/src/server/operations/index.ts +4 -0
  297. package/src/server/operations/mcp.ts +26 -1
  298. package/src/server/operations/ops/ax-presence.ts +1 -1
  299. package/src/server/operations/ops/ax-read.ts +57 -6
  300. package/src/server/operations/ops/ax-timeline.ts +28 -2
  301. package/src/server/operations/ops/backup.ts +5 -3
  302. package/src/server/operations/ops/batch.ts +35 -4
  303. package/src/server/operations/ops/boards.ts +188 -47
  304. package/src/server/operations/ops/canvas-wire.ts +31 -1
  305. package/src/server/operations/ops/edges.ts +36 -2
  306. package/src/server/operations/ops/groups.ts +6 -1
  307. package/src/server/operations/ops/lifecycle.ts +262 -0
  308. package/src/server/operations/ops/memory-graph.ts +45 -0
  309. package/src/server/operations/ops/nodes.ts +107 -24
  310. package/src/server/operations/ops/query.ts +281 -68
  311. package/src/server/operations/ops/snapshots.ts +73 -1
  312. package/src/server/operations/ops/validate.ts +5 -1
  313. package/src/server/operations/ops/viewport.ts +97 -3
  314. package/src/server/operations/registry.ts +74 -25
  315. package/src/server/placement.ts +29 -0
  316. package/src/server/scope-fence.ts +78 -6
  317. package/src/server/server.ts +115 -16
  318. package/src/server/spatial-analysis.ts +48 -23
  319. package/src/server/tour-control.ts +82 -0
  320. package/src/server/wiki-links.ts +137 -0
  321. package/src/server/workbench-auth.ts +11 -0
  322. package/src/shared/agent-context.ts +3 -0
  323. package/src/shared/agent-presence.ts +15 -15
  324. package/src/shared/auto-arrange.ts +2 -0
  325. package/src/shared/boards.ts +4 -4
  326. package/src/shared/knowledge-lifecycle.ts +22 -0
  327. package/src/shared/knowledge-presets.ts +80 -0
  328. package/src/shared/lifecycle-status.ts +26 -0
  329. package/src/shared/memory-graph.ts +74 -0
  330. package/src/shared/relations.ts +98 -0
  331. package/src/shared/semantic-attention.ts +15 -1
  332. package/src/shared/tour.ts +23 -5
  333. package/src/shared/workspace-settings.ts +13 -0
  334. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  335. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  336. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  337. package/dist/types/server/agent-context.d.ts +0 -22
  338. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -1,24 +1,22 @@
1
- import { useEffect, useRef, useState } from 'preact/hooks';
1
+ import { useId, useLayoutEffect, useRef, useState } from 'preact/hooks';
2
+ import { BarPortal } from './BarHint';
3
+ import { agentSessionsEnabled, knowledgeLifecycleEnabled, workspaceSettings } from '../state/workspace-settings';
4
+ import type { WorkspaceSettings } from '../../shared/workspace-settings.js';
5
+ import { attachedSessions } from '../state/presence-store';
6
+ import { heldGates, pendingGates, scopeFence, sessionPanelOpened } from '../state/session-store';
2
7
  import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
3
8
  import {
4
9
  IconArrange,
5
- IconClearTrace,
6
10
  IconCursorTool,
7
11
  IconEraser,
8
12
  IconHandTool,
9
13
  IconLogo,
10
- IconMinimap,
11
- IconMoon,
12
14
  IconNodeGroup,
13
15
  IconNodeMarkdown,
14
16
  IconNodeWebpage,
15
17
  IconPen,
16
18
  IconSearch,
17
- IconShortcuts,
18
- IconSnapshot,
19
- IconSun,
20
19
  IconTextAnnotation,
21
- IconTrace,
22
20
  IconUpload,
23
21
  } from '../icons';
24
22
  import {
@@ -33,8 +31,8 @@ import {
33
31
  } from '../state/canvas-store';
34
32
  import { importFiles } from './import-files';
35
33
  import { canvasArea } from './canvas-area';
36
- import { saveCanvasTheme } from '../state/intent-bridge';
37
- import { openBoard } from '../state/boards-store';
34
+ import { requestJson, saveCanvasTheme } from '../state/intent-bridge';
35
+ import { browsingMemory, openBoard } from '../state/boards-store';
38
36
  import { createNodeInView } from './create-in-view';
39
37
  import { askText } from './TextPrompt';
40
38
  import { FeedbackDialog } from './FeedbackDialog';
@@ -46,7 +44,6 @@ import {
46
44
  CANVAS_THEMES,
47
45
  CANVAS_THEME_META,
48
46
  type CanvasThemeName,
49
- canvasThemeScheme,
50
47
  normalizeCanvasThemeName,
51
48
  } from '../../shared/themes.js';
52
49
 
@@ -144,6 +141,84 @@ function RailButton({
144
141
  );
145
142
  }
146
143
 
144
+ /** Menu descriptions escape its scrolling container and also work with keyboard focus. */
145
+ function SettingsItem({
146
+ label,
147
+ description,
148
+ hint,
149
+ beta,
150
+ toggle = beta,
151
+ checked,
152
+ disabled,
153
+ onClick,
154
+ }: {
155
+ label: string;
156
+ description: string;
157
+ hint?: string;
158
+ beta?: boolean;
159
+ toggle?: boolean;
160
+ checked?: boolean;
161
+ disabled?: boolean;
162
+ onClick: () => void;
163
+ }) {
164
+ const id = useId();
165
+ const [anchor, setAnchor] = useState<DOMRect | null>(null);
166
+ const show = (event: Event) => setAnchor((event.currentTarget as HTMLElement).getBoundingClientRect());
167
+ const hide = () => setAnchor(null);
168
+ return (
169
+ <>
170
+ <button
171
+ type="button"
172
+ role={checked === undefined ? 'menuitem' : 'menuitemcheckbox'}
173
+ aria-label={beta ? `${label} (Beta)` : label}
174
+ aria-checked={checked}
175
+ aria-describedby={anchor ? id : undefined}
176
+ class="toolbar-menu-item"
177
+ aria-disabled={disabled || undefined}
178
+ onPointerEnter={show}
179
+ onPointerLeave={hide}
180
+ onFocus={show}
181
+ onBlur={hide}
182
+ onClick={() => {
183
+ if (disabled) return;
184
+ hide();
185
+ onClick();
186
+ }}
187
+ >
188
+ <span class="settings-item-label">
189
+ {label} {beta && <span class="beta-tag">Beta</span>}
190
+ </span>
191
+ {toggle ? (
192
+ <span class={`settings-switch${checked ? ' is-on' : ''}`} aria-hidden="true" />
193
+ ) : hint ? (
194
+ <span class="toolbar-menu-hint">{hint}</span>
195
+ ) : null}
196
+ </button>
197
+ {anchor && (
198
+ <BarPortal>
199
+ <span
200
+ id={id}
201
+ class="toolbar-tooltip settings-tooltip"
202
+ role="tooltip"
203
+ ref={(tip) => {
204
+ if (!tip) return;
205
+ const beside = window.innerWidth - anchor.right >= tip.offsetWidth + 20;
206
+ tip.style.left = `${beside ? anchor.right + 10 : Math.max(12, Math.min(anchor.left, window.innerWidth - tip.offsetWidth - 12))}px`;
207
+ tip.style.top = `${Math.max(12, Math.min(beside ? anchor.top : anchor.top - tip.offsetHeight - 8, window.innerHeight - tip.offsetHeight - 12))}px`;
208
+ }}
209
+ >
210
+ <span class="toolbar-tooltip-label">
211
+ {label}
212
+ {beta ? ' (Beta)' : ''}
213
+ </span>
214
+ <span class="toolbar-tooltip-body">{description}</span>
215
+ </span>
216
+ </BarPortal>
217
+ )}
218
+ </>
219
+ );
220
+ }
221
+
147
222
  /**
148
223
  * The persistent 52px left tool rail (rail-chrome-v2 phase 1): brand → tools →
149
224
  * node palette → utilities. Every button shows a hover/focus tooltip with its
@@ -175,47 +250,83 @@ export function ToolRail({
175
250
  const traceNodeCount = Array.from(nodes.value.values()).filter((n) => n.type === 'trace').length;
176
251
  const edgeCount = edges.value.size;
177
252
  const [feedbackOpen, setFeedbackOpen] = useState(false);
178
- const [openMenu, setOpenMenu] = useState<null | 'theme'>(null);
253
+ const [savingSettings, setSavingSettings] = useState(false);
254
+ const [settingsError, setSettingsError] = useState(false);
255
+ const setBeta = async (key: keyof WorkspaceSettings) => {
256
+ if (savingSettings || !workspaceSettings.value) return;
257
+ setSavingSettings(true);
258
+ setSettingsError(false);
259
+ try {
260
+ const result = await requestJson<{ ok: boolean }>(
261
+ 'saveWorkspaceSettings',
262
+ '/api/canvas/settings',
263
+ { ok: false },
264
+ {
265
+ method: 'POST',
266
+ headers: { 'Content-Type': 'application/json' },
267
+ body: JSON.stringify({ [key]: !workspaceSettings.value[key] }),
268
+ },
269
+ );
270
+ // Ordered SSE/poll frames, never a late POST acknowledgement, own the switches.
271
+ setSettingsError(result.ok !== true);
272
+ } finally {
273
+ setSavingSettings(false);
274
+ }
275
+ };
276
+ // The Settings menu (docs/design/Chrome.dc.html §7) and the view it shows.
277
+ const [openMenu, setOpenMenu] = useState<null | 'settings' | 'theme'>(null);
179
278
  const [menuAnchor, setMenuAnchor] = useState<{ bottom: number; right: number } | null>(null);
180
279
  const railRef = useRef<HTMLDivElement>(null);
181
280
  const fileInput = useRef<HTMLInputElement>(null);
182
281
 
183
- const toggleMenu = (menu: 'theme') => (e: MouseEvent) => {
184
- if (openMenu === menu) {
282
+ const toggleSettings = (e: MouseEvent) => {
283
+ if (openMenu) {
185
284
  setOpenMenu(null);
186
285
  return;
187
286
  }
188
287
  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
189
288
  setMenuAnchor({ bottom: rect.bottom, right: rect.right });
190
- setOpenMenu(menu);
289
+ setOpenMenu('settings');
290
+ };
291
+ /** Run a Settings action and close the menu; focus returns to the Settings button. */
292
+ const choose = (action: () => void) => () => {
293
+ setOpenMenu(null);
294
+ snapshotBtnRef.current?.focus();
295
+ action();
191
296
  };
192
297
 
193
298
  // The rail scrolls (overflow-y:auto), so an absolutely-positioned popover
194
299
  // would be clipped by it. Fixed positioning from the trigger's rect escapes
195
- // the scroll container; the theme menu bottom-aligns so it grows upward.
300
+ // the scroll container; the menu bottom-aligns so it grows upward.
196
301
  const sideMenuStyle = menuAnchor
197
302
  ? {
198
303
  position: 'fixed' as const,
199
304
  left: `${menuAnchor.right + 8}px`,
200
- bottom: `${window.innerHeight - menuAnchor.bottom}px`,
305
+ bottom: `${Math.max(10, window.innerHeight - menuAnchor.bottom)}px`,
201
306
  top: 'auto',
202
307
  right: 'auto',
203
308
  }
204
309
  : undefined;
205
310
 
206
- useEffect(() => {
311
+ useLayoutEffect(() => {
207
312
  if (!openMenu) return;
208
313
  const onPointerDown = (e: PointerEvent) => {
209
314
  if (railRef.current && e.target instanceof Node && !railRef.current.contains(e.target)) setOpenMenu(null);
210
315
  };
211
316
  const onKeyDown = (e: KeyboardEvent) => {
212
- if (e.key === 'Escape') setOpenMenu(null);
317
+ if (e.key === 'Escape') {
318
+ e.preventDefault();
319
+ e.stopPropagation();
320
+ setOpenMenu(null);
321
+ snapshotBtnRef.current?.focus({ preventScroll: true });
322
+ }
213
323
  };
214
324
  document.addEventListener('pointerdown', onPointerDown);
215
- document.addEventListener('keydown', onKeyDown);
325
+ // Own Escape even when a saving toggle has temporarily moved focus to body.
326
+ document.addEventListener('keydown', onKeyDown, true);
216
327
  return () => {
217
328
  document.removeEventListener('pointerdown', onPointerDown);
218
- document.removeEventListener('keydown', onKeyDown);
329
+ document.removeEventListener('keydown', onKeyDown, true);
219
330
  };
220
331
  }, [openMenu]);
221
332
 
@@ -253,189 +364,256 @@ export function ToolRail({
253
364
  </span>
254
365
  </RailButton>
255
366
 
256
- <RailButton
257
- label="Bug and feedback"
258
- menuOpen={feedbackOpen}
259
- onClick={() => {
260
- setOpenMenu(null);
261
- setFeedbackOpen(true);
262
- }}
263
- >
264
- <svg
265
- width="16"
266
- height="16"
267
- viewBox="0 0 16 16"
268
- fill="none"
269
- stroke="currentColor"
270
- stroke-width="1.5"
271
- stroke-linecap="round"
272
- stroke-linejoin="round"
273
- aria-hidden="true"
274
- >
275
- <rect x="4.5" y="5" width="7" height="9" rx="3.5" />
276
- <path d="M6 5V4a2 2 0 0 1 4 0v1M8 8v6M2 4l2.5 2M14 4l-2.5 2M1.5 9h3M11.5 9h3M2 14l2.5-2M14 14l-2.5-2" />
277
- </svg>
278
- </RailButton>
367
+ {!browsingMemory.value && (
368
+ <>
369
+ <div class="rail-divider" />
279
370
 
280
- <div class="rail-divider" />
281
-
282
- <RailButton
283
- label="Select"
284
- shortcut="V"
285
- active={tool === 'select'}
286
- onClick={() => (canvasTool.value = 'select')}
287
- >
288
- <IconCursorTool />
289
- </RailButton>
290
- <RailButton label="Pan" shortcut="Space" active={tool === 'pan'} onClick={() => (canvasTool.value = 'pan')}>
291
- <IconHandTool />
292
- </RailButton>
293
- <RailButton
294
- label="Connect"
295
- shortcut="C"
296
- active={tool === 'connect'}
297
- onClick={() => (canvasTool.value = 'connect')}
298
- >
299
- <svg
300
- width="15"
301
- height="15"
302
- viewBox="0 0 16 16"
303
- fill="none"
304
- stroke="currentColor"
305
- stroke-width="1.5"
306
- stroke-linecap="round"
307
- aria-hidden="true"
308
- >
309
- <circle cx="4" cy="4" r="2" />
310
- <circle cx="12" cy="12" r="2" />
311
- <path d="M5.5 5.5 C 8 8, 8 8, 10.5 10.5" />
312
- </svg>
313
- </RailButton>
371
+ <RailButton
372
+ label="Select"
373
+ shortcut="V"
374
+ active={tool === 'select'}
375
+ onClick={() => (canvasTool.value = 'select')}
376
+ >
377
+ <IconCursorTool />
378
+ </RailButton>
379
+ <RailButton label="Pan" shortcut="Space" active={tool === 'pan'} onClick={() => (canvasTool.value = 'pan')}>
380
+ <IconHandTool />
381
+ </RailButton>
382
+ <RailButton
383
+ label="Connect"
384
+ shortcut="C"
385
+ active={tool === 'connect'}
386
+ onClick={() => (canvasTool.value = 'connect')}
387
+ >
388
+ <svg
389
+ width="15"
390
+ height="15"
391
+ viewBox="0 0 16 16"
392
+ fill="none"
393
+ stroke="currentColor"
394
+ stroke-width="1.5"
395
+ stroke-linecap="round"
396
+ aria-hidden="true"
397
+ >
398
+ <circle cx="4" cy="4" r="2" />
399
+ <circle cx="12" cy="12" r="2" />
400
+ <path d="M5.5 5.5 C 8 8, 8 8, 10.5 10.5" />
401
+ </svg>
402
+ </RailButton>
314
403
 
315
- <div class="rail-divider" />
404
+ <div class="rail-divider" />
316
405
 
317
- <RailButton
318
- label="Markdown note"
319
- shortcut="M"
320
- onClick={() =>
321
- void createNodeInView({ type: 'markdown', title: 'New note', width: 520, height: 360 }).catch((error) =>
322
- logRailError('create markdown', error),
323
- )
324
- }
325
- >
326
- <IconNodeMarkdown size={15} />
327
- </RailButton>
328
- <RailButton
329
- label="Attach files"
330
- ariaLabel="Attach files"
331
- detail="Images (PNG, JPEG, SVG…), Markdown and text/code files. PDF, Word, Excel, PowerPoint and OpenDocument use agent import with review."
332
- onClick={() => fileInput.current?.click()}
333
- >
334
- <IconUpload size={15} />
335
- </RailButton>
336
- <input
337
- ref={fileInput}
338
- type="file"
339
- hidden
340
- multiple
341
- aria-label="Attach files"
342
- onChange={(event) => {
343
- const area = canvasArea();
344
- const view = viewport.value;
345
- void importFiles(
346
- Array.from(event.currentTarget.files ?? []),
347
- (area.width / 2 - view.x) / view.scale,
348
- (area.height / 2 - view.y) / view.scale,
349
- );
350
- event.currentTarget.value = '';
351
- }}
352
- />
353
- <RailButton label="Webpage" shortcut="W" onClick={() => promptedCreate('webpage')}>
354
- <IconNodeWebpage size={15} />
355
- </RailButton>
356
- <RailButton
357
- label="Group"
358
- shortcut="G"
359
- onClick={() =>
360
- void createNodeInView({ type: 'group', title: 'Group' }).catch((error) =>
361
- logRailError('create group', error),
362
- )
363
- }
364
- >
365
- <IconNodeGroup size={15} />
366
- </RailButton>
367
- <RailButton
368
- label="Draw"
369
- shortcut="A"
370
- active={annotationTool === 'pen'}
371
- onClick={() => onSetAnnotationTool(annotationTool === 'pen' ? null : 'pen')}
372
- >
373
- <IconPen />
374
- </RailButton>
375
- <RailButton
376
- label="Text note"
377
- active={annotationTool === 'text'}
378
- onClick={() => onSetAnnotationTool(annotationTool === 'text' ? null : 'text')}
379
- >
380
- <IconTextAnnotation />
381
- </RailButton>
382
- <RailButton
383
- label="Eraser"
384
- active={annotationTool === 'eraser'}
385
- onClick={() => onSetAnnotationTool(annotationTool === 'eraser' ? null : 'eraser')}
386
- >
387
- <IconEraser />
388
- </RailButton>
406
+ <RailButton
407
+ label="Markdown note"
408
+ shortcut="M"
409
+ onClick={() =>
410
+ void createNodeInView({ type: 'markdown', title: 'New note', width: 520, height: 360 }).catch((error) =>
411
+ logRailError('create markdown', error),
412
+ )
413
+ }
414
+ >
415
+ <IconNodeMarkdown size={15} />
416
+ </RailButton>
417
+ <RailButton
418
+ label="Attach files"
419
+ ariaLabel="Attach files"
420
+ detail="Images (PNG, JPEG, SVG…), Markdown and text/code files. PDF, Word, Excel, PowerPoint and OpenDocument use agent import with review."
421
+ onClick={() => fileInput.current?.click()}
422
+ >
423
+ <IconUpload size={15} />
424
+ </RailButton>
425
+ <input
426
+ ref={fileInput}
427
+ type="file"
428
+ hidden
429
+ multiple
430
+ aria-label="Attach files"
431
+ onChange={(event) => {
432
+ const area = canvasArea();
433
+ const view = viewport.value;
434
+ void importFiles(
435
+ Array.from(event.currentTarget.files ?? []),
436
+ (area.width / 2 - view.x) / view.scale,
437
+ (area.height / 2 - view.y) / view.scale,
438
+ );
439
+ event.currentTarget.value = '';
440
+ }}
441
+ />
442
+ <RailButton label="Webpage" shortcut="W" onClick={() => promptedCreate('webpage')}>
443
+ <IconNodeWebpage size={15} />
444
+ </RailButton>
445
+ <RailButton
446
+ label="Group"
447
+ shortcut="G"
448
+ onClick={() =>
449
+ void createNodeInView({ type: 'group', title: 'Group' }).catch((error) =>
450
+ logRailError('create group', error),
451
+ )
452
+ }
453
+ >
454
+ <IconNodeGroup size={15} />
455
+ </RailButton>
456
+ <RailButton
457
+ label="Draw"
458
+ shortcut="A"
459
+ active={annotationTool === 'pen'}
460
+ onClick={() => onSetAnnotationTool(annotationTool === 'pen' ? null : 'pen')}
461
+ >
462
+ <IconPen />
463
+ </RailButton>
464
+ <RailButton
465
+ label="Text note"
466
+ active={annotationTool === 'text'}
467
+ onClick={() => onSetAnnotationTool(annotationTool === 'text' ? null : 'text')}
468
+ >
469
+ <IconTextAnnotation />
470
+ </RailButton>
471
+ <RailButton
472
+ label="Eraser"
473
+ active={annotationTool === 'eraser'}
474
+ onClick={() => onSetAnnotationTool(annotationTool === 'eraser' ? null : 'eraser')}
475
+ >
476
+ <IconEraser />
477
+ </RailButton>
389
478
 
390
- <div class="rail-spacer" />
479
+ <div class="rail-divider" />
480
+ </>
481
+ )}
391
482
 
392
- <RailButton label="Search & commands" shortcut={modChord('K')} onClick={onOpenPalette}>
393
- <IconSearch />
394
- </RailButton>
395
- <RailButton
396
- label="Arrange"
397
- detail={edgeCount > 0 ? 'graph-aware' : 'grid'}
398
- onClick={() => (edgeCount > 0 ? forceDirectedArrange() : autoArrange())}
399
- >
400
- <IconArrange />
401
- </RailButton>
402
- {!isHostedWorkbench() && (
403
- <RailButton
404
- label={isTraceOn ? 'Disable trace' : 'Enable trace'}
405
- active={isTraceOn}
406
- onClick={() => sendIntent('trace-toggle', { enabled: !isTraceOn })}
407
- >
408
- <IconTrace />
483
+ {!browsingMemory.value && (
484
+ <RailButton label="Search & commands" shortcut={modChord('K')} onClick={onOpenPalette}>
485
+ <IconSearch />
409
486
  </RailButton>
410
487
  )}
411
- {!isHostedWorkbench() && (isTraceOn || traceNodeCount > 0) && (
412
- <RailButton label="Clear trace" onClick={() => sendIntent('trace-clear')}>
413
- <IconClearTrace />
488
+ {!browsingMemory.value && (
489
+ <RailButton
490
+ label="Arrange"
491
+ detail={edgeCount > 0 ? 'graph-aware' : 'grid'}
492
+ onClick={() => (edgeCount > 0 ? forceDirectedArrange() : autoArrange())}
493
+ >
494
+ <IconArrange />
414
495
  </RailButton>
415
496
  )}
416
- <RailButton
417
- label={minimapVisible ? 'Hide minimap' : 'Show minimap'}
418
- active={minimapVisible}
419
- onClick={onToggleMinimap}
420
- >
421
- <IconMinimap />
422
- </RailButton>
423
- <RailButton label="Snapshots" active={snapshotOpen} onClick={onToggleSnapshot} btnRef={snapshotBtnRef}>
424
- <IconSnapshot />
425
- </RailButton>
497
+ <div class="rail-spacer" />
426
498
  <span class="toolbar-menu-anchor">
427
499
  <RailButton
428
- label="Theme"
429
- detail={CANVAS_THEME_META[activeTheme].label}
430
- menuOpen={openMenu === 'theme'}
431
- ariaLabel="Choose theme"
432
- active={openMenu === 'theme'}
433
- onClick={toggleMenu('theme')}
500
+ label="Settings"
501
+ detail="Appearance, board tools, beta features, and feedback"
502
+ ariaLabel="Settings"
503
+ menuOpen={openMenu !== null}
504
+ active={openMenu !== null || snapshotOpen}
505
+ onClick={toggleSettings}
506
+ btnRef={snapshotBtnRef}
434
507
  >
435
- {canvasThemeScheme(activeTheme) === 'dark' ? <IconSun /> : <IconMoon />}
508
+ <svg
509
+ width="16"
510
+ height="16"
511
+ viewBox="0 0 24 24"
512
+ fill="none"
513
+ stroke="currentColor"
514
+ stroke-width="1.8"
515
+ stroke-linecap="round"
516
+ stroke-linejoin="round"
517
+ aria-hidden="true"
518
+ >
519
+ <path d="M4 7h9M17 7h3M4 17h3M11 17h9M13 7a2 2 0 1 0 4 0a2 2 0 1 0-4 0M7 17a2 2 0 1 0 4 0a2 2 0 1 0-4 0" />
520
+ </svg>
436
521
  </RailButton>
522
+ {openMenu === 'settings' && (
523
+ <div class="toolbar-menu rail-settings-menu" style={sideMenuStyle} role="menu" aria-label="Settings">
524
+ <SettingsItem
525
+ label="Theme"
526
+ description="Choose the canvas appearance. Your choice is saved with the board."
527
+ hint={`${CANVAS_THEME_META[activeTheme].label} ›`}
528
+ onClick={() => setOpenMenu('theme')}
529
+ />
530
+ {!browsingMemory.value && (
531
+ <>
532
+ <SettingsItem
533
+ label="Snapshots"
534
+ description="Save a board checkpoint, compare changes, or restore an earlier version."
535
+ onClick={choose(onToggleSnapshot)}
536
+ />
537
+ <SettingsItem
538
+ label="Minimap"
539
+ description="Show a small overview of the board. Drag its viewport to move around the canvas."
540
+ checked={minimapVisible}
541
+ hint={minimapVisible ? 'On' : 'Off'}
542
+ onClick={choose(onToggleMinimap)}
543
+ />
544
+ </>
545
+ )}
546
+ <SettingsItem
547
+ label="Keyboard shortcuts"
548
+ description="See shortcuts for creating cards, selecting, navigating, and editing the canvas."
549
+ hint="?"
550
+ onClick={choose(onOpenShortcuts)}
551
+ />
552
+ <div class="toolbar-menu-divider" role="separator" />
553
+ <span class="settings-caption">Beta features · workspace-wide · off by default</span>
554
+ <SettingsItem
555
+ label="Agent sessions"
556
+ beta
557
+ checked={agentSessionsEnabled.value}
558
+ disabled={!workspaceSettings.value || savingSettings}
559
+ description="Show session chips, tasks, timeline, steering composer, and receipts. Turning off hides these without ending attached agents. Approvals and edit protections remain available. Saved for this workspace."
560
+ onClick={() => void setBeta('agentSessions')}
561
+ />
562
+ <SettingsItem
563
+ label="Knowledge lifecycle"
564
+ beta
565
+ checked={knowledgeLifecycleEnabled.value}
566
+ disabled={!workspaceSettings.value || savingSettings}
567
+ description="This turns on context lifecycle, where boards and cards decay over time as their relevance fades. Includes Review, review policies, categories, and due or retired indicators. Turning off hides lifecycle tools without deleting their data or drafts. Saved for this workspace."
568
+ onClick={() => void setBeta('knowledgeLifecycle')}
569
+ />
570
+ {settingsError && (
571
+ <p class="settings-error" role="alert">
572
+ Could not save settings. Try again.
573
+ </p>
574
+ )}
575
+ {!agentSessionsEnabled.value &&
576
+ (attachedSessions.value.length > 0 ||
577
+ pendingGates.value.length > 0 ||
578
+ heldGates.value.length > 0 ||
579
+ scopeFence.value) && (
580
+ <SettingsItem
581
+ label="Agent controls"
582
+ description="Review pending approvals, inspect or clear the write scope, or end an existing attachment without enabling the session beta."
583
+ onClick={choose(() => (sessionPanelOpened.value = true))}
584
+ />
585
+ )}
586
+ <div class="toolbar-menu-divider" role="separator" />
587
+ {!isHostedWorkbench() && !browsingMemory.value && (
588
+ <>
589
+ <SettingsItem
590
+ label="Trace"
591
+ description="Show execution traces sent by connected tools. Turning off keeps existing traces. This does not start an agent."
592
+ toggle
593
+ checked={isTraceOn}
594
+ onClick={() => sendIntent('trace-toggle', { enabled: !isTraceOn })}
595
+ />
596
+ {(isTraceOn || traceNodeCount > 0) && (
597
+ <SettingsItem
598
+ label="Clear trace"
599
+ description="Remove recorded execution traces from this board without changing whether trace capture is enabled."
600
+ onClick={choose(() => sendIntent('trace-clear'))}
601
+ />
602
+ )}
603
+ </>
604
+ )}
605
+ <SettingsItem
606
+ label="Send feedback"
607
+ description="Report a problem or suggestion. Review the feedback before sending."
608
+ onClick={choose(() => setFeedbackOpen(true))}
609
+ />
610
+ </div>
611
+ )}
437
612
  {openMenu === 'theme' && (
438
613
  <div class="toolbar-menu" style={sideMenuStyle} role="menu" aria-label="Theme">
614
+ <button type="button" role="menuitem" class="toolbar-menu-item" onClick={() => setOpenMenu('settings')}>
615
+ <span>‹ Settings</span>
616
+ </button>
439
617
  {CANVAS_THEMES.map((name) => (
440
618
  <button
441
619
  key={name}
@@ -459,9 +637,6 @@ export function ToolRail({
459
637
  </div>
460
638
  )}
461
639
  </span>
462
- <RailButton label="Shortcuts" shortcut="?" onClick={onOpenShortcuts}>
463
- <IconShortcuts />
464
- </RailButton>
465
640
  </div>
466
641
  {feedbackOpen && <FeedbackDialog onClose={() => setFeedbackOpen(false)} />}
467
642
  </>