pmx-canvas 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +66 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2759 -510
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2936 -632
  8. package/dist/json-render/index.css +81 -1
  9. package/dist/json-render/index.js +20 -20
  10. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  11. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  12. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  13. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  15. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  17. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  18. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  19. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  20. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  21. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  22. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  23. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  24. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  25. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  26. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  27. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  28. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  29. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  30. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  31. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  32. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  33. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  34. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  35. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  36. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  37. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  38. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  39. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  40. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  41. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  42. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  43. package/dist/types/client/icons.d.ts +0 -10
  44. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  45. package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
  46. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  47. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  48. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  49. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  50. package/dist/types/client/state/boards-store.d.ts +81 -9
  51. package/dist/types/client/state/canvas-store.d.ts +14 -1
  52. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  53. package/dist/types/client/state/context-status-store.d.ts +35 -0
  54. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  55. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  56. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  57. package/dist/types/client/state/presence-store.d.ts +4 -4
  58. package/dist/types/client/state/presentation.d.ts +28 -0
  59. package/dist/types/client/state/relation-store.d.ts +10 -0
  60. package/dist/types/client/state/session-store.d.ts +41 -2
  61. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  62. package/dist/types/client/types.d.ts +13 -1
  63. package/dist/types/client/utils/keyboard.d.ts +2 -0
  64. package/dist/types/client/utils/time-ago.d.ts +4 -0
  65. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  66. package/dist/types/server/agent-presence.d.ts +10 -3
  67. package/dist/types/server/ax-context.d.ts +1 -1
  68. package/dist/types/server/canvas-db.d.ts +30 -7
  69. package/dist/types/server/canvas-operations.d.ts +24 -2
  70. package/dist/types/server/canvas-provenance.d.ts +22 -2
  71. package/dist/types/server/canvas-serialization.d.ts +2 -0
  72. package/dist/types/server/canvas-state.d.ts +93 -15
  73. package/dist/types/server/canvas-validation.d.ts +1 -1
  74. package/dist/types/server/context-brief.d.ts +94 -6
  75. package/dist/types/server/context-reads.d.ts +33 -7
  76. package/dist/types/server/edit-summary.d.ts +20 -0
  77. package/dist/types/server/index.d.ts +71 -10
  78. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  79. package/dist/types/server/lifecycle-status.d.ts +10 -0
  80. package/dist/types/server/memory-graph.d.ts +9 -0
  81. package/dist/types/server/mutation-history.d.ts +2 -0
  82. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  83. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  84. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  85. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  86. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  87. package/dist/types/server/operations/ops/query.d.ts +23 -0
  88. package/dist/types/server/operations/registry.d.ts +1 -0
  89. package/dist/types/server/placement.d.ts +7 -0
  90. package/dist/types/server/scope-fence.d.ts +2 -1
  91. package/dist/types/server/spatial-analysis.d.ts +6 -3
  92. package/dist/types/server/tour-control.d.ts +32 -0
  93. package/dist/types/server/wiki-links.d.ts +14 -0
  94. package/dist/types/server/workbench-auth.d.ts +2 -0
  95. package/dist/types/shared/agent-context.d.ts +2 -0
  96. package/dist/types/shared/agent-presence.d.ts +15 -12
  97. package/dist/types/shared/auto-arrange.d.ts +1 -0
  98. package/dist/types/shared/boards.d.ts +4 -4
  99. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  100. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  101. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  102. package/dist/types/shared/memory-graph.d.ts +53 -0
  103. package/dist/types/shared/relations.d.ts +68 -0
  104. package/dist/types/shared/semantic-attention.d.ts +2 -0
  105. package/dist/types/shared/tour.d.ts +38 -0
  106. package/dist/types/shared/workspace-settings.d.ts +11 -0
  107. package/docs/RELEASE.md +21 -3
  108. package/docs/adr-001-bun-only-runtime.md +7 -1
  109. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  110. package/docs/api-reference.md +109 -0
  111. package/docs/ax-host-adapter-contract.md +1 -1
  112. package/docs/ax-state-contract.md +5 -4
  113. package/docs/cli.md +153 -15
  114. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  115. package/docs/design/AgentContext.dc.html +3 -3
  116. package/docs/design/BoardPins.dc.html +37 -22
  117. package/docs/design/Categories.dc.html +208 -0
  118. package/docs/design/CategoryEditor.dc.html +33 -0
  119. package/docs/design/Chrome.dc.html +55 -9
  120. package/docs/design/ContextChip.dc.html +224 -0
  121. package/docs/design/ContextChip600.dc.html +136 -0
  122. package/docs/design/EmptyState.dc.html +45 -0
  123. package/docs/design/Flows.dc.html +23 -1
  124. package/docs/design/HeaderMarks.dc.html +183 -0
  125. package/docs/design/Home.dc.html +19 -1
  126. package/docs/design/HomeGraph.dc.html +86 -202
  127. package/docs/design/HomeMap.dc.html +23 -1
  128. package/docs/design/HomeTeam.dc.html +32 -14
  129. package/docs/design/Lifecycle.dc.html +22 -0
  130. package/docs/design/Lifecycle600.dc.html +18 -0
  131. package/docs/design/LifecycleVariants.dc.html +157 -0
  132. package/docs/design/LinkAuthorship.dc.html +139 -0
  133. package/docs/design/LinksChanged.dc.html +203 -0
  134. package/docs/design/LinksOptions.dc.html +137 -0
  135. package/docs/design/LinksOptions600.dc.html +134 -0
  136. package/docs/design/NearPin.dc.html +9 -10
  137. package/docs/design/Pane600.dc.html +4 -3
  138. package/docs/design/Relations.dc.html +3 -2
  139. package/docs/design/Restore.dc.html +152 -0
  140. package/docs/design/SeeChange.dc.html +267 -0
  141. package/docs/design/Uses.dc.html +12 -6
  142. package/docs/design/canvas.json +146 -19
  143. package/docs/design.md +454 -18
  144. package/docs/environment.md +0 -1
  145. package/docs/evals/curation-effect.md +128 -0
  146. package/docs/http-api.md +465 -38
  147. package/docs/mcp.md +150 -19
  148. package/docs/node-types.md +58 -3
  149. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  150. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  151. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  152. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  153. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  154. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  155. package/docs/product-context-vision-2026-09.md +17 -1
  156. package/docs/product-vision-2026-09.md +1082 -43
  157. package/docs/product-vision-review-2026-09.md +13 -1
  158. package/docs/screenshots/workbench-live-dark.png +0 -0
  159. package/docs/screenshots/workbench-live-light.png +0 -0
  160. package/docs/sdk.md +62 -2
  161. package/package.json +1 -1
  162. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/plugin.json +1 -1
  164. package/plugins/pmx-canvas/runtime.json +1 -1
  165. package/plugins/pmx-canvas/tools.json +601 -22
  166. package/skills/pmx-canvas/SKILL.md +234 -43
  167. package/skills/pmx-canvas/evals/evals.json +62 -0
  168. package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
  169. package/skills/pmx-canvas/references/full-reference.md +120 -21
  170. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  171. package/skills/pmx-canvas/references/importing-context.md +132 -0
  172. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  173. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  174. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  175. package/src/cli/agent.ts +2 -0
  176. package/src/cli/commands/ax.ts +12 -0
  177. package/src/cli/commands/boards.ts +87 -11
  178. package/src/cli/commands/edges.ts +28 -0
  179. package/src/cli/commands/groups.ts +7 -0
  180. package/src/cli/commands/nodes.ts +4 -2
  181. package/src/cli/commands/query.ts +34 -2
  182. package/src/cli/commands/record.ts +199 -48
  183. package/src/cli/index.ts +7 -1
  184. package/src/client/App.tsx +177 -70
  185. package/src/client/canvas/AttentionHistory.tsx +0 -2
  186. package/src/client/canvas/BarHint.tsx +40 -2
  187. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  188. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  189. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  190. package/src/client/canvas/BoardPinButton.tsx +33 -0
  191. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  192. package/src/client/canvas/CanvasNode.tsx +360 -237
  193. package/src/client/canvas/CanvasViewport.tsx +45 -7
  194. package/src/client/canvas/CommandBar.tsx +29 -23
  195. package/src/client/canvas/CommandPalette.tsx +5 -12
  196. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  197. package/src/client/canvas/ContextChip.tsx +296 -0
  198. package/src/client/canvas/ContextMenu.tsx +63 -5
  199. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  200. package/src/client/canvas/EdgeLayer.tsx +339 -76
  201. package/src/client/canvas/EmptyState.tsx +33 -18
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  203. package/src/client/canvas/ExternalWriters.tsx +57 -42
  204. package/src/client/canvas/HomeGraphView.tsx +942 -0
  205. package/src/client/canvas/HomeMapView.tsx +641 -0
  206. package/src/client/canvas/HomeReviewView.tsx +167 -0
  207. package/src/client/canvas/HomeView.tsx +687 -160
  208. package/src/client/canvas/IntentLayer.tsx +19 -12
  209. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  210. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  211. package/src/client/canvas/LifecycleFields.tsx +91 -0
  212. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  213. package/src/client/canvas/MemoryInspector.tsx +104 -0
  214. package/src/client/canvas/Minimap.tsx +3 -3
  215. package/src/client/canvas/NearPinLayer.tsx +187 -0
  216. package/src/client/canvas/NodeContextMark.tsx +175 -97
  217. package/src/client/canvas/Presentation.tsx +23 -57
  218. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  219. package/src/client/canvas/RelationsPanel.tsx +82 -0
  220. package/src/client/canvas/SelectionBar.tsx +3 -6
  221. package/src/client/canvas/SessionPanel.tsx +90 -50
  222. package/src/client/canvas/SessionReceipt.tsx +311 -39
  223. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  224. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  225. package/src/client/canvas/ToolRail.tsx +367 -192
  226. package/src/client/canvas/TopBar.tsx +354 -124
  227. package/src/client/canvas/canvas-area.ts +10 -3
  228. package/src/client/canvas/graph-geometry.ts +195 -0
  229. package/src/client/canvas/graph-model.ts +254 -0
  230. package/src/client/canvas/memory-layout.ts +106 -0
  231. package/src/client/canvas/use-chrome-height.ts +20 -0
  232. package/src/client/canvas/use-focus-trap.ts +31 -5
  233. package/src/client/canvas/use-memory-graph.ts +58 -0
  234. package/src/client/canvas/use-node-drag.ts +2 -1
  235. package/src/client/canvas/use-pan-zoom.ts +64 -7
  236. package/src/client/icons.tsx +0 -63
  237. package/src/client/mcp-app/style.css +0 -6
  238. package/src/client/nodes/AxStepControls.tsx +74 -50
  239. package/src/client/nodes/BoardNode.tsx +5 -1
  240. package/src/client/nodes/ContextNode.tsx +18 -11
  241. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  242. package/src/client/nodes/FileNode.tsx +46 -42
  243. package/src/client/nodes/HtmlNode.tsx +44 -2
  244. package/src/client/nodes/ImageNode.tsx +1 -1
  245. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  246. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  247. package/src/client/nodes/MarkdownNode.tsx +377 -109
  248. package/src/client/nodes/McpAppNode.tsx +50 -20
  249. package/src/client/nodes/MdFormatBar.tsx +21 -18
  250. package/src/client/nodes/PromptNode.tsx +1 -1
  251. package/src/client/nodes/ResponseNode.tsx +1 -1
  252. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  253. package/src/client/state/attention-bridge.ts +64 -15
  254. package/src/client/state/boards-store.ts +193 -39
  255. package/src/client/state/canvas-store.ts +99 -12
  256. package/src/client/state/context-chip-store.ts +70 -0
  257. package/src/client/state/context-status-store.ts +53 -0
  258. package/src/client/state/intent-bridge.ts +51 -30
  259. package/src/client/state/knowledge-store.ts +43 -0
  260. package/src/client/state/near-pin-store.ts +115 -6
  261. package/src/client/state/presence-store.ts +7 -10
  262. package/src/client/state/presentation.ts +91 -0
  263. package/src/client/state/relation-store.ts +81 -0
  264. package/src/client/state/session-store.ts +144 -18
  265. package/src/client/state/sse-bridge.ts +206 -29
  266. package/src/client/state/workspace-settings.ts +11 -0
  267. package/src/client/theme/global.css +2759 -510
  268. package/src/client/types.ts +12 -0
  269. package/src/client/utils/keyboard.ts +10 -0
  270. package/src/client/utils/time-ago.ts +14 -0
  271. package/src/json-render/renderer/index.css +6 -1
  272. package/src/json-render/renderer/index.tsx +31 -1
  273. package/src/mcp/canvas-app.ts +12 -2
  274. package/src/mcp/context-uri-template.ts +7 -4
  275. package/src/mcp/server.ts +63 -41
  276. package/src/mcp/workbench-app.ts +39 -6
  277. package/src/server/agent-presence.ts +128 -28
  278. package/src/server/ax-context.ts +27 -13
  279. package/src/server/board-export.ts +12 -1
  280. package/src/server/canvas-db.ts +331 -42
  281. package/src/server/canvas-operations.ts +196 -84
  282. package/src/server/canvas-provenance.ts +53 -22
  283. package/src/server/canvas-serialization.ts +2 -0
  284. package/src/server/canvas-state.ts +950 -122
  285. package/src/server/canvas-validation.ts +9 -2
  286. package/src/server/context-brief.ts +345 -38
  287. package/src/server/context-reads.ts +179 -30
  288. package/src/server/demo-state.json +355 -24
  289. package/src/server/edit-summary.ts +74 -0
  290. package/src/server/html-surface.ts +8 -1
  291. package/src/server/index.ts +203 -13
  292. package/src/server/knowledge-lifecycle.ts +304 -0
  293. package/src/server/lifecycle-status.ts +232 -0
  294. package/src/server/memory-graph.ts +166 -0
  295. package/src/server/mutation-history.ts +18 -6
  296. package/src/server/operations/composites.ts +32 -11
  297. package/src/server/operations/http.ts +7 -2
  298. package/src/server/operations/index.ts +4 -0
  299. package/src/server/operations/mcp.ts +32 -1
  300. package/src/server/operations/ops/ax-presence.ts +1 -1
  301. package/src/server/operations/ops/ax-read.ts +57 -6
  302. package/src/server/operations/ops/ax-timeline.ts +28 -2
  303. package/src/server/operations/ops/backup.ts +5 -3
  304. package/src/server/operations/ops/batch.ts +35 -4
  305. package/src/server/operations/ops/boards.ts +188 -47
  306. package/src/server/operations/ops/canvas-wire.ts +31 -1
  307. package/src/server/operations/ops/edges.ts +36 -2
  308. package/src/server/operations/ops/groups.ts +6 -1
  309. package/src/server/operations/ops/json-render.ts +5 -1
  310. package/src/server/operations/ops/lifecycle.ts +262 -0
  311. package/src/server/operations/ops/memory-graph.ts +45 -0
  312. package/src/server/operations/ops/nodes.ts +107 -24
  313. package/src/server/operations/ops/query.ts +281 -68
  314. package/src/server/operations/ops/snapshots.ts +73 -1
  315. package/src/server/operations/ops/validate.ts +5 -1
  316. package/src/server/operations/ops/viewport.ts +97 -3
  317. package/src/server/operations/registry.ts +77 -26
  318. package/src/server/placement.ts +29 -0
  319. package/src/server/scope-fence.ts +78 -6
  320. package/src/server/server.ts +115 -16
  321. package/src/server/spatial-analysis.ts +48 -23
  322. package/src/server/tour-control.ts +82 -0
  323. package/src/server/wiki-links.ts +137 -0
  324. package/src/server/workbench-auth.ts +11 -0
  325. package/src/shared/agent-context.ts +3 -0
  326. package/src/shared/agent-presence.ts +15 -15
  327. package/src/shared/auto-arrange.ts +2 -0
  328. package/src/shared/boards.ts +4 -4
  329. package/src/shared/knowledge-lifecycle.ts +22 -0
  330. package/src/shared/knowledge-presets.ts +80 -0
  331. package/src/shared/lifecycle-status.ts +26 -0
  332. package/src/shared/memory-graph.ts +74 -0
  333. package/src/shared/relations.ts +98 -0
  334. package/src/shared/semantic-attention.ts +15 -1
  335. package/src/shared/tour.ts +23 -5
  336. package/src/shared/workspace-settings.ts +13 -0
  337. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  338. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  339. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  340. package/dist/types/server/agent-context.d.ts +0 -22
  341. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -0,0 +1,641 @@
