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,20 +1,34 @@
1
- import { useEffect, useRef, useState } from 'preact/hooks';
2
- import type { ComponentChildren } from 'preact';
1
+ import { timeAgo } from '../utils/time-ago';
2
+ import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
3
+ import { isSelectionEscape } from '../utils/keyboard';
3
4
  import {
4
5
  boardList,
5
6
  activeBoardId,
6
7
  createAndOpenBoard,
7
8
  deleteBoard,
8
9
  openBoard,
10
+ openMemoryTarget,
11
+ setBoardPinned,
9
12
  updateBoard,
10
13
  type BoardSummary,
11
14
  } from '../state/boards-store';
12
- import { categoryAncestors, MIGRATED_SNAPSHOT_CATEGORY } from '../../shared/boards.js';
15
+ import { folderAncestors, MIGRATED_SNAPSHOT_FOLDER, normalizeBoardFolder } from '../../shared/boards.js';
13
16
  import { askText } from './TextPrompt';
14
17
  import { BoardFolderDialog } from './BoardFolderDialog';
15
18
  import { requestJson } from '../state/intent-bridge';
16
- import { focusNode, nodes } from '../state/canvas-store';
19
+ import { approxTokens } from '../state/context-chip-store';
17
20
  import { BoardCopyDialog } from './BoardCopyDialog';
21
+ import { BoardPinButton } from './BoardPinButton';
22
+ import { HomeMapView } from './HomeMapView';
23
+ import { HomeGraphView } from './HomeGraphView';
24
+ import { HomeReviewView } from './HomeReviewView';
25
+ import { KnowledgeDialog } from './KnowledgeDialog';
26
+ import { RelationPresetsDialog } from './RelationPresetsDialog';
27
+ import { knowledge, lifecycleFilter } from '../state/knowledge-store';
28
+ import { knowledgeLifecycleEnabled } from '../state/workspace-settings';
29
+ import { lifecycleUiGeneration } from '../state/boards-store';
30
+ import type { LifecycleQualification } from '../../shared/lifecycle-status.js';
31
+ import { IconChevronDown, IconChevronRight, IconMore, IconPin, IconSearch } from '../icons';
18
32
 
