pmx-canvas 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +66 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2759 -510
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2936 -632
  8. package/dist/json-render/index.css +81 -1
  9. package/dist/json-render/index.js +20 -20
  10. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  11. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  12. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  13. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  15. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  17. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  18. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  19. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  20. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  21. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  22. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  23. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  24. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  25. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  26. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  27. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  28. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  29. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  30. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  31. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  32. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  33. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  34. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  35. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  36. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  37. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  38. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  39. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  40. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  41. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  42. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  43. package/dist/types/client/icons.d.ts +0 -10
  44. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  45. package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
  46. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  47. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  48. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  49. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  50. package/dist/types/client/state/boards-store.d.ts +81 -9
  51. package/dist/types/client/state/canvas-store.d.ts +14 -1
  52. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  53. package/dist/types/client/state/context-status-store.d.ts +35 -0
  54. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  55. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  56. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  57. package/dist/types/client/state/presence-store.d.ts +4 -4
  58. package/dist/types/client/state/presentation.d.ts +28 -0
  59. package/dist/types/client/state/relation-store.d.ts +10 -0
  60. package/dist/types/client/state/session-store.d.ts +41 -2
  61. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  62. package/dist/types/client/types.d.ts +13 -1
  63. package/dist/types/client/utils/keyboard.d.ts +2 -0
  64. package/dist/types/client/utils/time-ago.d.ts +4 -0
  65. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  66. package/dist/types/server/agent-presence.d.ts +10 -3
  67. package/dist/types/server/ax-context.d.ts +1 -1
  68. package/dist/types/server/canvas-db.d.ts +30 -7
  69. package/dist/types/server/canvas-operations.d.ts +24 -2
  70. package/dist/types/server/canvas-provenance.d.ts +22 -2
  71. package/dist/types/server/canvas-serialization.d.ts +2 -0
  72. package/dist/types/server/canvas-state.d.ts +93 -15
  73. package/dist/types/server/canvas-validation.d.ts +1 -1
  74. package/dist/types/server/context-brief.d.ts +94 -6
  75. package/dist/types/server/context-reads.d.ts +33 -7
  76. package/dist/types/server/edit-summary.d.ts +20 -0
  77. package/dist/types/server/index.d.ts +71 -10
  78. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  79. package/dist/types/server/lifecycle-status.d.ts +10 -0
  80. package/dist/types/server/memory-graph.d.ts +9 -0
  81. package/dist/types/server/mutation-history.d.ts +2 -0
  82. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  83. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  84. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  85. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  86. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  87. package/dist/types/server/operations/ops/query.d.ts +23 -0
  88. package/dist/types/server/operations/registry.d.ts +1 -0
  89. package/dist/types/server/placement.d.ts +7 -0
  90. package/dist/types/server/scope-fence.d.ts +2 -1
  91. package/dist/types/server/spatial-analysis.d.ts +6 -3
  92. package/dist/types/server/tour-control.d.ts +32 -0
  93. package/dist/types/server/wiki-links.d.ts +14 -0
  94. package/dist/types/server/workbench-auth.d.ts +2 -0
  95. package/dist/types/shared/agent-context.d.ts +2 -0
  96. package/dist/types/shared/agent-presence.d.ts +15 -12
  97. package/dist/types/shared/auto-arrange.d.ts +1 -0
  98. package/dist/types/shared/boards.d.ts +4 -4
  99. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  100. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  101. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  102. package/dist/types/shared/memory-graph.d.ts +53 -0
  103. package/dist/types/shared/relations.d.ts +68 -0
  104. package/dist/types/shared/semantic-attention.d.ts +2 -0
  105. package/dist/types/shared/tour.d.ts +38 -0
  106. package/dist/types/shared/workspace-settings.d.ts +11 -0
  107. package/docs/RELEASE.md +21 -3
  108. package/docs/adr-001-bun-only-runtime.md +7 -1
  109. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  110. package/docs/api-reference.md +109 -0
  111. package/docs/ax-host-adapter-contract.md +1 -1
  112. package/docs/ax-state-contract.md +5 -4
  113. package/docs/cli.md +153 -15
  114. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  115. package/docs/design/AgentContext.dc.html +3 -3
  116. package/docs/design/BoardPins.dc.html +37 -22
  117. package/docs/design/Categories.dc.html +208 -0
  118. package/docs/design/CategoryEditor.dc.html +33 -0
  119. package/docs/design/Chrome.dc.html +55 -9
  120. package/docs/design/ContextChip.dc.html +224 -0
  121. package/docs/design/ContextChip600.dc.html +136 -0
  122. package/docs/design/EmptyState.dc.html +45 -0
  123. package/docs/design/Flows.dc.html +23 -1
  124. package/docs/design/HeaderMarks.dc.html +183 -0
  125. package/docs/design/Home.dc.html +19 -1
  126. package/docs/design/HomeGraph.dc.html +86 -202
  127. package/docs/design/HomeMap.dc.html +23 -1
  128. package/docs/design/HomeTeam.dc.html +32 -14
  129. package/docs/design/Lifecycle.dc.html +22 -0
  130. package/docs/design/Lifecycle600.dc.html +18 -0
  131. package/docs/design/LifecycleVariants.dc.html +157 -0
  132. package/docs/design/LinkAuthorship.dc.html +139 -0
  133. package/docs/design/LinksChanged.dc.html +203 -0
  134. package/docs/design/LinksOptions.dc.html +137 -0
  135. package/docs/design/LinksOptions600.dc.html +134 -0
  136. package/docs/design/NearPin.dc.html +9 -10
  137. package/docs/design/Pane600.dc.html +4 -3
  138. package/docs/design/Relations.dc.html +3 -2
  139. package/docs/design/Restore.dc.html +152 -0
  140. package/docs/design/SeeChange.dc.html +267 -0
  141. package/docs/design/Uses.dc.html +12 -6
  142. package/docs/design/canvas.json +146 -19
  143. package/docs/design.md +454 -18
  144. package/docs/environment.md +0 -1
  145. package/docs/evals/curation-effect.md +128 -0
  146. package/docs/http-api.md +465 -38
  147. package/docs/mcp.md +150 -19
  148. package/docs/node-types.md +58 -3
  149. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  150. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  151. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  152. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  153. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  154. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  155. package/docs/product-context-vision-2026-09.md +17 -1
  156. package/docs/product-vision-2026-09.md +1082 -43
  157. package/docs/product-vision-review-2026-09.md +13 -1
  158. package/docs/screenshots/workbench-live-dark.png +0 -0
  159. package/docs/screenshots/workbench-live-light.png +0 -0
  160. package/docs/sdk.md +62 -2
  161. package/package.json +1 -1
  162. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/plugin.json +1 -1
  164. package/plugins/pmx-canvas/runtime.json +1 -1
  165. package/plugins/pmx-canvas/tools.json +601 -22
  166. package/skills/pmx-canvas/SKILL.md +234 -43
  167. package/skills/pmx-canvas/evals/evals.json +62 -0
  168. package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
  169. package/skills/pmx-canvas/references/full-reference.md +120 -21
  170. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  171. package/skills/pmx-canvas/references/importing-context.md +132 -0
  172. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  173. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  174. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  175. package/src/cli/agent.ts +2 -0
  176. package/src/cli/commands/ax.ts +12 -0
  177. package/src/cli/commands/boards.ts +87 -11
  178. package/src/cli/commands/edges.ts +28 -0
  179. package/src/cli/commands/groups.ts +7 -0
  180. package/src/cli/commands/nodes.ts +4 -2
  181. package/src/cli/commands/query.ts +34 -2
  182. package/src/cli/commands/record.ts +199 -48
  183. package/src/cli/index.ts +7 -1
  184. package/src/client/App.tsx +177 -70
  185. package/src/client/canvas/AttentionHistory.tsx +0 -2
  186. package/src/client/canvas/BarHint.tsx +40 -2
  187. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  188. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  189. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  190. package/src/client/canvas/BoardPinButton.tsx +33 -0
  191. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  192. package/src/client/canvas/CanvasNode.tsx +360 -237
  193. package/src/client/canvas/CanvasViewport.tsx +45 -7
  194. package/src/client/canvas/CommandBar.tsx +29 -23
  195. package/src/client/canvas/CommandPalette.tsx +5 -12
  196. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  197. package/src/client/canvas/ContextChip.tsx +296 -0
  198. package/src/client/canvas/ContextMenu.tsx +63 -5
  199. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  200. package/src/client/canvas/EdgeLayer.tsx +339 -76
  201. package/src/client/canvas/EmptyState.tsx +33 -18
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  203. package/src/client/canvas/ExternalWriters.tsx +57 -42
  204. package/src/client/canvas/HomeGraphView.tsx +942 -0
  205. package/src/client/canvas/HomeMapView.tsx +641 -0
  206. package/src/client/canvas/HomeReviewView.tsx +167 -0
  207. package/src/client/canvas/HomeView.tsx +687 -160
  208. package/src/client/canvas/IntentLayer.tsx +19 -12
  209. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  210. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  211. package/src/client/canvas/LifecycleFields.tsx +91 -0
  212. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  213. package/src/client/canvas/MemoryInspector.tsx +104 -0
  214. package/src/client/canvas/Minimap.tsx +3 -3
  215. package/src/client/canvas/NearPinLayer.tsx +187 -0
  216. package/src/client/canvas/NodeContextMark.tsx +175 -97
  217. package/src/client/canvas/Presentation.tsx +23 -57
  218. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  219. package/src/client/canvas/RelationsPanel.tsx +82 -0
  220. package/src/client/canvas/SelectionBar.tsx +3 -6
  221. package/src/client/canvas/SessionPanel.tsx +90 -50
  222. package/src/client/canvas/SessionReceipt.tsx +311 -39
  223. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  224. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  225. package/src/client/canvas/ToolRail.tsx +367 -192
  226. package/src/client/canvas/TopBar.tsx +354 -124
  227. package/src/client/canvas/canvas-area.ts +10 -3
  228. package/src/client/canvas/graph-geometry.ts +195 -0
  229. package/src/client/canvas/graph-model.ts +254 -0
  230. package/src/client/canvas/memory-layout.ts +106 -0
  231. package/src/client/canvas/use-chrome-height.ts +20 -0
  232. package/src/client/canvas/use-focus-trap.ts +31 -5
  233. package/src/client/canvas/use-memory-graph.ts +58 -0
  234. package/src/client/canvas/use-node-drag.ts +2 -1
  235. package/src/client/canvas/use-pan-zoom.ts +64 -7
  236. package/src/client/icons.tsx +0 -63
  237. package/src/client/mcp-app/style.css +0 -6
  238. package/src/client/nodes/AxStepControls.tsx +74 -50
  239. package/src/client/nodes/BoardNode.tsx +5 -1
  240. package/src/client/nodes/ContextNode.tsx +18 -11
  241. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  242. package/src/client/nodes/FileNode.tsx +46 -42
  243. package/src/client/nodes/HtmlNode.tsx +44 -2
  244. package/src/client/nodes/ImageNode.tsx +1 -1
  245. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  246. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  247. package/src/client/nodes/MarkdownNode.tsx +377 -109
  248. package/src/client/nodes/McpAppNode.tsx +50 -20
  249. package/src/client/nodes/MdFormatBar.tsx +21 -18
  250. package/src/client/nodes/PromptNode.tsx +1 -1
  251. package/src/client/nodes/ResponseNode.tsx +1 -1
  252. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  253. package/src/client/state/attention-bridge.ts +64 -15
  254. package/src/client/state/boards-store.ts +193 -39
  255. package/src/client/state/canvas-store.ts +99 -12
  256. package/src/client/state/context-chip-store.ts +70 -0
  257. package/src/client/state/context-status-store.ts +53 -0
  258. package/src/client/state/intent-bridge.ts +51 -30
  259. package/src/client/state/knowledge-store.ts +43 -0
  260. package/src/client/state/near-pin-store.ts +115 -6
  261. package/src/client/state/presence-store.ts +7 -10
  262. package/src/client/state/presentation.ts +91 -0
  263. package/src/client/state/relation-store.ts +81 -0
  264. package/src/client/state/session-store.ts +144 -18
  265. package/src/client/state/sse-bridge.ts +206 -29
  266. package/src/client/state/workspace-settings.ts +11 -0
  267. package/src/client/theme/global.css +2759 -510
  268. package/src/client/types.ts +12 -0
  269. package/src/client/utils/keyboard.ts +10 -0
  270. package/src/client/utils/time-ago.ts +14 -0
  271. package/src/json-render/renderer/index.css +6 -1
  272. package/src/json-render/renderer/index.tsx +31 -1
  273. package/src/mcp/canvas-app.ts +12 -2
  274. package/src/mcp/context-uri-template.ts +7 -4
  275. package/src/mcp/server.ts +63 -41
  276. package/src/mcp/workbench-app.ts +39 -6
  277. package/src/server/agent-presence.ts +128 -28
  278. package/src/server/ax-context.ts +27 -13
  279. package/src/server/board-export.ts +12 -1
  280. package/src/server/canvas-db.ts +331 -42
  281. package/src/server/canvas-operations.ts +196 -84
  282. package/src/server/canvas-provenance.ts +53 -22
  283. package/src/server/canvas-serialization.ts +2 -0
  284. package/src/server/canvas-state.ts +950 -122
  285. package/src/server/canvas-validation.ts +9 -2
  286. package/src/server/context-brief.ts +345 -38
  287. package/src/server/context-reads.ts +179 -30
  288. package/src/server/demo-state.json +355 -24
  289. package/src/server/edit-summary.ts +74 -0
  290. package/src/server/html-surface.ts +8 -1
  291. package/src/server/index.ts +203 -13
  292. package/src/server/knowledge-lifecycle.ts +304 -0
  293. package/src/server/lifecycle-status.ts +232 -0
  294. package/src/server/memory-graph.ts +166 -0
  295. package/src/server/mutation-history.ts +18 -6
  296. package/src/server/operations/composites.ts +32 -11
  297. package/src/server/operations/http.ts +7 -2
  298. package/src/server/operations/index.ts +4 -0
  299. package/src/server/operations/mcp.ts +32 -1
  300. package/src/server/operations/ops/ax-presence.ts +1 -1
  301. package/src/server/operations/ops/ax-read.ts +57 -6
  302. package/src/server/operations/ops/ax-timeline.ts +28 -2
  303. package/src/server/operations/ops/backup.ts +5 -3
  304. package/src/server/operations/ops/batch.ts +35 -4
  305. package/src/server/operations/ops/boards.ts +188 -47
  306. package/src/server/operations/ops/canvas-wire.ts +31 -1
  307. package/src/server/operations/ops/edges.ts +36 -2
  308. package/src/server/operations/ops/groups.ts +6 -1
  309. package/src/server/operations/ops/json-render.ts +5 -1
  310. package/src/server/operations/ops/lifecycle.ts +262 -0
  311. package/src/server/operations/ops/memory-graph.ts +45 -0
  312. package/src/server/operations/ops/nodes.ts +107 -24
  313. package/src/server/operations/ops/query.ts +281 -68
  314. package/src/server/operations/ops/snapshots.ts +73 -1
  315. package/src/server/operations/ops/validate.ts +5 -1
  316. package/src/server/operations/ops/viewport.ts +97 -3
  317. package/src/server/operations/registry.ts +77 -26
  318. package/src/server/placement.ts +29 -0
  319. package/src/server/scope-fence.ts +78 -6
  320. package/src/server/server.ts +115 -16
  321. package/src/server/spatial-analysis.ts +48 -23
  322. package/src/server/tour-control.ts +82 -0
  323. package/src/server/wiki-links.ts +137 -0
  324. package/src/server/workbench-auth.ts +11 -0
  325. package/src/shared/agent-context.ts +3 -0
  326. package/src/shared/agent-presence.ts +15 -15
  327. package/src/shared/auto-arrange.ts +2 -0
  328. package/src/shared/boards.ts +4 -4
  329. package/src/shared/knowledge-lifecycle.ts +22 -0
  330. package/src/shared/knowledge-presets.ts +80 -0
  331. package/src/shared/lifecycle-status.ts +26 -0
  332. package/src/shared/memory-graph.ts +74 -0
  333. package/src/shared/relations.ts +98 -0
  334. package/src/shared/semantic-attention.ts +15 -1
  335. package/src/shared/tour.ts +23 -5
  336. package/src/shared/workspace-settings.ts +13 -0
  337. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  338. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  339. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  340. package/dist/types/server/agent-context.d.ts +0 -22
  341. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -1,5 +1,7 @@
