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,91 @@
1
+ import type { Lifecycle } from '../../server/knowledge-lifecycle.js';
2
+ import { ChoiceMenu } from './ChoiceMenu';
3
+
4
+ export const DEFAULT_LIFECYCLE: Lifecycle = {
5
+ decayType: 'reality',
6
+ rate: 'medium',
7
+ cadenceDays: 90,
8
+ triggers: [],
9
+ revalidationMethod: '',
10
+ };
11
+
12
+ export function LifecycleFields({ value, onChange }: { value: Lifecycle; onChange: (value: Lifecycle) => void }) {
13
+ return (
14
+ <div class="lifecycle-fields">
15
+ <div class="knowledge-field">
16
+ <span>Why review?</span>
17
+ <ChoiceMenu
18
+ label="Why review?"
19
+ value={value.decayType}
20
+ onChange={(decayType) => onChange({ ...value, decayType: decayType as Lifecycle['decayType'] })}
21
+ options={[
22
+ { value: 'reality', label: 'Reality · facts change' },
23
+ { value: 'decision', label: 'Decision · superseded' },
24
+ { value: 'dependency', label: 'Dependency · prerequisites change' },
25
+ { value: 'relevance', label: 'Relevance · task changes' },
26
+ ]}
27
+ />
28
+ </div>
29
+ <div class="knowledge-field">
30
+ <span>Review frequency</span>
31
+ <ChoiceMenu
32
+ label="Review frequency"
33
+ value={value.rate}
34
+ onChange={(rate) => onChange({ ...value, rate: rate as Lifecycle['rate'] })}
35
+ options={[
36
+ { value: 'fast', label: 'Fast · at use' },
37
+ { value: 'medium', label: 'Medium · on a cadence' },
38
+ { value: 'slow', label: 'Slow · on resume' },
39
+ { value: 'durable', label: 'Durable · no passive expiry' },
40
+ ]}
41
+ />
42
+ </div>
43
+ {value.rate === 'medium' && (
44
+ <label>
45
+ Cadence in days
46
+ <input
47
+ type="number"
48
+ min="1"
49
+ max="36500"
50
+ required
51
+ value={value.cadenceDays ?? ''}
52
+ onInput={(event) =>
53
+ onChange({
54
+ ...value,
55
+ cadenceDays: event.currentTarget.value ? Number(event.currentTarget.value) : undefined,
56
+ })
57
+ }
58
+ />
59
+ </label>
60
+ )}
61
+ <details class="lifecycle-triggers">
62
+ <summary>Additional review triggers</summary>
63
+ <label>
64
+ Also review when · one trigger per line
65
+ <textarea
66
+ value={value.triggers.join('\n')}
67
+ onInput={(event) => onChange({ ...value, triggers: event.currentTarget.value.split('\n') })}
68
+ />
69
+ </label>
70
+ <p>
71
+ Source changes, contradictions and superseding decisions are observed through explicit relations. Other
72
+ triggers describe what the reviewer should check.
73
+ </p>
74
+ </details>
75
+ <label>
76
+ How to check
77
+ <input
78
+ required
79
+ pattern={'.*\\S.*'}
80
+ value={value.revalidationMethod}
81
+ onInput={(event) => onChange({ ...value, revalidationMethod: event.currentTarget.value })}
82
+ />
83
+ </label>
84
+ </div>
85
+ );
86
+ }
87
+
88
+ export const cleanLifecycle = (value: Lifecycle): Lifecycle => ({
89
+ ...value,
90
+ triggers: value.triggers.map((s) => s.trim()).filter(Boolean),
91
+ });
@@ -0,0 +1,533 @@
1
+ import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
2
+ import { knowledgeLifecycleEnabled } from '../state/workspace-settings';
3
+ import type { Lifecycle, LifecycleResults, ReviewRecord } from '../../server/knowledge-lifecycle.js';
4
+ import {
5
+ activeBoardId,
6
+ boardIncarnation,
7
+ browsingMemory,
8
+ lifecycleUiGeneration,
9
+ openMemoryTarget,
10
+ } from '../state/boards-store';
11
+ import { focusNode, nodes } from '../state/canvas-store';
12
+ import { requestJson } from '../state/intent-bridge';
13
+ import {
14
+ cardQualification,
15
+ knowledgeUnavailable,
16
+ lifecycleTarget,
17
+ refreshKnowledge,
18
+ retirementUndo,
19
+ } from '../state/knowledge-store';
20
+ import { sendSteering } from '../state/session-store';
21
+ import { hasUnsavedMarkdown } from '../nodes/MarkdownNode';
22
+ import { cleanLifecycle, DEFAULT_LIFECYCLE, LifecycleFields } from './LifecycleFields';
23
+
24
+ export function LifecyclePanel() {
25
+ const target = lifecycleTarget.value;
26
+ if (
27
+ !target ||
28
+ browsingMemory.value ||
29
+ activeBoardId.value !== target.boardId ||
30
+ boardIncarnation !== target.incarnation ||
31
+ !nodes.value.has(target.nodeId)
32
+ )
33
+ return null;
34
+ return <LifecycleDialog key={`${target.boardId}:${target.nodeId}:${target.incarnation}`} target={target} />;
35
+ }
36
+
37
+ function LifecycleDialog({ target }: { target: NonNullable<typeof lifecycleTarget.value> }) {
38
+ const enabled = knowledgeLifecycleEnabled.value;
39
+ const ref = useRef<HTMLDivElement>(null);
40
+ const loadSequence = useRef(0);
41
+ const [detail, setDetail] = useState<LifecycleResults['get'] | null>(null);
42
+ const [override, setOverride] = useState(false);
43
+ const [policy, setPolicy] = useState<Lifecycle>(DEFAULT_LIFECYCLE);
44
+ const [outcome, setOutcome] = useState<Exclude<ReviewRecord['outcome'], 'restore'>>('retain');
45
+ const [evidence, setEvidence] = useState('');
46
+ const [applied, setApplied] = useState(false);
47
+ const [busy, setBusy] = useState(false);
48
+ const [error, setError] = useState('');
49
+ const [retiredHere, setRetiredHere] = useState(false);
50
+ const [needsReload, setNeedsReload] = useState(false);
51
+ const node = nodes.value.get(target.nodeId);
52
+ const unsavedContent = hasUnsavedMarkdown(target.boardId, target.nodeId);
53
+ const current = cardQualification(target.nodeId) ?? detail?.qualification;
54
+ const path = `/api/canvas/node/${encodeURIComponent(target.nodeId)}`;
55
+ const sameBoard = () => activeBoardId.value === target.boardId && boardIncarnation === target.incarnation;
56
+ const ownsDialog = () => knowledgeLifecycleEnabled.value && sameBoard() && lifecycleTarget.value === target;
57
+ const load = async () => {
58
+ if (!ownsDialog()) return;
59
+ const generation = lifecycleUiGeneration;
60
+ const sequence = ++loadSequence.current;
61
+ const result = await requestJson<LifecycleResults['get'] | null>(
62
+ 'lifecycle',
63
+ `${path}/lifecycle?board=${encodeURIComponent(target.boardId)}`,
64
+ null,
65
+ );
66
+ if (!ownsDialog() || generation !== lifecycleUiGeneration || sequence !== loadSequence.current) return;
67
+ if (result?.ok !== true) {
68
+ setError('Could not load lifecycle. Retry before reviewing.');
69
+ return;
70
+ }
71
+ setDetail(result);
72
+ setNeedsReload(false);
73
+ setError('');
74
+ setOverride(result.lifecycleSource === 'card');
75
+ setPolicy(result.lifecycle ?? DEFAULT_LIFECYCLE);
76
+ // New preconditions must never be attached to evidence for an earlier observation.
77
+ setEvidence('');
78
+ setApplied(false);
79
+ };
80
+ useEffect(() => {
81
+ const previous = document.activeElement;
82
+ const dock = ref.current;
83
+ if (knowledgeLifecycleEnabled.value) {
84
+ dock?.querySelector<HTMLElement>('button')?.focus({ preventScroll: true });
85
+ // The dock has committed its new geometry before focusing the real card.
86
+ focusNode(target.nodeId, { recordHistory: false });
87
+ }
88
+ return () => {
89
+ if (
90
+ dock?.contains(document.activeElement) &&
91
+ sameBoard() &&
92
+ previous instanceof HTMLElement &&
93
+ previous.isConnected
94
+ )
95
+ previous.focus({ preventScroll: true });
96
+ };
97
+ }, []);
98
+ useLayoutEffect(() => {
99
+ if (!enabled && ref.current?.contains(document.activeElement))
100
+ document.querySelector<HTMLElement>('button[aria-label="Settings"]')?.focus({ preventScroll: true });
101
+ }, [enabled]);
102
+ useEffect(() => {
103
+ if (!enabled) return;
104
+ // Refresh qualification, not the observation to which the existing draft refers.
105
+ void refreshKnowledge();
106
+ if (!detail || needsReload) void load();
107
+ }, [enabled]);
108
+ const mutate = async (suffix: string, body: Record<string, unknown>) => {
109
+ if (!ownsDialog() || busy || needsReload) return false;
110
+ setBusy(true);
111
+ setError('');
112
+ const result = await requestJson<{ ok: boolean } | null>('lifecycleWrite', `${path}${suffix}`, null, {
113
+ method: 'POST',
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify({ ...body, expectedBoardId: target.boardId }),
116
+ });
117
+ if (!sameBoard() || lifecycleTarget.value !== target) {
118
+ setBusy(false);
119
+ return false;
120
+ }
121
+ if (!result?.ok) {
122
+ setBusy(false);
123
+ setError(
124
+ 'Not saved. The card or its sources may have changed. Reload and check the current evidence before retrying.',
125
+ );
126
+ return false;
127
+ }
128
+ // Invalidate acknowledged preconditions without unmounting focused controls.
129
+ // If suspended, resume reloads before another mutation is allowed.
130
+ setNeedsReload(true);
131
+ await load();
132
+ await refreshKnowledge();
133
+ setBusy(false);
134
+ return sameBoard() && lifecycleTarget.value === target;
135
+ };
136
+ const recordReview = async (reviewOutcome: typeof outcome, wasApplied: boolean) => {
137
+ if (!detail) return false;
138
+ if (
139
+ wasApplied &&
140
+ ['retain', 'revise', 'externalize'].includes(reviewOutcome) &&
141
+ (!evidence.trim() ||
142
+ hasUnsavedMarkdown(target.boardId, target.nodeId) ||
143
+ node?.contentRevision !== detail.contentRevision ||
144
+ error)
145
+ )
146
+ return false;
147
+ const ok = await mutate('/review', {
148
+ outcome: reviewOutcome,
149
+ evidence,
150
+ applied: wasApplied,
151
+ expectedContentRevision: detail.contentRevision,
152
+ expectedSources: detail.qualification.observations,
153
+ });
154
+ if (ok) {
155
+ setRetiredHere(wasApplied && reviewOutcome === 'retire');
156
+ if (wasApplied && reviewOutcome === 'retire') retirementUndo.value = target;
157
+ setEvidence('');
158
+ setApplied(false);
159
+ }
160
+ return ok;
161
+ };
162
+ const recipe = node?.data.provenance as { recipe?: { tool: string; input?: unknown; prompt: string } } | undefined;
163
+ const date = (value: string | null | undefined) => (value ? new Date(value).toLocaleString() : 'Unknown');
164
+ return (
165
+ <div
166
+ class="lifecycle-dock"
167
+ hidden={!enabled}
168
+ style={enabled ? undefined : { display: 'none' }}
169
+ onKeyDown={(event) => {
170
+ if (!ownsDialog()) return;
171
+ // This dialog owns its keys, including Space on the disclosure summary.
172
+ event.stopPropagation();
173
+ if (event.key === 'Escape') {
174
+ event.preventDefault();
175
+ lifecycleTarget.value = null;
176
+ }
177
+ }}
178
+ >
179
+ <div ref={ref} class="knowledge-dialog lifecycle-dialog" role="dialog" aria-label="Review card">
180
+ <header>
181
+ <h2>Review card · {String(node?.data.title ?? 'Card')}</h2>
182
+ <button
183
+ type="button"
184
+ aria-label="Close review"
185
+ onClick={() => {
186
+ if (!ownsDialog()) return;
187
+ lifecycleTarget.value = null;
188
+ }}
189
+ >
190
+ ×
191
+ </button>
192
+ </header>
193
+ {error && <p role="alert">{error}</p>}
194
+ {!detail && error && (
195
+ <button
196
+ type="button"
197
+ aria-disabled={busy}
198
+ onClick={() => {
199
+ if (!busy) void load();
200
+ }}
201
+ >
202
+ Reload and recheck
203
+ </button>
204
+ )}
205
+ {knowledgeUnavailable.value && (
206
+ <p role="status">Live qualification unavailable. Last loaded evidence is shown.</p>
207
+ )}
208
+ {!detail ? (
209
+ <p>Loading review…</p>
210
+ ) : (
211
+ <>
212
+ {detail.retired ? (
213
+ <div class="lifecycle-review">
214
+ <p>This card is retired. Restoring it does not mark it checked.</p>
215
+ <button
216
+ type="button"
217
+ aria-disabled={busy || needsReload}
218
+ onClick={() => void mutate('/lifecycle/restore', { evidence: 'Restored by the human reviewer.' })}
219
+ >
220
+ Restore card
221
+ </button>
222
+ </div>
223
+ ) : (
224
+ <>
225
+ <h3>Why review?</h3>
226
+ {current?.due ? (
227
+ <p class="lifecycle-warning">
228
+ ⧗{' '}
229
+ {current.reasons.some((reason) => reason.startsWith('Content changed'))
230
+ ? 'This card changed since its last check.'
231
+ : 'This card needs a fresh check.'}
232
+ </p>
233
+ ) : (
234
+ <p>Last checked: {date(detail.lastValidation?.when)}</p>
235
+ )}
236
+ {current?.reasons.length ? <p>{current.reasons.join(' ')}</p> : null}
237
+ {(current?.sources ?? detail.qualification.sources).map((source) => (
238
+ <div class="lifecycle-warning" key={`${source.boardId}:${source.cardId}`}>
239
+ ⧗ Source needs a check: {source.title}
240
+ {!source.unavailable && (
241
+ <button
242
+ type="button"
243
+ onClick={() => {
244
+ if (!ownsDialog()) return;
245
+ lifecycleTarget.value = null;
246
+ void openMemoryTarget(source);
247
+ }}
248
+ >
249
+ Open source →
250
+ </button>
251
+ )}
252
+ </div>
253
+ ))}
254
+ <form
255
+ class="lifecycle-quick-review"
256
+ noValidate
257
+ onSubmit={(event) => {
258
+ event.preventDefault();
259
+ void recordReview('retain', true).then((ok) => {
260
+ if (ok && ownsDialog()) lifecycleTarget.value = null;
261
+ });
262
+ }}
263
+ >
264
+ <p>Check the card and its sources before confirming.</p>
265
+ {unsavedContent && (
266
+ <p role="status">Save the card's edits, then reload and recheck before validating.</p>
267
+ )}
268
+ <label>
269
+ What did you check?
270
+ <textarea
271
+ required
272
+ rows={2}
273
+ placeholder="A short note or source link"
274
+ value={evidence}
275
+ onInput={(event) => setEvidence(event.currentTarget.value)}
276
+ />
277
+ </label>
278
+ <div class="lifecycle-review-actions">
279
+ <button
280
+ type="submit"
281
+ aria-disabled={
282
+ busy ||
283
+ needsReload ||
284
+ !!error ||
285
+ unsavedContent ||
286
+ !evidence.trim() ||
287
+ node?.contentRevision !== detail.contentRevision
288
+ }
289
+ >
290
+ Checked, still correct
291
+ </button>
292
+ <button
293
+ type="button"
294
+ aria-disabled={busy}
295
+ onClick={() => {
296
+ if (!ownsDialog() || busy) return;
297
+ lifecycleTarget.value = null;
298
+ }}
299
+ >
300
+ Not now
301
+ </button>
302
+ </div>
303
+ </form>
304
+ </>
305
+ )}
306
+ {(error || node?.contentRevision !== detail.contentRevision) && (
307
+ <div role="status">
308
+ <p>Reload, check the current card and sources, then write a fresh note.</p>
309
+ <button
310
+ type="button"
311
+ aria-disabled={busy}
312
+ onClick={() => {
313
+ if (!busy) void load();
314
+ }}
315
+ >
316
+ Reload and recheck
317
+ </button>
318
+ </div>
319
+ )}
320
+ <details class="lifecycle-advanced">
321
+ <summary>Settings and history</summary>
322
+ <button
323
+ type="button"
324
+ aria-disabled={busy}
325
+ onClick={() => {
326
+ if (!busy) void load();
327
+ }}
328
+ >
329
+ Reload lifecycle and source revisions
330
+ </button>
331
+ <p class="lifecycle-category">
332
+ {detail.category
333
+ ? `${detail.category.glyph} ${detail.category.name} · from ${detail.categorySourceFolder}`
334
+ : 'No inherited category'}{' '}
335
+ · category belongs to the folder
336
+ </p>
337
+ <form
338
+ onSubmit={(event) => {
339
+ event.preventDefault();
340
+ void mutate('/lifecycle', { lifecycleOverride: override ? cleanLifecycle(policy) : null });
341
+ }}
342
+ >
343
+ <label>
344
+ <input
345
+ type="checkbox"
346
+ checked={override}
347
+ onChange={(event) => setOverride(event.currentTarget.checked)}
348
+ />{' '}
349
+ Set a card lifecycle override
350
+ </label>
351
+ {override ? (
352
+ <LifecycleFields value={policy} onChange={setPolicy} />
353
+ ) : (
354
+ <p>
355
+ Inherit from {detail.lifecycleSourceFolder ?? 'the folder'} · {detail.lifecycle?.rate ?? 'unknown'}
356
+ </p>
357
+ )}
358
+ <button type="submit" aria-disabled={busy || needsReload}>
359
+ Save lifecycle
360
+ </button>
361
+ </form>
362
+ <div class="lifecycle-clocks">
363
+ <div class="is-read">
364
+ <span>Read by agent</span>
365
+ <strong>{date(detail.qualification.lastReadAt)}</strong>
366
+ <small>Delivery only, not validation</small>
367
+ </div>
368
+ <div>
369
+ <span>Content changed</span>
370
+ <strong>{date(detail.qualification.contentChangedAt)}</strong>
371
+ <small>Revision {detail.contentRevision}</small>
372
+ </div>
373
+ <div>
374
+ <span>Last validated</span>
375
+ <strong>{date(detail.lastValidation?.when)}</strong>
376
+ <small>{detail.lastValidation?.evidence ?? 'No evidence-backed validation'}</small>
377
+ </div>
378
+ <div>
379
+ <span>Task relevance</span>
380
+ <strong>
381
+ {detail.qualification.relevance === 'card-pinned'
382
+ ? 'Card pinned'
383
+ : detail.qualification.relevance === 'board-pinned'
384
+ ? 'Board pinned'
385
+ : 'Not explicitly selected'}
386
+ </strong>
387
+ <small>Curation, not a truth claim</small>
388
+ </div>
389
+ </div>
390
+ <p>Reading, refreshing and regeneration never count as validating.</p>
391
+ {current?.due && <p class="lifecycle-warning">⧗ review due · {current.reasons.join(' ')}</p>}
392
+ {(current?.sources ?? detail.qualification.sources).map((source) => (
393
+ <div class="lifecycle-warning" key={`${source.boardId}:${source.cardId}`}>
394
+ ⧗ {source.title} · {source.reasons.join(' ')}
395
+ {source.observedAt && <span> · observed {date(source.observedAt)}</span>}
396
+ {source.observedRevision !== undefined && <span> · source revision {source.observedRevision}</span>}
397
+ {!source.unavailable && (
398
+ <button
399
+ type="button"
400
+ onClick={() => {
401
+ if (!ownsDialog()) return;
402
+ lifecycleTarget.value = null;
403
+ void openMemoryTarget(source);
404
+ }}
405
+ >
406
+ Open source →
407
+ </button>
408
+ )}
409
+ </div>
410
+ ))}
411
+ {node?.contentRevision !== detail.contentRevision && (
412
+ <p role="alert">Content changed while this review was open. Reload and check it before applying.</p>
413
+ )}
414
+ {detail.retired ? (
415
+ <div class="lifecycle-review">
416
+ <p role="status">Retired from active context. Content and history remain.</p>
417
+ <label>
418
+ Restoration note
419
+ <input value={evidence} onInput={(event) => setEvidence(event.currentTarget.value)} />
420
+ </label>
421
+ <button
422
+ type="button"
423
+ aria-disabled={busy || needsReload}
424
+ onClick={() =>
425
+ void mutate('/lifecycle/restore', {
426
+ evidence: evidence || 'Restored by the human reviewer.',
427
+ }).then((ok) => {
428
+ if (ok) setRetiredHere(false);
429
+ })
430
+ }
431
+ >
432
+ {retiredHere ? 'Undo retirement' : 'Restore to active context'}
433
+ </button>
434
+ <p>Restore is recorded and does not validate.</p>
435
+ </div>
436
+ ) : (
437
+ <form
438
+ class="lifecycle-review"
439
+ onSubmit={(event) => {
440
+ event.preventDefault();
441
+ void recordReview(outcome, applied);
442
+ }}
443
+ >
444
+ <label>
445
+ Outcome
446
+ <select
447
+ value={outcome}
448
+ onChange={(event) => setOutcome(event.currentTarget.value as typeof outcome)}
449
+ >
450
+ <option value="retain">Retain</option>
451
+ <option value="revise">Revise</option>
452
+ <option value="externalize">Externalize</option>
453
+ <option value="retire">Retire from active context</option>
454
+ <option value="unresolved">Unresolved</option>
455
+ </select>
456
+ </label>
457
+ <p>Uses the check note above. Applied retain, revise and externalize require evidence.</p>
458
+ <label>
459
+ <input
460
+ type="checkbox"
461
+ checked={applied}
462
+ onChange={(event) => setApplied(event.currentTarget.checked)}
463
+ />{' '}
464
+ I applied this outcome and checked the content and sources
465
+ </label>
466
+ <p>Revise and externalize record work already performed. They do not edit or execute anything.</p>
467
+ <button
468
+ type="submit"
469
+ aria-disabled={
470
+ busy ||
471
+ needsReload ||
472
+ (applied &&
473
+ ['retain', 'revise', 'externalize'].includes(outcome) &&
474
+ (!evidence.trim() ||
475
+ unsavedContent ||
476
+ !!error ||
477
+ node?.contentRevision !== detail.contentRevision))
478
+ }
479
+ >
480
+ Record review
481
+ </button>
482
+ </form>
483
+ )}
484
+ {recipe?.recipe && (
485
+ <section>
486
+ <h3>Recipe</h3>
487
+ <p>
488
+ {recipe.recipe.tool} · {recipe.recipe.prompt}
489
+ </p>
490
+ <button
491
+ type="button"
492
+ aria-disabled={busy}
493
+ onClick={async () => {
494
+ if (!ownsDialog() || busy) return;
495
+ setBusy(true);
496
+ const ok = await sendSteering(
497
+ `Refresh this board using the recorded recipe for ${target.boardId}/${target.nodeId}. Review the recipe before running it. Tool: ${recipe.recipe!.tool}\nInput: ${JSON.stringify(recipe.recipe!.input ?? null)}\nPrompt: ${recipe.recipe!.prompt}\nRegeneration is not validation. Preserve source observations and report evidence separately.`,
498
+ );
499
+ setBusy(false);
500
+ setError(
501
+ ok
502
+ ? 'Recipe request sent. No tool was executed by Canvas.'
503
+ : 'Could not send the request. No tool was executed.',
504
+ );
505
+ }}
506
+ >
507
+ Ask agent to refresh this board
508
+ </button>
509
+ </section>
510
+ )}
511
+ <section>
512
+ <h3>Review history</h3>
513
+ {detail.records.length === 0 && <p>No reviews recorded.</p>}
514
+ {[...detail.records].reverse().map((record) => (
515
+ <div class="lifecycle-history" key={record.id}>
516
+ <strong>
517
+ {record.outcome}
518
+ {!record.applied ? ' · not applied' : ''}
519
+ </strong>
520
+ <span>
521
+ {date(record.when)} · {record.who.actor} · {record.who.source}
522
+ </span>
523
+ <p>{record.evidence || 'No evidence'}</p>
524
+ </div>
525
+ ))}
526
+ </section>
527
+ </details>
528
+ </>
529
+ )}
530
+ </div>
531
+ </div>
532
+ );
533
+ }