1
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
2
+ import { memoryKey, type MemoryGraph, type MemoryGraphEdge, type MemoryGraphNode } from '../../shared/memory-graph.js';
3
+ import { relationDefinition, relationText } from '../../shared/relations.js';
4
+ import {
5
+ boardList,
6
+ browsingMemory,
7
+ catalogEpoch,
8
+ memoryGraphRevision,
9
+ openMemoryTarget,
10
+ updateBoard,
11
+ } from '../state/boards-store';
12
+ import { requestJson } from '../state/intent-bridge';
13
+ import { isSelectionEscape } from '../utils/keyboard';
14
+ import { BoardPinButton } from './BoardPinButton';
15
+ import { MemoryInspector } from './MemoryInspector';
16
+ import { MAP_CARD, mapLayout, type Point } from './memory-layout';
17
+ import { useMemoryGraph } from './use-memory-graph';
18
+
19
+ const linkNames = { portal: 'Portals', wiki: 'Wiki links', card: 'Card relations', previous: 'Previous boards' };
20
+ type LinkKind = keyof typeof linkNames;
21
+
22
+ export function HomeMapView() {
23
+ const boards = boardList.value;
24
+ const liveRevision = memoryGraphRevision.value;
25
+ const { graph, loading, error, retry } = useMemoryGraph('boards');
26
+ const [query, setQuery] = useState('');
27
+ const [showBoards, setShowBoards] = useState(true);
28
+ const [context, setContext] = useState(false);
29
+ const [unfiled, setUnfiled] = useState(true);
30
+ const [hiddenFolders, setHiddenFolders] = useState(new Set<string>());
31
+ const [hiddenLinks, setHiddenLinks] = useState(new Set<LinkKind>());
32
+ const [collapsed, setCollapsed] = useState(new Set<string>());
33
+ const [selectedKey, setSelectedKey] = useState<string | null>(null);
34
+ const [hoverKey, setHoverKey] = useState<string | null>(null);
35
+ const [details, setDetails] = useState<{ key: string; graph: MemoryGraph | null } | null>(null);
36
+ const [offset, setOffset] = useState<Point>({ x: 0, y: 0 });
37
+ const [zoom, setZoom] = useState(1);
38
+ const [moved, setMoved] = useState<Record<string, Point>>({});
39
+ const [sidebar, setSidebar] = useState(false);
40
+ const viewport = useRef<HTMLDivElement>(null);
41
+ const fitPending = useRef(true);
42
+ const suppressClick = useRef(false);
43
+ const gesture = useRef<{
44
+ id: number;
45
+ start: Point;
46
+ origin: Point;
47
+ key?: string;
48
+ boardId?: string;
49
+ current?: Point;
50
+ moved: boolean;
51
+ } | null>(null);
52
+ useLayoutEffect(() => {
53
+ browsingMemory.value = true;
54
+ }, []);
55
+ useLayoutEffect(() => {
56
+ if (!selectedKey) return;
57
+ const onKey = (event: KeyboardEvent) => {
58
+ if (isSelectionEscape(event)) {
59
+ event.preventDefault();
60
+ setSelectedKey(null);
61
+ }
62
+ };
63
+ window.addEventListener('keydown', onKey);
64
+ return () => window.removeEventListener('keydown', onKey);
65
+ }, [selectedKey]);
66
+ useEffect(() => {
67
+ if (!selectedKey) return;
68
+ let cancelled = false;
69
+ setDetails(null);
70
+ const epoch = catalogEpoch;
71
+ const revision = memoryGraphRevision.peek();
72
+ const params = new URLSearchParams({ focus: selectedKey, depth: '2' });
73
+ void requestJson<MemoryGraph | null>('memoryGraph', `/api/canvas/memory-graph?${params}`, null).then((result) => {
74
+ if (!cancelled && epoch === catalogEpoch && revision === memoryGraphRevision.peek())
75
+ setDetails({
76
+ key: selectedKey,
77
+ graph: result && Array.isArray(result.nodes) && Array.isArray(result.edges) ? result : null,
78
+ });
79
+ });
80
+ return () => {
81
+ cancelled = true;
82
+ };
83
+ }, [selectedKey, graph, liveRevision]);
84
+ const nodes = useMemo(
85
+ () =>
86
+ (graph?.nodes ?? [])
87
+ .filter((node) => node.kind === 'board')
88
+ .map((node) => {
89
+ const board = boards.find((entry) => entry.id === node.boardId);
90
+ return board
91
+ ? {
92
+ ...node,
93
+ title: board.name,
94
+ folder: board.folder,
95
+ inContext: !!board.pin,
96
+ nodeCount: board.nodeCount,
97
+ position: board.mapPosition ?? undefined,
98
+ }
99
+ : node;
100
+ }),
101
+ [graph, boards],
102
+ );
103
+ const generated = useMemo(() => mapLayout(nodes, collapsed), [nodes, collapsed]);
104
+ const folderCounts = new Map<string, number>();
105
+ for (const node of nodes) {
106
+ const folder = node.folder?.split('/')[0] ?? '';
107
+ folderCounts.set(folder, (folderCounts.get(folder) ?? 0) + 1);
108
+ }
109
+ const visible = nodes.filter(
110
+ (node) =>
111
+ showBoards &&
112
+ (!context || node.inContext) &&
113
+ (unfiled || !!node.folder) &&
114
+ !hiddenFolders.has(node.folder?.split('/')[0] ?? '') &&
115
+ !generated.hidden.has(node.key) &&
116
+ `${node.title} ${node.summary} ${node.folder ?? ''}`
117
+ .toLocaleLowerCase()
118
+ .includes(query.trim().toLocaleLowerCase()),
119
+ );
120
+ const keys = new Set(visible.map((node) => node.key));
121
+ const position = (key: string) => moved[key] ?? generated.positions.get(key);
122
+ const selected = nodes.find((node) => node.key === selectedKey);
123
+ const emphasized = nodes.find((node) => node.key === (hoverKey ?? selectedKey));
124
+ const bundles = new Map<string, { from: string; to: string; edges: MemoryGraphEdge[] }>();
125
+ for (const edge of graph?.edges ?? []) {
126
+ if (edge.kind === 'member' || hiddenLinks.has(edge.kind)) continue;
127
+ const from = memoryKey({ boardId: edge.source.boardId }),
128
+ to = memoryKey({ boardId: edge.target.boardId });
129
+ if (!keys.has(from) || (!edge.missing && (!keys.has(to) || from === to))) continue;
130
+ const key = JSON.stringify([from, to, edge.kind, edge.relationKind, edge.missing]);
131
+ const old = bundles.get(key);
132
+ if (old) old.edges.push(edge);
133
+ else bundles.set(key, { from, to, edges: [edge] });
134
+ }
135
+ const neighbors = new Set<string>(emphasized ? [emphasized.key] : []);
136
+ for (const { from, to } of bundles.values())
137
+ if (emphasized && (from === emphasized.key || to === emphasized.key)) {
138
+ neighbors.add(from);
139
+ neighbors.add(to);
140
+ }
141
+ const connectionCount = (node: MemoryGraphNode) =>
142
+ new Set(
143
+ (graph?.edges ?? [])
144
+ .filter(
145
+ (edge) =>
146
+ edge.kind !== 'member' && (edge.source.boardId === node.boardId || edge.target.boardId === node.boardId),
147
+ )
148
+ .map((edge) => (edge.source.boardId === node.boardId ? edge.target.boardId : edge.source.boardId))
149
+ .filter((id) => id !== node.boardId),
150
+ ).size;
151
+ const fitView = () => {
152
+ const stage = viewport.current;
153
+ if (!stage?.clientWidth || !stage.clientHeight) return;
154
+ const bounds = visible.flatMap((node) => {
155
+ const p = position(node.key);
156
+ return p
157
+ ? [
158
+ { x: p.x - 110, y: p.y - 46 },
159
+ { x: p.x + 110, y: p.y + 46 },
160
+ ]
161
+ : [];
162
+ });
163
+ for (const region of generated.regions) {
164
+ if (hiddenFolders.has(region.path.split('/')[0] ?? '') || (!unfiled && !region.path)) continue;
165
+ bounds.push({ x: region.x, y: region.y }, { x: region.x + region.width, y: region.y + region.height });
166
+ }
167
+ if (!bounds.length) return;
168
+ const x = Math.min(...bounds.map((p) => p.x)),
169
+ y = Math.min(...bounds.map((p) => p.y));
170
+ const width = Math.max(...bounds.map((p) => p.x)) - x,
171
+ height = Math.max(...bounds.map((p) => p.y)) - y;
172
+ const scale = Math.min(
173
+ 1.25,
174
+ Math.max(
175
+ 0.08,
176
+ Math.min((stage.clientWidth - 64) / Math.max(1, width), (stage.clientHeight - 100) / Math.max(1, height)),
177
+ ),
178
+ );
179
+ setZoom(scale);
180
+ setOffset({
181
+ x: (stage.clientWidth - width * scale) / 2 - x * scale,
182
+ y: (stage.clientHeight - height * scale) / 2 - y * scale - 12,
183
+ });
184
+ };
185
+ useEffect(() => {
186
+ fitPending.current = true;
187
+ }, [collapsed]);
188
+ useEffect(() => {
189
+ if (fitPending.current && graph && (visible.length || generated.regions.length) && !gesture.current) {
190
+ fitPending.current = false;
191
+ fitView();
192
+ }
193
+ }, [graph, collapsed]);
194
+ const zoomAt = (next: number, center?: Point) => {
195
+ const stage = viewport.current;
196
+ if (!stage) return;
197
+ const p = center ?? { x: stage.clientWidth / 2, y: stage.clientHeight / 2 },
198
+ scale = Math.min(3, Math.max(0.08, next));
199
+ setOffset({ x: p.x - ((p.x - offset.x) * scale) / zoom, y: p.y - ((p.y - offset.y) * scale) / zoom });
200
+ setZoom(scale);
201
+ };
202
+ const select = (node: MemoryGraphNode, recenter = false) => {
203
+ setSelectedKey(node.key);
204
+ const p = position(node.key),
205
+ stage = viewport.current;
206
+ if (recenter && p && stage)
207
+ setOffset({ x: stage.clientWidth / 2 - p.x * zoom, y: stage.clientHeight / 2 - p.y * zoom });
208
+ };
209
+ const endGesture = (cancelled: boolean) => {
210
+ const drag = gesture.current;
211
+ gesture.current = null;
212
+ if (!drag) return;
213
+ suppressClick.current = drag.moved;
214
+ if (!drag.key) return;
215
+ const key = drag.key;
216
+ const clear = () =>
217
+ setMoved((old) => {
218
+ const next = { ...old };
219
+ delete next[key];
220
+ return next;
221
+ });
222
+ if (drag.boardId && drag.moved && !cancelled) {
223
+ const p = drag.current ?? drag.origin;
224
+ void updateBoard(drag.boardId, {
225
+ mapPosition: { x: p.x - MAP_CARD.width / 2, y: p.y - MAP_CARD.height / 2 },
226
+ }).finally(clear);
227
+ } else clear();
228
+ };
229
+ const detailGraph = details?.key === selectedKey ? details.graph : null;
230
+ const selectedConnections = (detailGraph?.edges ?? []).filter(
231
+ (edge) =>
232
+ selected &&
233
+ edge.kind !== 'member' &&
234
+ (edge.source.boardId === selected.boardId || edge.target.boardId === selected.boardId),
235
+ );
236
+ const selectedBoard = boards.find((board) => board.id === selected?.boardId);
237
+ const filter = (label: string, count: number, checked: boolean, change: () => void) => (
238
+ <label class="home-memory-check">
239
+ <input type="checkbox" aria-label={label} checked={checked} onChange={change} />
240
+ <span>{label}</span>
241
+ <small>{count}</small>
242
+ </label>
243
+ );
244
+ return (
245
+ <main class={`home-memory is-map${sidebar ? ' sidebar-open' : ''}`} aria-label="Library Map">
246
+ <div class="home-memory-toolbar">
247
+ <span>
248
+ {nodes.length} boards · {boards.reduce((sum, board) => sum + board.nodeCount, 0)} cards ·{' '}
249
+ {graph?.edges.filter((edge) => edge.kind !== 'member').length ?? 0} links
250
+ </span>
251
+ <span class={`home-memory-live${error ? ' is-error' : ''}`} role="status">
252
+ {error ? 'Update failed' : loading ? 'Updating…' : 'Live'}
253
+ </span>
254
+ <button
255
+ type="button"
256
+ class="home-button home-memory-sidebar-toggle"
257
+ aria-expanded={sidebar}
258
+ onClick={() => setSidebar(!sidebar)}
259
+ >
260
+ Filters
261
+ </button>
262
+ </div>
263
+ <div class="home-memory-body">
264
+ <div
265
+ class="home-memory-stage"
266
+ ref={viewport}
267
+ aria-label="Pan the memory view"
268
+ onWheel={(event) => {
269
+ event.preventDefault();
270
+ const r = event.currentTarget.getBoundingClientRect();
271
+ zoomAt(zoom * Math.exp(-event.deltaY * 0.0015), { x: event.clientX - r.left, y: event.clientY - r.top });
272
+ }}
273
+ onPointerDown={(event) => {
274
+ if (event.button !== 0 || (event.target as HTMLElement).closest('button')) return;
275
+ suppressClick.current = false;
276
+ gesture.current = {
277
+ id: event.pointerId,
278
+ start: { x: event.clientX, y: event.clientY },
279
+ origin: offset,
280
+ moved: false,
281
+ };
282
+ event.currentTarget.setPointerCapture(event.pointerId);
283
+ }}
284
+ onPointerMove={(event) => {
285
+ const drag = gesture.current;
286
+ if (!drag || drag.id !== event.pointerId) return;
287
+ const dx = event.clientX - drag.start.x,
288
+ dy = event.clientY - drag.start.y;
289
+ if (Math.abs(dx) + Math.abs(dy) > 4) drag.moved = true;
290
+ if (!drag.moved) return;
291
+ const p = { x: drag.origin.x + dx / (drag.key ? zoom : 1), y: drag.origin.y + dy / (drag.key ? zoom : 1) };
292
+ drag.current = p;
293
+ if (drag.key) setMoved((old) => ({ ...old, [drag.key!]: p }));
294
+ else setOffset(p);
295
+ }}
296
+ onPointerUp={() => endGesture(false)}
297
+ onPointerCancel={() => endGesture(true)}
298
+ >
299
+ <div
300
+ class="home-memory-world"
301
+ style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`, '--memory-zoom': zoom }}
302
+ >
303
+ {generated.regions
304
+ .filter((region) => !hiddenFolders.has(region.path.split('/')[0] ?? '') && (unfiled || region.path))
305
+ .map((region) => (
306
+ <div
307
+ key={region.path}
308
+ class={`home-memory-region${region.depth ? ' is-nested' : ''}`}
309
+ style={{ left: region.x, top: region.y, width: region.width, height: region.height }}
310
+ >
311
+ <button
312
+ type="button"
313
+ aria-expanded={!region.collapsed}
314
+ onClick={() =>
315
+ setCollapsed((old) => {
316
+ const next = new Set(old);
317
+ if (next.has(region.path)) next.delete(region.path);
318
+ else next.add(region.path);
319
+ return next;
320
+ })
321
+ }
322
+ >
323
+ {region.collapsed ? '▸' : '▾'} {region.path.split('/').at(-1) || 'Unfiled'}{' '}
324
+ <small>{region.count} boards</small>
325
+ </button>
326
+ </div>
327
+ ))}
328
+ <svg class="home-memory-links" aria-hidden="true">
329
+ {[...bundles].map(([key, { from, to, edges }]) => {
330
+ const edge = edges[0],
331
+ a = position(from),
332
+ b = position(to);
333
+ if (!a) return null;
334
+ const end = b ?? { x: a.x + 100, y: a.y - 70 },
335
+ startY = a.y + (end.y > a.y ? 46 : -46),
336
+ endY = b ? end.y + (end.y > a.y ? -46 : 46) : end.y;
337
+ const hi =
338
+ emphasized &&
339
+ (edge.source.boardId === emphasized.boardId || edge.target.boardId === emphasized.boardId);
340
+ const definition = relationDefinition(
341
+ { kind: edge.relationKind, type: edge.type },
342
+ boards.find((board) => board.id === edge.source.boardId)?.relationVocabulary,
343
+ );
344
+ const ink = edge.missing
345
+ ? 'var(--c-warn)'
346
+ : edge.kind === 'card'
347
+ ? `var(--c-rel-${definition.ink})`
348
+ : undefined;
349
+ return (
350
+ <g
351
+ key={key}
352
+ data-from={from}
353
+ data-to={to}
354
+ data-count={edges.length}
355
+ style={ink ? { stroke: ink } : undefined}
356
+ class={`memory-edge edge-${edge.kind}${edge.missing ? ' is-missing' : ''}${hi ? ' is-emphasized' : emphasized ? ' is-dimmed' : ''}`}
357
+ >
358
+ <path
359
+ d={`M ${a.x} ${startY} C ${a.x} ${(startY + endY) / 2}, ${end.x} ${(startY + endY) / 2}, ${end.x} ${endY}`}
360
+ />
361
+ {edges.length > 1 && (
362
+ <text x={(a.x + end.x) / 2} y={(a.y + end.y) / 2 - 6}>
363
+ {edges.length} links
364
+ </text>
365
+ )}
366
+ {edge.missing && (
367
+ <text x={end.x} y={end.y - 5}>
368
+ ⚠ Missing {edge.kind} link
369
+ </text>
370
+ )}
371
+ </g>
372
+ );
373
+ })}
374
+ </svg>
375
+ {visible.map((node, index) => {
376
+ const p = position(node.key);
377
+ if (!p) return null;
378
+ const board = boards.find((entry) => entry.id === node.boardId);
379
+ return (
380
+ <article
381
+ key={node.key}
382
+ data-key={node.key}
383
+ class={`home-memory-node is-board${node.inContext ? ' in-context' : ''}${selectedKey === node.key ? ' is-selected' : ''}${neighbors.has(node.key) ? ' is-emphasized' : emphasized ? ' is-dimmed' : ''}`}
384
+ style={{ left: p.x, top: p.y }}
385
+ onPointerEnter={() => {
386
+ if (!gesture.current) setHoverKey(node.key);
387
+ }}
388
+ onPointerLeave={() => setHoverKey(null)}
389
+ >
390
+ <button
391
+ type="button"
392
+ class="home-memory-select"
393
+ aria-label={`board: ${node.title}`}
394
+ aria-pressed={selectedKey === node.key}
395
+ onFocus={(event) => {
396
+ if (!gesture.current && event.currentTarget.matches(':focus-visible')) select(node, true);
397
+ }}
398
+ onClick={() => {
399
+ if (!suppressClick.current) select(node);
400
+ suppressClick.current = false;
401
+ }}
402
+ onDblClick={() => void openMemoryTarget(node)}
403
+ onPointerDown={(event) => {
404
+ if (event.button !== 0) return;
405
+ event.stopPropagation();
406
+ suppressClick.current = false;
407
+ gesture.current = {
408
+ id: event.pointerId,
409
+ start: { x: event.clientX, y: event.clientY },
410
+ origin: p,
411
+ key: node.key,
412
+ boardId: node.boardId,
413
+ moved: false,
414
+ };
415
+ event.currentTarget.setPointerCapture(event.pointerId);
416
+ }}
417
+ onKeyDown={(event) => {
418
+ if (event.key === 'Enter') {
419
+ event.preventDefault();
420
+ void openMemoryTarget(node);
421
+ }
422
+ if (event.key.startsWith('Arrow')) {
423
+ event.preventDefault();
424
+ const nextIndex =
425
+ (index + (event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 1) + visible.length) %
426
+ visible.length;
427
+ const next = visible[nextIndex];
428
+ if (next) {
429
+ select(next, true);
430
+ viewport.current
431
+ ?.querySelectorAll<HTMLButtonElement>('.home-memory-select')
432
+ [nextIndex]?.focus({ preventScroll: true });
433
+ }
434
+ }
435
+ }}
436
+ >
437
+ <strong>{node.title}</strong>
438
+ <span>
439
+ {node.nodeCount ?? 0} cards · {connectionCount(node)} connected boards
440
+ </span>
441
+ <span class="home-memory-summary">{node.summary || 'No summary yet'}</span>
442
+ {node.inContext && <span class="home-memory-context">⌖ In context</span>}
443
+ </button>
444
+ {board && <BoardPinButton board={board} />}
445
+ </article>
446
+ );
447
+ })}
448
+ </div>
449
+ <div class="home-memory-zoom">
450
+ <button type="button" class="home-button" aria-label="Zoom out" onClick={() => zoomAt(zoom / 1.2)}>
451
+ −
452
+ </button>
453
+ <span>{Math.round(zoom * 100)}%</span>
454
+ <button type="button" class="home-button" aria-label="Zoom in" onClick={() => zoomAt(zoom * 1.2)}>
455
+ +
456
+ </button>
457
+ <button type="button" class="home-button" onClick={fitView}>
458
+ Fit view
459
+ </button>
460
+ </div>
461
+ {(!graph || !visible.length || error) && (
462
+ <div class="home-memory-status" role="status">
463
+ {error ? (
464
+ <>
465
+ Memory update failed.{' '}
466
+ <button type="button" class="home-button" onClick={retry}>
467
+ Retry
468
+ </button>
469
+ </>
470
+ ) : !graph ? (
471
+ 'Loading memory…'
472
+ ) : generated.hidden.size ? (
473
+ 'Folders collapsed. Expand a folder to see its boards.'
474
+ ) : (
475
+ 'No matching nodes.'
476
+ )}
477
+ </div>
478
+ )}
479
+ </div>
480
+ <aside class="home-memory-sidebar" aria-label="Map settings">
481
+ <section>
482
+ <h2>Filters</h2>
483
+ <label class="home-memory-search">
484
+ <span>Find boards and cards</span>
485
+ <input
486
+ type="search"
487
+ value={query}
488
+ placeholder="Search boards and cards…"
489
+ onInput={(event) => setQuery(event.currentTarget.value)}
490
+ />
491
+ </label>
492
+ {filter('Boards', nodes.length, showBoards, () => setShowBoards(!showBoards))}
493
+ {filter('Only in context', nodes.filter((node) => node.inContext).length, context, () =>
494
+ setContext(!context),
495
+ )}
496
+ {filter('Include unfiled', nodes.filter((node) => !node.folder).length, unfiled, () =>
497
+ setUnfiled(!unfiled),
498
+ )}
499
+ </section>
500
+ <section>
501
+ <h2>Groups · by folder</h2>
502
+ {[...folderCounts]
503
+ .sort(([a], [b]) => a.localeCompare(b))
504
+ .map(([folder, count]) => (
505
+ <div key={folder}>
506
+ {filter(folder || 'Unfiled', count, !hiddenFolders.has(folder), () =>
507
+ setHiddenFolders((old) => {
508
+ const next = new Set(old);
509
+ if (next.has(folder)) next.delete(folder);
510
+ else next.add(folder);
511
+ return next;
512
+ }),
513
+ )}
514
+ </div>
515
+ ))}
516
+ </section>
517
+ <section>
518
+ <h2>Links</h2>
519
+ {(Object.keys(linkNames) as LinkKind[]).map((kind) => (
520
+ <div key={kind} class={`memory-legend-${kind}`}>
521
+ {filter(
522
+ linkNames[kind],
523
+ graph?.edges.filter((edge) => edge.kind === kind).length ?? 0,
524
+ !hiddenLinks.has(kind),
525
+ () =>
526
+ setHiddenLinks((old) => {
527
+ const next = new Set(old);
528
+ if (next.has(kind)) next.delete(kind);
529
+ else next.add(kind);
530
+ return next;
531
+ }),
532
+ )}
533
+ </div>
534
+ ))}
535
+ </section>
536
+ </aside>
537
+ </div>
538
+ <div class="home-memory-legend" aria-label="Legend">
539
+ <span>▣ Board</span>
540
+ <span class="memory-context-legend">⌖ In context</span>
541
+ <span class="memory-legend-portal">━ Portal</span>
542
+ <span class="memory-legend-wiki">┄ Wiki</span>
543
+ <span class="memory-legend-card">━ Relation</span>
544
+ <span>Drag boards to save positions · drag background to pan · scroll to zoom</span>
545
+ </div>
546
+ <MemoryInspector>
547
+ {selected ? (
548
+ <section class="home-memory-details" role="complementary" aria-label="Selection details">
549
+ <button
550
+ type="button"
551
+ class="home-memory-clear"
552
+ aria-label="Close details"
553
+ onClick={() => setSelectedKey(null)}
554
+ >
555
+ ×
556
+ </button>
557
+ <div class="home-memory-detail-header">
558
+ <div class="home-memory-selection-title">
559
+ <h3>
560
+ {selected.title} · {selectedConnections.length} connections
561
+ </h3>
562
+ <p>{selected.folder || 'Unfiled'} · board</p>
563
+ </div>
564
+ <div class="home-memory-actions">
565
+ <button type="button" class="home-button is-primary" onClick={() => void openMemoryTarget(selected)}>
566
+ Open board
567
+ </button>
568
+ {selectedBoard && <BoardPinButton board={selectedBoard} />}
569
+ <button
570
+ type="button"
571
+ class="home-button"
572
+ onClick={() => void updateBoard(selected.boardId, { mapPosition: null })}
573
+ >
574
+ Reset position
575
+ </button>
576
+ </div>
577
+ </div>
578
+ <div class="home-memory-detail-body">
579
+ <p>
580
+ {selected.nodeCount ?? 0} cards · {connectionCount(selected)} connected boards
581
+ </p>
582
+ <p>{selected.summary || 'No summary yet.'}</p>
583
+ <p class={selected.inContext ? 'memory-context-legend' : ''}>
584
+ {selected.inContext ? '⌖ In context' : 'Not in context'}
585
+ </p>
586
+ <details class="home-memory-connections" open>
587
+ <summary>Connections</summary>
588
+ {!detailGraph && (
589
+ <p role="status">
590
+ {details?.key === selectedKey ? 'Connections could not be loaded.' : 'Loading connections…'}
591
+ </p>
592
+ )}
593
+ <ul>
594
+ {selectedConnections.map((edge) => {
595
+ const outgoing = edge.source.boardId === selected.boardId,
596
+ target = outgoing ? edge.target : edge.source;
597
+ const other = detailGraph?.nodes.find((node) => node.key === memoryKey(target));
598
+ return (
599
+ <li key={edge.id}>
600
+ <span>
601
+ {outgoing ? '→' : '←'}{' '}
602
+ {edge.kind === 'card'
603
+ ? relationText({ ...edge, kind: edge.relationKind })
604
+ : `${edge.kind}${edge.label ? ` · ${edge.label}` : ''}`}
605
+ </span>
606
+ {edge.missing ? (
607
+ <span class="home-memory-missing">
608
+ ⚠ Missing target: {target.boardId}
609
+ {target.cardId ? ` / ${target.cardId}` : ''}
610
+ </span>
611
+ ) : (
612
+ <button type="button" class="home-button" onClick={() => void openMemoryTarget(target)}>
613
+ {other?.title ?? target.boardId}
614
+ </button>
615
+ )}
616
+ </li>
617
+ );
618
+ })}
619
+ {(detailGraph?.unresolvedLinks ?? [])
620
+ .filter((link) => link.source.boardId === selected.boardId)
621
+ .map((link) => (
622
+ <li key={`${memoryKey(link.source)}:${link.ref}`} class="home-memory-missing">
623
+ ⚠ Unresolved wiki: [[{link.ref}]]
624
+ </li>
625
+ ))}
626
+ </ul>
627
+ </details>
628
+ </div>
629
+ </section>
630
+ ) : (
631
+ <div class="home-memory-empty-details">
632
+ <h3>Relations</h3>
633
+ <p class="home-memory-help">
634
+ Overview groups links between boards. Select a board to inspect connections. Double-click to open.
635
+ </p>
636
+ </div>
637
+ )}
638
+ </MemoryInspector>
639
+ </main>
640
+ );
641
+ }