1
1
  import { useEffect } from 'preact/hooks';
2
+ import { BarHint } from './BarHint';
2
3
  import { nodes } from '../state/canvas-store';
4
+ import { activeBoardId } from '../state/boards-store';
3
5
  import { hoveredIntentId, intents, removeIntent, type ClientIntent } from '../state/intent-store';
4
6
  import { vetoGhostIntent } from '../state/intent-bridge';
5
7
  import { getNodeIcon, IconClose } from '../icons';
@@ -92,18 +94,19 @@ function GhostInfo({ intent }: { intent: ClientIntent }) {
92
94
  <span class="intent-chip-label">{label}</span>
93
95
  {confidencePct && <span class="intent-confidence">{confidencePct}</span>}
94
96
  {intent.phase === 'forming' && !intent.auto && (
95
- <button
96
- type="button"
97
- class="intent-veto"
98
- title="Veto this move (Esc)"
99
- aria-label="Veto this move"
100
- onClick={(e) => {
101
- e.stopPropagation();
102
- void vetoGhostIntent(intent);
103
- }}
104
- >
105
- <IconClose size={14} />
106
- </button>
97
+ <BarHint label="Veto this move" shortcut="Esc">
98
+ <button
99
+ type="button"
100
+ class="intent-veto"
101
+ aria-label="Veto this move"
102
+ onClick={(e) => {
103
+ e.stopPropagation();
104
+ void vetoGhostIntent(intent);
105
+ }}
106
+ >
107
+ <IconClose size={14} />
108
+ </button>
109
+ </BarHint>
107
110
  )}
108
111
  </div>
109
112
  {intent.reason && <div class="intent-reason">{intent.reason}</div>}
@@ -189,6 +192,8 @@ function renderGhost(intent: ClientIntent) {
189
192
  }
190
193
  case 'connect': {
191
194
  if (!intent.edge) return null;
195
+ if ('toBoardId' in intent.edge && intent.edge.toBoardId && intent.edge.toBoardId !== activeBoardId.value)
196
+ return null;
192
197
  const from = getNodeRect(intent.edge.from);
193
198
  const to = getNodeRect(intent.edge.to);
194
199
  if (!from || !to) return null;
@@ -268,6 +273,8 @@ export function IntentLayer() {
268
273
  </defs>
269
274
  {list.map((intent) => {
270
275
  if (intent.kind === 'connect' && intent.edge) {
276
+ if ('toBoardId' in intent.edge && intent.edge.toBoardId && intent.edge.toBoardId !== activeBoardId.value)
277
+ return null;
271
278
  const from = getNodeRect(intent.edge.from);
272
279
  const to = getNodeRect(intent.edge.to);
273
280
  if (!from || !to) return null;
@@ -0,0 +1,488 @@
1
+ import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
2
+ import { knowledgeLifecycleEnabled } from '../state/workspace-settings';
3
+ import { lifecycleUiGeneration } from '../state/boards-store';
4
+ import { categorySchema, type KnowledgeCategory, type Lifecycle } from '../../shared/knowledge-lifecycle.js';
5
+ import { KNOWLEDGE_PRESETS } from '../../shared/knowledge-presets.js';
6
+ import { knowledge, refreshKnowledge } from '../state/knowledge-store';
7
+ import { requestJson } from '../state/intent-bridge';
8
+ import { cleanLifecycle, DEFAULT_LIFECYCLE, LifecycleFields } from './LifecycleFields';
9
+ import { useFocusTrap } from './use-focus-trap';
10
+ import { ChoiceMenu } from './ChoiceMenu';
11
+
12
+ export function KnowledgeDialog({ folder, onClose }: { folder?: string; onClose: () => void }) {
13
+ const enabled = knowledgeLifecycleEnabled.value;
14
+ const ref = useRef<HTMLDivElement>(null);
15
+ const catalog = knowledge.value;
16
+ const properties = catalog?.folders.find((item) => item.path === folder);
17
+ const effective = folder ? catalog?.resolvedFolders[folder] : null;
18
+ const [categoryId, setCategoryId] = useState(properties?.categoryId ?? '');
19
+ const [override, setOverride] = useState(!!properties?.lifecycleOverride);
20
+ const [policy, setPolicy] = useState<Lifecycle>(
21
+ properties?.lifecycleOverride ?? effective?.lifecycle ?? DEFAULT_LIFECYCLE,
22
+ );
23
+ const [editing, setEditing] = useState<KnowledgeCategory | null>(() =>
24
+ catalog?.categories[0] ? structuredClone(catalog.categories[0]) : null,
25
+ );
26
+ const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
27
+ const existing = catalog?.categories.find((category) => category.id === editing?.id);
28
+ const assignedFolders = catalog?.folders.filter((item) => item.categoryId === editing?.id) ?? [];
29
+ const [busy, setBusy] = useState(false);
30
+ const [message, setMessage] = useState('');
31
+ const [baseline, setBaseline] = useState(() => JSON.stringify(editing));
32
+ const [tab, setTab] = useState('Basics');
33
+ const [search, setSearch] = useState('');
34
+ const [pending, setPending] = useState<(() => void) | null>(null);
35
+ const dirty = !folder && JSON.stringify(editing) !== baseline;
36
+ const requestAction = (action: () => void) => {
37
+ if (!knowledgeLifecycleEnabled.value || busy || pending) return;
38
+ if (dirty) setPending(() => action);
39
+ else action();
40
+ };
41
+ const handleEscape = () => {
42
+ if (!knowledgeLifecycleEnabled.value) return;
43
+ if (pending) setPending(null);
44
+ else if (confirmDelete) setConfirmDelete(null);
45
+ else requestAction(onClose);
46
+ };
47
+ useFocusTrap(ref, enabled, { onEscape: handleEscape });
48
+ useLayoutEffect(() => {
49
+ if (!enabled && ref.current?.contains(document.activeElement))
50
+ document.querySelector<HTMLElement>('button[aria-label="Settings"]')?.focus({ preventScroll: true });
51
+ }, [enabled]);
52
+ useEffect(() => {
53
+ if (enabled) void refreshKnowledge();
54
+ }, [enabled]);
55
+ const select = (category: KnowledgeCategory) => {
56
+ const draft = structuredClone(category);
57
+ setEditing(draft);
58
+ setBaseline(JSON.stringify(draft));
59
+ setTab('Basics');
60
+ setConfirmDelete(null);
61
+ setMessage('');
62
+ };
63
+ const write = async (path: string, body: unknown) => {
64
+ if (!knowledgeLifecycleEnabled.value) return false;
65
+ const result = await requestJson<{ ok: boolean } | null>('knowledgeWrite', `/api/canvas/knowledge/${path}`, null, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify(body),
69
+ });
70
+ return result?.ok === true;
71
+ };
72
+ const adopt = async (name: string) => {
73
+ if (!knowledgeLifecycleEnabled.value || busy || !catalog) return;
74
+ const generation = lifecycleUiGeneration;
75
+ setBusy(true);
76
+ setMessage('');
77
+ let ok = true;
78
+ const preset = KNOWLEDGE_PRESETS[name];
79
+ for (const category of preset.categories) {
80
+ if (!knowledgeLifecycleEnabled.value || generation !== lifecycleUiGeneration) {
81
+ ok = false;
82
+ break;
83
+ }
84
+ if (!catalog.categories.some((item) => item.id === category.id)) ok = await write('category', { category });
85
+ if (!ok) break;
86
+ }
87
+ if (ok)
88
+ for (const [path, id] of Object.entries(preset.folders)) {
89
+ if (!knowledgeLifecycleEnabled.value || generation !== lifecycleUiGeneration) {
90
+ ok = false;
91
+ break;
92
+ }
93
+ if (!catalog.folders.some((item) => item.path === path))
94
+ ok = await write('folder', { path, categoryId: id, lifecycleOverride: null });
95
+ if (!ok) break;
96
+ }
97
+ await refreshKnowledge();
98
+ setBusy(false);
99
+ setMessage(
100
+ ok
101
+ ? name === 'Empty'
102
+ ? 'No starter data added. Existing data is unchanged.'
103
+ : 'Starter data added. Existing categories and folder settings were preserved.'
104
+ : 'Template stopped at a failed save. Saved entries remain; retry adds only missing entries.',
105
+ );
106
+ };
107
+ return (
108
+ <div
109
+ class="knowledge-backdrop"
110
+ hidden={!enabled}
111
+ style={enabled ? undefined : { display: 'none' }}
112
+ onKeyDown={(event) => {
113
+ if (event.key === 'Escape') {
114
+ event.stopPropagation();
115
+ event.preventDefault();
116
+ handleEscape();
117
+ }
118
+ }}
119
+ >
120
+ <div
121
+ ref={ref}
122
+ class={`knowledge-dialog${folder ? '' : ' is-category-editor'}`}
123
+ role="dialog"
124
+ aria-modal="true"
125
+ aria-label={folder ? 'Folder settings' : 'Categories'}
126
+ >
127
+ <header>
128
+ <div class="knowledge-dialog-title">
129
+ <h2>{folder ? `${folder.split('/').at(-1)} · Folder settings` : 'Workspace categories'}</h2>
130
+ {folder && <span class="knowledge-folder-path">{folder}</span>}
131
+ </div>
132
+ <button type="button" aria-label="Close settings" disabled={busy} onClick={() => requestAction(onClose)}>
133
+ ×
134
+ </button>
135
+ </header>
136
+ {message && <p role="status">{message}</p>}
137
+ {pending && (
138
+ <div role="group" aria-label="Discard category changes" class="category-discard">
139
+ <p>Discard unsaved category changes?</p>
140
+ <button type="button" onClick={() => setPending(null)}>
141
+ Keep editing
142
+ </button>
143
+ <button
144
+ type="button"
145
+ onClick={() => {
146
+ if (!knowledgeLifecycleEnabled.value) return;
147
+ const action = pending;
148
+ setPending(null);
149
+ setBaseline(JSON.stringify(editing));
150
+ action();
151
+ }}
152
+ >
153
+ Discard changes
154
+ </button>
155
+ </div>
156
+ )}
157
+ {!catalog ? (
158
+ <p>Knowledge settings unavailable. Close and retry.</p>
159
+ ) : folder ? (
160
+ <form
161
+ onSubmit={async (event) => {
162
+ event.preventDefault();
163
+ if (!knowledgeLifecycleEnabled.value || busy) return;
164
+ const generation = lifecycleUiGeneration;
165
+ setBusy(true);
166
+ const ok = await write('folder', {
167
+ path: folder,
168
+ categoryId: categoryId || null,
169
+ lifecycleOverride: override ? cleanLifecycle(policy) : null,
170
+ });
171
+ await refreshKnowledge();
172
+ setBusy(false);
173
+ if (ok && knowledgeLifecycleEnabled.value && generation === lifecycleUiGeneration) onClose();
174
+ else if (!ok) setMessage('Folder settings were not saved. Retry.');
175
+ }}
176
+ >
177
+ <div class="knowledge-field">
178
+ <span>Category</span>
179
+ <ChoiceMenu
180
+ label="Category"
181
+ value={categoryId}
182
+ onChange={setCategoryId}
183
+ options={[
184
+ { value: '', label: 'Use the parent’s' },
185
+ ...catalog.categories.map((category) => ({
186
+ value: category.id,
187
+ label: `${category.glyph} ${category.name}`,
188
+ })),
189
+ ]}
190
+ />
191
+ </div>
192
+ <p>
193
+ Currently{' '}
194
+ {effective?.category
195
+ ? `${effective.category.glyph} ${effective.category.name} · ${effective.categorySourceFolder === folder ? 'set here' : `inherited from ${effective.categorySourceFolder}`}`
196
+ : 'unknown'}
197
+ . Cards inherit their category; only lifecycle can be overridden.
198
+ </p>
199
+ <label>
200
+ <input
201
+ type="checkbox"
202
+ checked={override}
203
+ onChange={(event) => setOverride(event.currentTarget.checked)}
204
+ />{' '}
205
+ Override lifecycle for this folder
206
+ </label>
207
+ {override ? (
208
+ <LifecycleFields value={policy} onChange={setPolicy} />
209
+ ) : (
210
+ <p>Lifecycle comes from the selected category or parent. Unknown has no passive expiry.</p>
211
+ )}
212
+ <button type="submit" disabled={busy}>
213
+ Save folder settings
214
+ </button>
215
+ </form>
216
+ ) : (
217
+ <>
218
+ <details class="category-templates">
219
+ <summary>Add from template</summary>
220
+ <div class="knowledge-presets">
221
+ {Object.entries(KNOWLEDGE_PRESETS).map(([name, preset]) => (
222
+ <button
223
+ type="button"
224
+ key={name}
225
+ disabled={busy || !!pending}
226
+ onClick={() =>
227
+ requestAction(() => {
228
+ if (dirty) {
229
+ setEditing(existing ? structuredClone(existing) : null);
230
+ setBaseline(JSON.stringify(existing ?? null));
231
+ }
232
+ void adopt(name);
233
+ })
234
+ }
235
+ >
236
+ <strong>{name}</strong>
237
+ <span>{Object.keys(preset.folders).join(' / ') || 'No folders or categories'}</span>
238
+ </button>
239
+ ))}
240
+ </div>
241
+ <p>Templates add missing data. They never replace your existing settings. Rename any category below.</p>
242
+ </details>
243
+ <div class="category-editor-columns">
244
+ <section class="category-list-panel">
245
+ <h3>{catalog.categories.length} categories</h3>
246
+ <label>
247
+ Search categories
248
+ <input type="search" value={search} onInput={(event) => setSearch(event.currentTarget.value)} />
249
+ </label>
250
+ <button
251
+ type="button"
252
+ disabled={busy}
253
+ onClick={() =>
254
+ requestAction(() => select({ ...DEFAULT_LIFECYCLE, id: crypto.randomUUID(), name: '', glyph: '◇' }))
255
+ }
256
+ >
257
+ Add category
258
+ </button>
259
+ <div class="category-library" role="group" aria-label="Workspace categories">
260
+ {catalog.categories
261
+ .filter((category) => category.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()))
262
+ .map((category) => (
263
+ <button
264
+ type="button"
265
+ class="category-choice"
266
+ key={category.id}
267
+ disabled={busy}
268
+ aria-pressed={editing?.id === category.id}
269
+ onClick={() => {
270
+ if (editing?.id !== category.id) requestAction(() => select(category));
271
+ }}
272
+ >
273
+ <strong>
274
+ {category.glyph} {category.name}
275
+ </strong>
276
+ <span>
277
+ {category.decayType} · {category.rate}
278
+ {category.rate === 'medium' && category.cadenceDays ? ` · ${category.cadenceDays} days` : ''}
279
+ </span>
280
+ </button>
281
+ ))}
282
+ </div>
283
+ <p>Folders with no category above them stay unknown. Categories carry no colour.</p>
284
+ </section>
285
+ {editing && (
286
+ <form
287
+ class="category-inspector"
288
+ noValidate
289
+ onSubmit={async (event) => {
290
+ event.preventDefault();
291
+ if (!knowledgeLifecycleEnabled.value || busy) return;
292
+ const generation = lifecycleUiGeneration;
293
+ const parsed = categorySchema.safeParse({ ...editing, ...cleanLifecycle(editing) });
294
+ if (!parsed.success) {
295
+ const field = String(parsed.error.issues[0].path[0]);
296
+ setTab(['name', 'glyph', 'id'].includes(field) ? 'Basics' : 'Review policy');
297
+ setMessage(parsed.error.issues[0].message);
298
+ setTimeout(() => {
299
+ if (!knowledgeLifecycleEnabled.value || generation !== lifecycleUiGeneration) return;
300
+ const label =
301
+ field === 'name'
302
+ ? 'Name'
303
+ : field === 'glyph'
304
+ ? 'Glyph'
305
+ : field === 'cadenceDays'
306
+ ? 'Cadence in days'
307
+ : 'How to check';
308
+ const labels = ref.current?.querySelectorAll('label');
309
+ Array.from(labels ?? [])
310
+ .find((item) => item.textContent?.trim().startsWith(label))
311
+ ?.querySelector<HTMLInputElement>('input')
312
+ ?.focus();
313
+ }, 0);
314
+ return;
315
+ }
316
+ setBusy(true);
317
+ const result = await requestJson<{ ok: boolean; category?: KnowledgeCategory } | null>(
318
+ 'knowledgeWrite',
319
+ '/api/canvas/knowledge/category',
320
+ null,
321
+ {
322
+ method: 'POST',
323
+ headers: { 'Content-Type': 'application/json' },
324
+ body: JSON.stringify({ category: parsed.data }),
325
+ },
326
+ );
327
+ const ok = result?.ok === true;
328
+ await refreshKnowledge();
329
+ setBusy(false);
330
+ if (!knowledgeLifecycleEnabled.value || generation !== lifecycleUiGeneration) return;
331
+ if (ok) {
332
+ const saved = result?.category ?? parsed.data;
333
+ setEditing(saved);
334
+ setBaseline(JSON.stringify(saved));
335
+ }
336
+ setMessage(
337
+ ok
338
+ ? 'Category saved. Inheriting folders now use these defaults.'
339
+ : 'Category was not saved. Retry.',
340
+ );
341
+ }}
342
+ >
343
+ <h3>{existing ? 'Edit category defaults' : 'New category'}</h3>
344
+ <div role="tablist" aria-label="Category settings" class="category-tabs">
345
+ {['Basics', 'Review policy', 'Usage'].map((name) => (
346
+ <button
347
+ key={name}
348
+ type="button"
349
+ role="tab"
350
+ id={`category-tab-${name.replace(' ', '-')}`}
351
+ aria-controls="category-tab-panel"
352
+ aria-selected={tab === name}
353
+ onClick={() => setTab(name)}
354
+ >
355
+ {name}
356
+ </button>
357
+ ))}
358
+ </div>
359
+ <fieldset
360
+ disabled={busy || !!pending}
361
+ id="category-tab-panel"
362
+ role="tabpanel"
363
+ aria-labelledby={`category-tab-${tab.replace(' ', '-')}`}
364
+ class="category-tab-content"
365
+ >
366
+ {tab === 'Basics' && (
367
+ <>
368
+ <label>
369
+ Name
370
+ <input
371
+ required
372
+ value={editing.name}
373
+ onInput={(event) => setEditing({ ...editing, name: event.currentTarget.value })}
374
+ />
375
+ </label>
376
+ <label>
377
+ Glyph
378
+ <input
379
+ required
380
+ value={editing.glyph}
381
+ onInput={(event) => setEditing({ ...editing, glyph: event.currentTarget.value })}
382
+ />
383
+ </label>
384
+ <div class="category-glyph-choices" role="group" aria-label="Glyph choices">
385
+ {[
386
+ ...new Set(
387
+ Object.values(KNOWLEDGE_PRESETS).flatMap((preset) =>
388
+ preset.categories.map((category) => category.glyph),
389
+ ),
390
+ ),
391
+ ].map((glyph) => (
392
+ <button
393
+ key={glyph}
394
+ type="button"
395
+ aria-label={`Choose glyph ${glyph}`}
396
+ aria-pressed={editing.glyph === glyph}
397
+ onClick={() => setEditing({ ...editing, glyph })}
398
+ >
399
+ {glyph}
400
+ </button>
401
+ ))}
402
+ </div>
403
+ </>
404
+ )}
405
+ {tab === 'Review policy' && (
406
+ <LifecycleFields value={editing} onChange={(value) => setEditing({ ...editing, ...value })} />
407
+ )}
408
+ {tab === 'Usage' && (
409
+ <>
410
+ <p>
411
+ Folders inherit these defaults. Folder settings and card lifecycle overrides remain unchanged.
412
+ Unknown has no passive expiry.
413
+ </p>
414
+ {assignedFolders.length === 0 && <p>No folders assigned.</p>}
415
+ {existing &&
416
+ (assignedFolders.length > 0 ? (
417
+ <p>
418
+ Cannot delete: assigned to {assignedFolders.map((item) => item.path).join(', ')}. Reassign
419
+ or clear those folder categories first. Empty folders also count.
420
+ </p>
421
+ ) : confirmDelete === existing.id ? (
422
+ <div role="group" aria-label="Confirm category deletion">
423
+ <p>Delete {existing.name}? This removes the category from this workspace.</p>
424
+ <button type="button" disabled={busy} onClick={() => setConfirmDelete(null)}>
425
+ Cancel deletion
426
+ </button>
427
+ <button
428
+ type="button"
429
+ class="is-danger"
430
+ disabled={busy}
431
+ onClick={async () => {
432
+ if (!knowledgeLifecycleEnabled.value || busy) return;
433
+ const generation = lifecycleUiGeneration;
434
+ setBusy(true);
435
+ const result = await requestJson<{ ok: boolean; error?: string } | null>(
436
+ 'knowledgeRemove',
437
+ `/api/canvas/knowledge/category/${encodeURIComponent(existing.id)}`,
438
+ null,
439
+ { method: 'DELETE' },
440
+ );
441
+ await refreshKnowledge();
442
+ setBusy(false);
443
+ if (!knowledgeLifecycleEnabled.value || generation !== lifecycleUiGeneration) return;
444
+ setConfirmDelete(null);
445
+ if (result?.ok === true) {
446
+ setEditing(null);
447
+ setBaseline('null');
448
+ }
449
+ setMessage(
450
+ result?.ok === true
451
+ ? 'Category deleted.'
452
+ : (result?.error ?? 'Category was not deleted. Retry.'),
453
+ );
454
+ }}
455
+ >
456
+ Confirm deletion
457
+ </button>
458
+ </div>
459
+ ) : (
460
+ <button
461
+ type="button"
462
+ class="is-danger"
463
+ disabled={busy}
464
+ onClick={() => setConfirmDelete(existing.id)}
465
+ >
466
+ Delete category
467
+ </button>
468
+ ))}
469
+ </>
470
+ )}
471
+ </fieldset>
472
+ <footer class="category-editor-footer">
473
+ <button type="button" disabled={busy || !!pending} onClick={() => requestAction(onClose)}>
474
+ Cancel
475
+ </button>
476
+ <button type="submit" disabled={busy || !!pending}>
477
+ Save category
478
+ </button>
479
+ </footer>
480
+ </form>
481
+ )}
482
+ </div>
483
+ </>
484
+ )}
485
+ </div>
486
+ </div>
487
+ );
488
+ }
@@ -0,0 +1,133 @@
1
+ import { useEffect, useState } from 'preact/hooks';
2
+ import { knowledgeLifecycleEnabled } from '../state/workspace-settings';
3
+ import { activeBoardId, boardList, boardIncarnation } from '../state/boards-store';
4
+ import { nodes, edges } from '../state/canvas-store';
5
+ import { requestJson } from '../state/intent-bridge';
6
+ import {
7
+ cardQualification,
8
+ knowledgeUnavailable,
9
+ lifecycleFilter,
10
+ openLifecycle,
11
+ refreshKnowledge,
12
+ retirementUndo,
13
+ lifecycleTarget,
14
+ } from '../state/knowledge-store';
15
+
16
+ export function KnowledgeSync() {
17
+ const board = activeBoardId.value;
18
+ const boards = boardList.value;
19
+ const cards = nodes.value;
20
+ const links = edges.value;
21
+ const enabled = knowledgeLifecycleEnabled.value;
22
+ useEffect(() => {
23
+ const timer = setTimeout(() => void refreshKnowledge(), 100);
24
+ return () => clearTimeout(timer);
25
+ }, [board, boards, cards, links, enabled]);
26
+ useEffect(() => {
27
+ const timer = setInterval(() => void refreshKnowledge(), 60000);
28
+ return () => clearInterval(timer);
29
+ }, []);
30
+ return null;
31
+ }
32
+
33
+ export function LifecycleFilters() {
34
+ if (!knowledgeLifecycleEnabled.value) return null;
35
+ const counts = [...nodes.value.keys()].map(cardQualification);
36
+ return (
37
+ <>
38
+ <div class="lifecycle-filters" role="group" aria-label="Lifecycle filter">
39
+ {(['All', 'To review', 'Retired'] as const).map((filter) => (
40
+ <button
41
+ type="button"
42
+ key={filter}
43
+ aria-pressed={lifecycleFilter.value === filter}
44
+ onClick={() => {
45
+ if (!knowledgeLifecycleEnabled.value) return;
46
+ lifecycleFilter.value = filter;
47
+ }}
48
+ >
49
+ {filter}
50
+ {filter === 'All'
51
+ ? ''
52
+ : ` · ${counts.filter((status) => (filter === 'Retired' ? status?.retired : !status?.retired && (status?.due || status?.sources.length))).length}`}
53
+ </button>
54
+ ))}
55
+ {knowledgeUnavailable.value && <span role="status">Qualification unavailable</span>}
56
+ </div>
57
+ <RetirementToast />
58
+ </>
59
+ );
60
+ }
61
+
62
+ function RetirementToast() {
63
+ const target = retirementUndo.value;
64
+ const [busy, setBusy] = useState(false);
65
+ if (
66
+ !knowledgeLifecycleEnabled.value ||
67
+ !target ||
68
+ target.boardId !== activeBoardId.value ||
69
+ target.incarnation !== boardIncarnation ||
70
+ lifecycleTarget.value ||
71
+ !cardQualification(target.nodeId)?.retired
72
+ )
73
+ return null;
74
+ return (
75
+ <div class="retirement-toast" role="status">
76
+ <span>Retired from active context. History kept.</span>
77
+ <button
78
+ type="button"
79
+ disabled={busy}
80
+ onClick={async () => {
81
+ if (busy || !knowledgeLifecycleEnabled.value) return;
82
+ setBusy(true);
83
+ const result = await requestJson<{ ok: boolean } | null>(
84
+ 'restore',
85
+ `/api/canvas/node/${encodeURIComponent(target.nodeId)}/lifecycle/restore`,
86
+ null,
87
+ {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({ expectedBoardId: target.boardId, evidence: 'Human undid retirement.' }),
91
+ },
92
+ );
93
+ if (result?.ok) {
94
+ retirementUndo.value = null;
95
+ await refreshKnowledge();
96
+ }
97
+ setBusy(false);
98
+ }}
99
+ >
100
+ Undo retirement
101
+ </button>
102
+ <button
103
+ type="button"
104
+ aria-label="Dismiss retirement notice"
105
+ onClick={() => {
106
+ retirementUndo.value = null;
107
+ }}
108
+ >
109
+ ×
110
+ </button>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ export function LifecycleFooter({ nodeId }: { nodeId: string }) {
116
+ if (!knowledgeLifecycleEnabled.value) return null;
117
+ const status = cardQualification(nodeId);
118
+ if (!status || status.retired || (!status.due && status.sources.length === 0)) return null;
119
+ const source = status.sources[0];
120
+ return (
121
+ <button
122
+ type="button"
123
+ class="node-lifecycle-warning"
124
+ onClick={(event) => {
125
+ event.stopPropagation();
126
+ openLifecycle(nodeId);
127
+ }}
128
+ >
129
+ <span>{status.due ? '⧗ review due' : '⧗ source needs review'}</span>
130
+ <span>{status.due ? status.reasons[0] : `${source.title} · ${source.reasons[0]}`}</span>
131
+ </button>
132
+ );
133
+ }