19
33
  interface LibraryHit {
20
34
  boardId: string;
@@ -22,32 +36,32 @@ interface LibraryHit {
22
36
  cardId: string;
23
37
  title: string | null;
24
38
  snippet: string;
39
+ qualification?: LifecycleQualification;
25
40
  }
26
41
 
27
- function timeAgo(iso: string | null): string {
42
+ function openedAgo(iso: string | null): string {
28
43
  if (!iso) return 'never opened';
29
- const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
30
- if (mins < 1) return 'just now';
31
- if (mins < 60) return `${mins}m ago`;
32
- const hrs = Math.floor(mins / 60);
33
- if (hrs < 24) return `${hrs}h ago`;
34
- const days = Math.floor(hrs / 24);
35
- if (days < 30) return `${days}d ago`;
36
- return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
44
+ const days = (Date.now() - new Date(iso).getTime()) / 86_400_000;
45
+ if (days >= 30)
46
+ return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
47
+ return timeAgo(iso);
37
48
  }
38
49
 
39
- function nodesLabel(count: number): string {
40
- return `${count} node${count !== 1 ? 's' : ''}`;
50
+ const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? '' : 's'}`;
51
+
52
+ /** Board-to-board links in either direction: the "↔ N links" a card shows. */
53
+ function linkCount(board: BoardSummary): number {
54
+ return (board.links?.length ?? 0) + (board.backlinks?.length ?? 0);
41
55
  }
42
56
 
43
- export async function promptNewBoard(): Promise<void> {
57
+ export async function promptNewBoard(folder?: string | null): Promise<void> {
44
58
  const name = await askText('New board', 'Board name', { confirm: 'Create' });
45
- if (name) await createAndOpenBoard(name);
59
+ if (name) await createAndOpenBoard(name, folder);
46
60
  }
47
61
 
48
62
  const COLLAPSED_KEY = 'pmx-canvas-home-collapsed';
49
63
 
50
- /** Which categories this viewer folded away; the migrated-snapshot shelf starts folded. */
64
+ /** Which folders this viewer folded away in the tree; the migrated-snapshot shelf starts folded. */
51
65
  function readCollapsed(): Set<string> {
52
66
  try {
53
67
  const raw = localStorage.getItem(COLLAPSED_KEY);
@@ -55,7 +69,7 @@ function readCollapsed(): Set<string> {
55
69
  } catch {
56
70
  // Storage can be unavailable (private windows, embedded panes): fall back to the default.
57
71
  }
58
- return new Set([MIGRATED_SNAPSHOT_CATEGORY]);
72
+ return new Set([MIGRATED_SNAPSHOT_FOLDER]);
59
73
  }
60
74
 
61
75
  function writeCollapsed(collapsed: Set<string>): void {
@@ -66,30 +80,117 @@ function writeCollapsed(collapsed: Set<string>): void {
66
80
  }
67
81
  }
68
82
 
83
+ const inFolder = (board: BoardSummary, path: string) => board.folder === path || !!board.folder?.startsWith(`${path}/`);
84
+
85
+ const leaf = (path: string) => path.split('/').at(-1) ?? path;
86
+
87
+ /** A board's layout in miniature: its cards as rectangles, pinned ones in pin blue. */
88
+ function BoardThumbnail({ board }: { board: BoardSummary }) {
89
+ return (
90
+ <div class="home-thumb" aria-hidden="true">
91
+ <div class="home-thumb-inner">
92
+ {(board.preview ?? []).map((rect, index) => (
93
+ <span
94
+ key={index}
95
+ class={`home-thumb-card${rect.pinned ? ' is-pinned' : ''}`}
96
+ style={{
97
+ left: `${rect.x * 100}%`,
98
+ top: `${rect.y * 100}%`,
99
+ width: `${rect.w * 100}%`,
100
+ height: `${rect.h * 100}%`,
101
+ }}
102
+ />
103
+ ))}
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+
69
109
  /**
70
- * Home (plan 012): the view when no board is open — every board, filed under
71
- * its category (most recently opened first within each). Categories keep the
72
- * library findable as it grows into the working memory of many sessions.
73
- * Deleting asks in place, naming the board and what it holds; browser confirm
74
- * dialogs are no-ops in agent panes.
110
+ * Home (docs/design/Home.dc.html, plan 016): the library when no board is
111
+ * open. A folder tree with pinned boards on the left, the selected folder's
112
+ * subfolders and board cards in the middle, the selected board's details on
113
+ * the right. At ≤ 760 px (Pane600.dc.html) the tree becomes a folder picker,
114
+ * the boards a list and the details a bottom sheet. Folders are board
115
+ * folder paths; folders carry no colour (decided 2026-10-07). Browser
116
+ * confirm dialogs are no-ops in agent panes, so Delete asks in place.
75
117
  */
76
118
  export function HomeView() {
77
119
  const boards = boardList.value;
78
- const [confirmingDelete, setConfirmingDelete] = useState<string | null>(null);
120
+ const enabled = knowledgeLifecycleEnabled.value;
121
+ const [selectedView, setView] = useState<'Folders' | 'Map' | 'Graph' | 'Review'>('Folders');
122
+ const view = !enabled && selectedView === 'Review' ? 'Folders' : selectedView;
123
+ const [folder, setFolder] = useState<string>(() => {
124
+ const recent = [...boardList.value].sort((a, b) => (b.lastOpenedAt ?? '').localeCompare(a.lastOpenedAt ?? ''))[0];
125
+ return recent?.folder ?? '';
126
+ });
127
+ const [extraFolders, setExtraFolders] = useState<string[]>([]);
128
+ const [knowledgeDialog, setKnowledgeDialog] = useState<string | null>(null);
129
+ const [relationPresetsOpen, setRelationPresetsOpen] = useState(false);
130
+ const [selectedId, setSelectedId] = useState<string | null>(null);
131
+ const [menuOpen, setMenuOpen] = useState(false);
132
+ const [confirmingDelete, setConfirmingDelete] = useState(false);
79
133
  const [moving, setMoving] = useState<string | null>(null);
80
134
  const [copying, setCopying] = useState<string | null>(null);
81
135
  const [search, setSearch] = useState('');
82
136
  const [hits, setHits] = useState<LibraryHit[]>([]);
83
137
  const [collapsed, setCollapsed] = useState<Set<string>>(readCollapsed);
84
- const openHitSequence = useRef(0);
138
+ const [pickerOpen, setPickerOpen] = useState(false);
139
+ const [pinsOpen, setPinsOpen] = useState(false);
140
+ const moreButton = useRef<HTMLButtonElement>(null);
141
+ const moreMenu = useRef<HTMLSpanElement>(null);
142
+
85
143
  const folders = [
86
- ...new Set(boards.flatMap((board) => (board.category ? categoryAncestors(board.category) : []))),
144
+ ...new Set([
145
+ ...boards.flatMap((board) => (board.folder ? folderAncestors(board.folder) : [])),
146
+ ...(knowledge.value?.folders ?? []).flatMap((item) => folderAncestors(item.path)),
147
+ ...extraFolders.flatMap(folderAncestors),
148
+ ]),
87
149
  ].sort((a, b) => a.localeCompare(b));
150
+ const childrenOf = (path: string) =>
151
+ folders.filter((candidate) =>
152
+ path
153
+ ? candidate.startsWith(`${path}/`) && !candidate.slice(path.length + 1).includes('/')
154
+ : !candidate.includes('/'),
155
+ );
88
156
  const query = search.trim().toLocaleLowerCase();
89
- const visible = boards.filter((board) => `${board.name} ${board.category ?? ''}`.toLocaleLowerCase().includes(query));
90
- const visibleFolders = new Set(visible.flatMap((board) => (board.category ? categoryAncestors(board.category) : [])));
157
+ const pinned = boards.filter((board) => board.pin);
158
+ const pinnedChars = pinned.reduce((sum, board) => sum + (board.mapChars ?? 0), 0);
159
+ const subfolders = childrenOf(folder);
160
+ const here = boards
161
+ .filter((board) => (board.folder ?? '') === folder)
162
+ .sort((a, b) => (b.lastOpenedAt ?? '').localeCompare(a.lastOpenedAt ?? ''));
163
+ const matches = query
164
+ ? boards.filter((board) => `${board.name} ${board.folder ?? ''}`.toLocaleLowerCase().includes(query))
165
+ : [];
166
+ const selected = boards.find((board) => board.id === selectedId) ?? null;
91
167
  const movingBoard = boards.find((board) => board.id === moving);
92
168
  const copyingBoard = boards.find((board) => board.id === copying);
169
+ const reviewBoardIds = new Set(boards.filter((board) => !folder || inFolder(board, folder)).map((board) => board.id));
170
+ const reviewCards = Object.entries(knowledge.value?.cards ?? {})
171
+ .filter(([key]) => reviewBoardIds.has((JSON.parse(key) as [string, string])[0]))
172
+ .map(([, status]) => status);
173
+ const reviewNeeded = reviewCards.filter((status) => !status.retired && (status.due || status.sources.length)).length;
174
+ const withoutPolicy = reviewCards.filter((status) => status.unknown).length;
175
+ const folderCategory = knowledge.value?.resolvedFolders[folder]?.category;
176
+ const reviewTargets = new Map<string, string[]>();
177
+ for (const [key, status] of Object.entries(knowledge.value?.cards ?? {})) {
178
+ if (status.retired || !(status.due || status.sources.length)) continue;
179
+ const [boardId, nodeId] = JSON.parse(key) as [string, string];
180
+ const targets = reviewTargets.get(boardId) ?? [];
181
+ targets.push(nodeId);
182
+ reviewTargets.set(boardId, targets);
183
+ }
184
+ const shown = query ? matches : here;
185
+ const reviewBoard = async (id: string) => {
186
+ if (!knowledgeLifecycleEnabled.value) return;
187
+ const generation = lifecycleUiGeneration;
188
+ const cardId = reviewTargets.get(id)?.[0];
189
+ if (!cardId) return;
190
+ const opened = await openMemoryTarget({ boardId: id, cardId });
191
+ if (opened && knowledgeLifecycleEnabled.value && generation === lifecycleUiGeneration && activeBoardId.value === id)
192
+ lifecycleFilter.value = 'To review';
193
+ };
93
194
 
94
195
  useEffect(() => {
95
196
  const q = search.trim();
@@ -113,165 +214,591 @@ export function HomeView() {
113
214
  };
114
215
  }, [search]);
115
216
 
116
- const openHit = async (hit: LibraryHit) => {
117
- const sequence = ++openHitSequence.current;
118
- await openBoard(hit.boardId);
119
- for (let attempt = 0; attempt < 40; attempt++) {
120
- if (sequence !== openHitSequence.current || activeBoardId.value !== hit.boardId) return;
121
- if (nodes.value.has(hit.cardId)) {
122
- focusNode(hit.cardId);
123
- return;
124
- }
125
- await new Promise((resolve) => window.setTimeout(resolve, 25));
126
- }
127
- };
217
+ // The ⋯ menu takes focus on its first item when it opens.
218
+ useEffect(() => {
219
+ if (menuOpen) moreMenu.current?.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
220
+ }, [menuOpen]);
128
221
 
129
- const toggle = (name: string) => {
222
+ // A selection that left this folder (moved, deleted) closes the details.
223
+ useEffect(() => {
224
+ if (selectedId && !boards.some((board) => board.id === selectedId)) setSelectedId(null);
225
+ }, [boards, selectedId]);
226
+
227
+ useLayoutEffect(() => {
228
+ if (view !== 'Folders' || !selectedId) return;
229
+ const onKey = (event: KeyboardEvent) => {
230
+ if (!isSelectionEscape(event)) return;
231
+ event.preventDefault();
232
+ setSelectedId(null);
233
+ setConfirmingDelete(false);
234
+ };
235
+ window.addEventListener('keydown', onKey);
236
+ return () => window.removeEventListener('keydown', onKey);
237
+ }, [view, selectedId]);
238
+
239
+ const openHit = (hit: LibraryHit) => openMemoryTarget(hit);
240
+
241
+ const toggle = (path: string) => {
130
242
  const next = new Set(collapsed);
131
- if (next.has(name)) next.delete(name);
132
- else next.add(name);
243
+ if (next.has(path)) next.delete(path);
244
+ else next.add(path);
133
245
  setCollapsed(next);
134
246
  writeCollapsed(next);
135
247
  };
136
248
 
249
+ const goTo = (path: string) => {
250
+ setFolder(path);
251
+ setSearch('');
252
+ setSelectedId(null);
253
+ setPickerOpen(false);
254
+ };
255
+
256
+ const select = (id: string | null) => {
257
+ setSelectedId(id);
258
+ setMenuOpen(false);
259
+ setConfirmingDelete(false);
260
+ };
261
+
262
+ const newFolder = async () => {
263
+ const name = await askText('New folder', 'Folder name', { confirm: 'Create' });
264
+ if (!name) return;
265
+ let path: string | null;
266
+ try {
267
+ path = normalizeBoardFolder(folder ? `${folder}/${name}` : name);
268
+ } catch {
269
+ return;
270
+ }
271
+ if (!path) return;
272
+ // A folder exists once a board is filed in it; until then it lives in this view.
273
+ setExtraFolders((list) => [...list, path]);
274
+ goTo(path);
275
+ };
276
+
137
277
  const rename = async (board: BoardSummary) => {
138
278
  const name = await askText('Rename board', 'Board name', { initial: board.name, confirm: 'Rename' });
139
279
  if (name && name !== board.name) await updateBoard(board.id, { name });
140
280
  };
141
281
 
142
- const row = (board: BoardSummary) =>
143
- confirmingDelete === board.id ? (
144
- <li key={board.id} class="home-board is-confirming" data-testid="home-board">
145
- <div class="home-board-confirm-text">
146
- Delete <strong>{board.name}</strong> and its {nodesLabel(board.nodeCount)} and snapshots? This can't be
147
- undone.
148
- </div>
149
- <div class="home-board-actions">
150
- <button type="button" class="home-board-action" onClick={() => setConfirmingDelete(null)}>
151
- Cancel
152
- </button>
153
- <button
154
- type="button"
155
- class="home-board-action is-danger"
156
- onClick={() => {
157
- setConfirmingDelete(null);
158
- void deleteBoard(board.id);
159
- }}
160
- >
161
- Delete board
282
+ const treeRow = (path: string, depth: number) => {
283
+ const kids = childrenOf(path);
284
+ const open = !collapsed.has(path);
285
+ const count = boards.filter((board) => inFolder(board, path)).length;
286
+ const hasPin = boards.some((board) => board.pin && inFolder(board, path));
287
+ return (
288
+ <li key={path}>
289
+ <div class={`home-tree-row${folder === path && !query ? ' is-selected' : ''}`} data-folder={path}>
290
+ <span class="home-tree-indent" style={{ width: `${depth * 16}px` }} />
291
+ {kids.length > 0 ? (
292
+ <button
293
+ type="button"
294
+ class="home-tree-chevron"
295
+ aria-label={open ? `Fold ${leaf(path)}` : `Unfold ${leaf(path)}`}
296
+ aria-expanded={open}
297
+ onClick={() => toggle(path)}
298
+ >
299
+ {open ? <IconChevronDown /> : <IconChevronRight />}
300
+ </button>
301
+ ) : (
302
+ <span class="home-tree-chevron" />
303
+ )}
304
+ <button type="button" class="home-tree-name" onClick={() => goTo(path)}>
305
+ {leaf(path)}
162
306
  </button>
307
+ {hasPin && <span class="home-tree-pin" aria-label="Has a board in the agent's context" />}
308
+ <span class="home-tree-count">{count}</span>
163
309
  </div>
310
+ {open && kids.length > 0 && <ul class="home-tree">{kids.map((kid) => treeRow(kid, depth + 1))}</ul>}
164
311
  </li>
165
- ) : (
166
- <li key={board.id} class="home-board" data-testid="home-board">
167
- <button type="button" class="home-board-open" onClick={() => void openBoard(board.id)}>
168
- <span class="home-board-name">{board.name}</span>
169
- <span class="home-board-meta">
170
- {nodesLabel(board.nodeCount)} · {timeAgo(board.lastOpenedAt)}
171
- </span>
172
- </button>
173
- <div class="home-board-actions">
174
- <button type="button" class="home-board-action" aria-haspopup="dialog" onClick={() => setMoving(board.id)}>
175
- Move
176
- </button>
177
- <button type="button" class="home-board-action" aria-haspopup="dialog" onClick={() => setCopying(board.id)}>
178
- Create from…
179
- </button>
180
- <button type="button" class="home-board-action" onClick={() => void rename(board)}>
181
- Rename
182
- </button>
183
- <button type="button" class="home-board-action" onClick={() => setConfirmingDelete(board.id)}>
184
- Delete
312
+ );
313
+ };
314
+
315
+ const tree = (
316
+ <ul class="home-tree" aria-label="Folders">
317
+ {childrenOf('').map((path) => treeRow(path, 0))}
318
+ <li>
319
+ <div class={`home-tree-row is-unfiled${folder === '' && !query ? ' is-selected' : ''}`}>
320
+ <span class="home-tree-chevron" />
321
+ <button type="button" class="home-tree-name" onClick={() => goTo('')}>
322
+ Unfiled
185
323
  </button>
324
+ <span class="home-tree-count">{boards.filter((board) => !board.folder).length}</span>
186
325
  </div>
187
326
  </li>
188
- );
327
+ </ul>
328
+ );
189
329
 
190
- const folder = (path: string, depth: number): ComponentChildren => {
191
- const children = folders.filter(
192
- (candidate) => candidate.slice(0, candidate.lastIndexOf('/')) === path && candidate.includes('/'),
193
- );
194
- const direct = visible.filter((board) => board.category === path);
195
- const count = visible.filter((board) => board.category === path || board.category?.startsWith(`${path}/`)).length;
196
- const expanded = !!query || !collapsed.has(path);
197
- return (
198
- <section key={path} class="home-section" data-testid="home-section" data-folder={path} aria-label={path}>
199
- <button type="button" class="home-section-head" aria-expanded={expanded} onClick={() => toggle(path)}>
200
- <svg class="home-section-chevron" viewBox="0 0 20 20" width="18" height="18" fill="none" aria-hidden="true">
201
- <path d="m7.5 4.75 5.25 5.25-5.25 5.25" />
202
- </svg>
203
- <svg class="home-section-folder" viewBox="0 0 20 20" width="18" height="18" fill="none" aria-hidden="true">
204
- <path d="M2.75 5.75A1.75 1.75 0 0 1 4.5 4h3l1.75 2h6.25a1.75 1.75 0 0 1 1.75 1.75v6.5A1.75 1.75 0 0 1 15.5 16h-11a1.75 1.75 0 0 1-1.75-1.75v-8.5Z" />
205
- </svg>
206
- <span class="home-section-name">{path.split('/').at(-1)}</span>
207
- <span class="home-section-count">{count}</span>
330
+ const pinnedList = (
331
+ <ul class="home-pinned" aria-label="Pinned boards">
332
+ {pinned.map((board) => (
333
+ <li key={board.id}>
334
+ <button type="button" class="home-pinned-row" onClick={() => goTo(board.folder ?? '')}>
335
+ <span class="home-pin-dot" aria-hidden="true">
336
+ <IconPin />
337
+ </span>
338
+ <span class="home-pinned-name">{board.name}</span>
339
+ <span class="home-tree-count">{board.folder ? leaf(board.folder) : 'Unfiled'}</span>
340
+ </button>
341
+ </li>
342
+ ))}
343
+ </ul>
344
+ );
345
+
346
+ const searchField = (
347
+ <label class="home-search">
348
+ <IconSearch />
349
+ <span class="sr-only">Find boards, cards and folders</span>
350
+ <input
351
+ type="search"
352
+ placeholder="Boards, cards, folders…"
353
+ value={search}
354
+ onInput={(event) => setSearch(event.currentTarget.value)}
355
+ />
356
+ </label>
357
+ );
358
+
359
+ const card = (board: BoardSummary) => (
360
+ <li key={board.id}>
361
+ <article
362
+ class={`home-card${board.pin ? ' is-pinned' : ''}${selectedId === board.id ? ' is-selected' : ''}`}
363
+ data-testid="home-board"
364
+ data-board-id={board.id}
365
+ >
366
+ <button
367
+ type="button"
368
+ class="home-card-select"
369
+ aria-label={board.name}
370
+ aria-pressed={selectedId === board.id}
371
+ onClick={() => select(board.id)}
372
+ onDblClick={() => void openBoard(board.id)}
373
+ >
374
+ <BoardThumbnail board={board} />
375
+ <span class="home-card-body">
376
+ <span class="home-card-name">{board.name}</span>
377
+ <span class="home-card-readme">{board.summary ?? 'No README yet.'}</span>
378
+ <span class="home-card-meta">
379
+ <span>{plural(board.nodeCount, 'card')}</span>
380
+ {linkCount(board) > 0 && <span>↔ {plural(linkCount(board), 'link')}</span>}
381
+ <span class="home-card-when">{openedAgo(board.lastOpenedAt)}</span>
382
+ </span>
383
+ </span>
208
384
  </button>
209
- {expanded && (
210
- <div class="home-folder-children" style={{ marginLeft: depth < 5 ? '14px' : '0' }}>
211
- {children.filter((child) => visibleFolders.has(child)).map((child) => folder(child, depth + 1))}
212
- {direct.length > 0 && (
213
- <ul class="home-view-list" aria-label={`Boards in ${path}`}>
214
- {direct.map(row)}
215
- </ul>
216
- )}
217
- </div>
385
+ {enabled && reviewTargets.has(board.id) && (
386
+ <button
387
+ type="button"
388
+ class="home-card-review"
389
+ aria-label={`Review ${board.name}`}
390
+ onClick={() => void reviewBoard(board.id)}
391
+ >
392
+ ⧗ Review {plural(reviewTargets.get(board.id)!.length, 'card')}
393
+ </button>
218
394
  )}
219
- </section>
220
- );
221
- };
395
+ <BoardPinButton board={board} />
396
+ </article>
397
+ </li>
398
+ );
222
399
 
223
- return (
224
- <div class="home-view" data-testid="home-view">
225
- <div class="home-view-inner">
226
- <div class="home-view-head">
227
- <h1 class="home-view-title">Boards</h1>
228
- <button type="button" class="home-view-new" onClick={() => void promptNewBoard()}>
229
- New board
400
+ const details = selected && (
401
+ <aside class="home-details" aria-label="Board details" data-testid="home-details">
402
+ <button type="button" class="home-details-grip" aria-label="Close details" onClick={() => select(null)} />
403
+ <div class="home-details-head">
404
+ <span class="home-details-folder">{selected.folder ? leaf(selected.folder) : 'Unfiled'}</span>
405
+ <span class="home-details-name">{selected.name}</span>
406
+ </div>
407
+ <div class={`home-details-context${selected.pin ? ' is-on' : ''}`}>
408
+ <span class="home-details-context-text">
409
+ <span class="home-details-context-label">
410
+ {selected.pin ? "In the agent's context" : "Not in the agent's context"}
411
+ </span>
412
+ </span>
413
+ <button
414
+ type="button"
415
+ role="switch"
416
+ aria-checked={!!selected.pin}
417
+ aria-label="In the agent's context"
418
+ class="home-switch"
419
+ onClick={() => void setBoardPinned(selected.id, !selected.pin)}
420
+ >
421
+ <span class="home-switch-knob" />
422
+ </button>
423
+ </div>
424
+ {confirmingDelete ? (
425
+ <div class="home-details-confirm" role="alert">
426
+ <span>
427
+ Delete <strong>{selected.name}</strong> and its {plural(selected.nodeCount, 'card')} and snapshots? This
428
+ can't be undone.
429
+ </span>
430
+ <span class="home-details-actions">
431
+ <button type="button" class="home-button" onClick={() => setConfirmingDelete(false)}>
432
+ Cancel
433
+ </button>
434
+ <button
435
+ type="button"
436
+ class="home-button is-danger"
437
+ onClick={() => {
438
+ setConfirmingDelete(false);
439
+ void deleteBoard(selected.id);
440
+ }}
441
+ >
442
+ Delete board
443
+ </button>
444
+ </span>
445
+ </div>
446
+ ) : (
447
+ <div class="home-details-actions">
448
+ <button type="button" class="home-button is-primary" onClick={() => void openBoard(selected.id)}>
449
+ Open board
230
450
  </button>
451
+ {enabled && reviewTargets.has(selected.id) && (
452
+ <button type="button" class="home-button" onClick={() => void reviewBoard(selected.id)}>
453
+ Review cards
454
+ </button>
455
+ )}
456
+ <span class="home-more">
457
+ <button
458
+ ref={moreButton}
459
+ type="button"
460
+ class="home-button is-icon"
461
+ aria-label="More: move, rename, create from, delete"
462
+ aria-haspopup="menu"
463
+ aria-expanded={menuOpen}
464
+ onClick={() => setMenuOpen(!menuOpen)}
465
+ >
466
+ <IconMore />
467
+ </button>
468
+ {menuOpen && (
469
+ <span
470
+ ref={moreMenu}
471
+ class="home-more-menu"
472
+ role="menu"
473
+ onKeyDown={(event) => {
474
+ const items = [...(moreMenu.current?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [])];
475
+ const at = items.indexOf(document.activeElement as HTMLElement);
476
+ if (event.key === 'Escape') {
477
+ event.preventDefault();
478
+ setMenuOpen(false);
479
+ moreButton.current?.focus();
480
+ } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
481
+ event.preventDefault();
482
+ const step = event.key === 'ArrowDown' ? 1 : -1;
483
+ items[(at + step + items.length) % items.length]?.focus();
484
+ }
485
+ }}
486
+ >
487
+ {(
488
+ [
489
+ ['Move…', () => setMoving(selected.id)],
490
+ ['Rename…', () => void rename(selected)],
491
+ ['Create from…', () => setCopying(selected.id)],
492
+ ['Delete…', () => setConfirmingDelete(true)],
493
+ ] as const
494
+ ).map(([label, run]) => (
495
+ <button
496
+ key={label}
497
+ type="button"
498
+ role="menuitem"
499
+ class="home-more-item"
500
+ onClick={() => {
501
+ setMenuOpen(false);
502
+ run();
503
+ }}
504
+ >
505
+ {label}
506
+ </button>
507
+ ))}
508
+ </span>
509
+ )}
510
+ </span>
231
511
  </div>
232
- <input
233
- class="text-prompt-input home-search"
234
- type="search"
235
- aria-label="Find boards and folders"
236
- placeholder="Find boards and folders…"
237
- value={search}
238
- onInput={(event) => setSearch(event.currentTarget.value)}
512
+ )}
513
+ </aside>
514
+ );
515
+
516
+ return (
517
+ <div class="home-shell">
518
+ {knowledgeDialog !== null && (
519
+ <KnowledgeDialog
520
+ key={knowledgeDialog}
521
+ folder={knowledgeDialog || undefined}
522
+ onClose={() => setKnowledgeDialog(null)}
239
523
  />
240
- {boards.length === 0 ? (
241
- <div class="home-view-empty">No boards yet. Create one, or let an agent start writing — it opens one.</div>
242
- ) : query && hits.length > 0 ? (
243
- <ul class="home-view-list" aria-label="Library search results">
244
- {hits.map((hit) => (
245
- <li class="home-board" key={`${hit.boardId}:${hit.cardId}`}>
246
- <button type="button" class="home-board-open" onClick={() => void openHit(hit)}>
247
- <span class="home-board-name">{hit.title || hit.boardTitle}</span>
248
- <span class="home-board-meta">
249
- {hit.boardTitle} · {hit.snippet}
250
- </span>
251
- </button>
252
- </li>
253
- ))}
254
- </ul>
255
- ) : visible.length === 0 ? (
256
- <div class="home-view-empty" role="status">
257
- No boards match “{search}”.
258
- </div>
524
+ )}
525
+ {relationPresetsOpen && <RelationPresetsDialog onClose={() => setRelationPresetsOpen(false)} />}
526
+ <nav class="home-view-tabs" aria-label="Library view">
527
+ {(['Folders', 'Map', 'Graph', 'Review'] as const)
528
+ .filter((tab) => enabled || tab !== 'Review')
529
+ .map((tab) => (
530
+ <button
531
+ type="button"
532
+ class="home-button"
533
+ aria-pressed={view === tab}
534
+ onClick={() => setView(tab)}
535
+ key={tab}
536
+ >
537
+ {tab}
538
+ </button>
539
+ ))}
540
+ <button type="button" class="home-button" onClick={() => setRelationPresetsOpen(true)}>
541
+ Relation presets
542
+ </button>
543
+ </nav>
544
+ {view === 'Map' || view === 'Graph' ? (
545
+ view === 'Map' ? (
546
+ <HomeMapView />
259
547
  ) : (
260
- <>
261
- {folders.filter((path) => !path.includes('/') && visibleFolders.has(path)).map((path) => folder(path, 0))}
262
- {visible.some((board) => !board.category) && (
263
- <section aria-label="Unfiled boards">
264
- {folders.length > 0 && <div class="home-folder-hint">Unfiled boards</div>}
265
- <ul class="home-view-list">{visible.filter((board) => !board.category).map(row)}</ul>
548
+ <HomeGraphView />
549
+ )
550
+ ) : (
551
+ <div class={`home-view${selected && view === 'Folders' ? ' has-details' : ''}`} data-testid="home-view">
552
+ <aside class={`home-library${pickerOpen ? ' is-open' : ''}`} aria-label="Library">
553
+ <div class="home-library-head">
554
+ <span class="home-library-title">Library</span>
555
+ <span class="home-tree-count">{plural(boards.length, 'board')}</span>
556
+ {enabled && (
557
+ <button
558
+ type="button"
559
+ class="home-button"
560
+ onClick={() => {
561
+ if (knowledgeLifecycleEnabled.value) setKnowledgeDialog('');
562
+ }}
563
+ >
564
+ Categories
565
+ </button>
566
+ )}
567
+ </div>
568
+ {searchField}
569
+ {pinned.length > 0 && (
570
+ <section class="home-library-section">
571
+ <span class="home-section-label">Pinned boards</span>
572
+ {pinnedList}
573
+ <span class="home-pinned-cost">
574
+ ≈ {approxTokens(pinnedChars)} tokens · {plural(pinned.length, 'board')}
575
+ </span>
266
576
  </section>
267
577
  )}
268
- </>
269
- )}
270
- </div>
271
- {movingBoard && (
272
- <BoardFolderDialog key={movingBoard.id} board={movingBoard} folders={folders} onClose={() => setMoving(null)} />
578
+ <section class="home-library-section is-folders">
579
+ <span class="home-section-label">Folders</span>
580
+ {tree}
581
+ <button type="button" class="home-tree-new" onClick={() => void newFolder()}>
582
+ + New folder
583
+ </button>
584
+ </section>
585
+ </aside>
586
+
587
+ <main class="home-main">
588
+ {/* Pane600.dc.html: at the narrow end the tree and the pins fold into these two rows. */}
589
+ <div class="home-narrow">
590
+ {searchField}
591
+ {pinned.length > 0 && (
592
+ <>
593
+ <button
594
+ type="button"
595
+ class="home-narrow-context"
596
+ aria-expanded={pinsOpen}
597
+ onClick={() => setPinsOpen(!pinsOpen)}
598
+ >
599
+ <span class="home-pin-dot" aria-hidden="true">
600
+ <IconPin />
601
+ </span>
602
+ <span class="home-narrow-context-label">In context: {plural(pinned.length, 'board')}</span>
603
+ <span class="home-narrow-context-cost">≈ {approxTokens(pinnedChars)} tokens</span>
604
+ <span>{pinsOpen ? 'Hide ▴' : 'Show ▾'}</span>
605
+ </button>
606
+ {pinsOpen && pinnedList}
607
+ </>
608
+ )}
609
+ <button
610
+ type="button"
611
+ class="home-narrow-picker"
612
+ aria-expanded={pickerOpen}
613
+ onClick={() => setPickerOpen(!pickerOpen)}
614
+ >
615
+ {folder ? leaf(folder) : 'Library'}
616
+ <span class="home-tree-count">
617
+ · {folder ? boards.filter((board) => inFolder(board, folder)).length : here.length} ▾
618
+ </span>
619
+ </button>
620
+ </div>
621
+
622
+ {view === 'Review' ? (
623
+ <HomeReviewView folder={folder} folders={folders} onScopeChange={goTo} />
624
+ ) : (
625
+ <>
626
+ <div class="home-main-head">
627
+ <div class="home-main-title">
628
+ <nav class="home-crumbs" aria-label="Folder path">
629
+ <button type="button" onClick={() => goTo('')}>
630
+ Library
631
+ </button>
632
+ {folder &&
633
+ folderAncestors(folder).map((path) => (
634
+ <span key={path}>
635
+ <span class="home-crumb-sep">/</span>
636
+ <button type="button" onClick={() => goTo(path)}>
637
+ {leaf(path)}
638
+ </button>
639
+ </span>
640
+ ))}
641
+ </nav>
642
+ <h1 class="home-view-title">
643
+ {query ? `Results for “${search.trim()}”` : folder ? leaf(folder) : 'Library'}
644
+ </h1>
645
+ </div>
646
+ {enabled && (
647
+ <button
648
+ type="button"
649
+ class="home-button"
650
+ onClick={() => {
651
+ if (knowledgeLifecycleEnabled.value) setKnowledgeDialog(folder);
652
+ }}
653
+ >
654
+ {' '}
655
+ {folder ? 'Folder settings' : 'Categories'}
656
+ </button>
657
+ )}
658
+ <button
659
+ type="button"
660
+ class="home-button is-primary"
661
+ onClick={() => void promptNewBoard(folder || null)}
662
+ >
663
+ {folder ? 'New board here' : 'New board'}
664
+ </button>
665
+ </div>
666
+
667
+ {enabled && knowledge.value && !query && (
668
+ <section class="home-main-section" aria-label="Folder review summary">
669
+ <span class="home-section-label">
670
+ Card review · {folder ? 'In this folder and subfolders' : 'Across the library'}
671
+ </span>
672
+ <div class="home-card-meta">
673
+ {folderCategory && (
674
+ <span>
675
+ Category: {folderCategory.glyph} {folderCategory.name}
676
+ </span>
677
+ )}
678
+ <button
679
+ type="button"
680
+ class={`home-review-link${reviewNeeded ? ' has-review' : ''}`}
681
+ onClick={() => {
682
+ setView('Review');
683
+ setSelectedId(null);
684
+ }}
685
+ >
686
+ Review needed: {plural(reviewNeeded, 'card')}
687
+ </button>
688
+ <span>No review policy: {plural(withoutPolicy, 'card')}</span>
689
+ </div>
690
+ </section>
691
+ )}
692
+
693
+ {boards.length === 0 ? (
694
+ <div class="home-view-empty">
695
+ No boards yet. Create one, or let an agent start writing — it opens one.
696
+ </div>
697
+ ) : query ? (
698
+ <>
699
+ {hits.length > 0 && (
700
+ <section class="home-main-section">
701
+ <span class="home-section-label">Cards · {hits.length}</span>
702
+ <ul class="home-hits" aria-label="Library search results">
703
+ {hits.map((hit) => (
704
+ <li key={`${hit.boardId}:${hit.cardId}`}>
705
+ <button type="button" class="home-hit" onClick={() => void openHit(hit)}>
706
+ <span class="home-card-name">{hit.title || hit.boardTitle}</span>
707
+ {enabled && hit.qualification?.retired && <span class="category-tag">Retired</span>}
708
+ <span class="home-card-meta">
709
+ {hit.boardTitle} · {hit.snippet}
710
+ </span>
711
+ </button>
712
+ </li>
713
+ ))}
714
+ </ul>
715
+ </section>
716
+ )}
717
+ {matches.length > 0 ? (
718
+ <section class="home-main-section">
719
+ <span class="home-section-label">Boards · {matches.length}</span>
720
+ <ul class="home-cards">{matches.map(card)}</ul>
721
+ </section>
722
+ ) : (
723
+ hits.length === 0 && (
724
+ <div class="home-view-empty" role="status">
725
+ No boards match “{search.trim()}”.
726
+ </div>
727
+ )
728
+ )}
729
+ </>
730
+ ) : (
731
+ <>
732
+ {subfolders.length > 0 && (
733
+ <section class="home-main-section">
734
+ <span class="home-section-label">Folders · {subfolders.length}</span>
735
+ <ul class="home-folders">
736
+ {subfolders.map((path) => {
737
+ const inside = boards.filter((board) => inFolder(board, path));
738
+ const latest = inside
739
+ .map((board) => board.lastOpenedAt ?? '')
740
+ .sort()
741
+ .at(-1);
742
+ const inContext = inside.filter((board) => board.pin).length;
743
+ return (
744
+ <li key={path}>
745
+ <button
746
+ type="button"
747
+ class="home-folder"
748
+ data-testid="home-section"
749
+ data-folder={path}
750
+ onClick={() => goTo(path)}
751
+ >
752
+ <span class="home-folder-stack" aria-hidden="true">
753
+ <span />
754
+ <span />
755
+ <span />
756
+ </span>
757
+ {inContext > 0 && <span class="home-folder-context">{inContext} in context</span>}
758
+ <span class="home-folder-name">{leaf(path)}</span>
759
+ <span class="home-folder-meta">
760
+ {plural(inside.length, 'board')}
761
+ {latest ? ` · opened ${openedAgo(latest)}` : ''}
762
+ </span>
763
+ </button>
764
+ </li>
765
+ );
766
+ })}
767
+ </ul>
768
+ </section>
769
+ )}
770
+ <section class="home-main-section">
771
+ <span class="home-section-label">
772
+ {folder ? `Boards in ${leaf(folder)}` : 'Unfiled boards'} · {shown.length}
773
+ </span>
774
+ {shown.length > 0 ? (
775
+ <ul class="home-cards" aria-label={folder ? `Boards in ${folder}` : 'Unfiled boards'}>
776
+ {shown.map(card)}
777
+ </ul>
778
+ ) : (
779
+ <div class="home-view-empty">No boards here yet.</div>
780
+ )}
781
+ </section>
782
+ </>
783
+ )}
784
+ </>
785
+ )}
786
+ </main>
787
+
788
+ {view === 'Folders' && details}
789
+ {movingBoard && (
790
+ <BoardFolderDialog
791
+ key={movingBoard.id}
792
+ board={movingBoard}
793
+ folders={folders}
794
+ onClose={() => setMoving(null)}
795
+ />
796
+ )}
797
+ {copyingBoard && (
798
+ <BoardCopyDialog key={copyingBoard.id} board={copyingBoard} onClose={() => setCopying(null)} />
799
+ )}
800
+ </div>
273
801
  )}
274
- {copyingBoard && <BoardCopyDialog key={copyingBoard.id} board={copyingBoard} onClose={() => setCopying(null)} />}
275
802
  </div>
276
803
  );
277
804
  }