pmx-canvas 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +59 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2724 -476
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2899 -596
  8. package/dist/json-render/index.js +20 -20
  9. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  10. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  11. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  12. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  13. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  14. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  15. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  16. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  17. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  19. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  20. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  21. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  22. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  23. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  24. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  25. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  26. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  27. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  28. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  29. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  30. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  31. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  32. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  33. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  34. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  35. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  36. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  37. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  38. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  39. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  40. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  41. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  42. package/dist/types/client/icons.d.ts +0 -10
  43. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  44. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  45. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  46. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  47. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  48. package/dist/types/client/state/boards-store.d.ts +81 -9
  49. package/dist/types/client/state/canvas-store.d.ts +14 -1
  50. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  51. package/dist/types/client/state/context-status-store.d.ts +35 -0
  52. package/dist/types/client/state/human-store.d.ts +2 -1
  53. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  54. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  55. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  56. package/dist/types/client/state/presence-store.d.ts +4 -4
  57. package/dist/types/client/state/presentation.d.ts +28 -0
  58. package/dist/types/client/state/relation-store.d.ts +10 -0
  59. package/dist/types/client/state/session-store.d.ts +41 -2
  60. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  61. package/dist/types/client/types.d.ts +13 -1
  62. package/dist/types/client/utils/keyboard.d.ts +2 -0
  63. package/dist/types/client/utils/time-ago.d.ts +4 -0
  64. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  65. package/dist/types/server/agent-presence.d.ts +10 -3
  66. package/dist/types/server/ax-context.d.ts +1 -1
  67. package/dist/types/server/canvas-db.d.ts +30 -7
  68. package/dist/types/server/canvas-operations.d.ts +24 -2
  69. package/dist/types/server/canvas-provenance.d.ts +22 -2
  70. package/dist/types/server/canvas-serialization.d.ts +2 -0
  71. package/dist/types/server/canvas-state.d.ts +93 -15
  72. package/dist/types/server/canvas-validation.d.ts +1 -1
  73. package/dist/types/server/context-brief.d.ts +94 -6
  74. package/dist/types/server/context-reads.d.ts +33 -7
  75. package/dist/types/server/edit-summary.d.ts +20 -0
  76. package/dist/types/server/index.d.ts +71 -10
  77. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  78. package/dist/types/server/lifecycle-status.d.ts +10 -0
  79. package/dist/types/server/memory-graph.d.ts +9 -0
  80. package/dist/types/server/mutation-history.d.ts +2 -0
  81. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  82. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  83. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  84. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  85. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  86. package/dist/types/server/operations/ops/query.d.ts +23 -0
  87. package/dist/types/server/operations/registry.d.ts +1 -0
  88. package/dist/types/server/placement.d.ts +7 -0
  89. package/dist/types/server/scope-fence.d.ts +2 -1
  90. package/dist/types/server/spatial-analysis.d.ts +6 -3
  91. package/dist/types/server/tour-control.d.ts +32 -0
  92. package/dist/types/server/wiki-links.d.ts +14 -0
  93. package/dist/types/server/workbench-auth.d.ts +2 -0
  94. package/dist/types/shared/agent-context.d.ts +2 -0
  95. package/dist/types/shared/agent-presence.d.ts +15 -12
  96. package/dist/types/shared/auto-arrange.d.ts +1 -0
  97. package/dist/types/shared/boards.d.ts +4 -4
  98. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  99. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  100. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  101. package/dist/types/shared/memory-graph.d.ts +53 -0
  102. package/dist/types/shared/relations.d.ts +68 -0
  103. package/dist/types/shared/semantic-attention.d.ts +2 -0
  104. package/dist/types/shared/tour.d.ts +38 -0
  105. package/dist/types/shared/workspace-settings.d.ts +11 -0
  106. package/docs/RELEASE.md +21 -3
  107. package/docs/adr-001-bun-only-runtime.md +7 -1
  108. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  109. package/docs/api-reference.md +109 -0
  110. package/docs/ax-host-adapter-contract.md +1 -1
  111. package/docs/ax-state-contract.md +5 -4
  112. package/docs/cli.md +153 -15
  113. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  114. package/docs/design/AgentContext.dc.html +3 -3
  115. package/docs/design/BoardPins.dc.html +37 -22
  116. package/docs/design/Categories.dc.html +208 -0
  117. package/docs/design/CategoryEditor.dc.html +33 -0
  118. package/docs/design/Chrome.dc.html +52 -9
  119. package/docs/design/ContextChip.dc.html +224 -0
  120. package/docs/design/ContextChip600.dc.html +136 -0
  121. package/docs/design/EmptyState.dc.html +45 -0
  122. package/docs/design/Flows.dc.html +23 -1
  123. package/docs/design/HeaderMarks.dc.html +183 -0
  124. package/docs/design/Home.dc.html +19 -1
  125. package/docs/design/HomeGraph.dc.html +86 -202
  126. package/docs/design/HomeMap.dc.html +23 -1
  127. package/docs/design/HomeTeam.dc.html +32 -14
  128. package/docs/design/Lifecycle.dc.html +22 -0
  129. package/docs/design/Lifecycle600.dc.html +18 -0
  130. package/docs/design/LifecycleVariants.dc.html +157 -0
  131. package/docs/design/LinkAuthorship.dc.html +139 -0
  132. package/docs/design/LinksChanged.dc.html +203 -0
  133. package/docs/design/LinksOptions.dc.html +137 -0
  134. package/docs/design/LinksOptions600.dc.html +134 -0
  135. package/docs/design/NearPin.dc.html +9 -10
  136. package/docs/design/Pane600.dc.html +4 -3
  137. package/docs/design/Relations.dc.html +3 -2
  138. package/docs/design/Restore.dc.html +152 -0
  139. package/docs/design/SeeChange.dc.html +267 -0
  140. package/docs/design/Uses.dc.html +12 -6
  141. package/docs/design/canvas.json +146 -19
  142. package/docs/design.md +449 -16
  143. package/docs/environment.md +0 -1
  144. package/docs/evals/curation-effect.md +128 -0
  145. package/docs/http-api.md +459 -37
  146. package/docs/mcp.md +146 -18
  147. package/docs/node-types.md +58 -3
  148. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  149. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  150. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  151. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  152. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  153. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  154. package/docs/product-context-vision-2026-09.md +17 -1
  155. package/docs/product-vision-2026-09.md +1082 -43
  156. package/docs/product-vision-review-2026-09.md +13 -1
  157. package/docs/screenshots/workbench-live-dark.png +0 -0
  158. package/docs/screenshots/workbench-live-light.png +0 -0
  159. package/docs/sdk.md +62 -2
  160. package/package.json +1 -1
  161. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  162. package/plugins/pmx-canvas/plugin.json +1 -1
  163. package/plugins/pmx-canvas/runtime.json +1 -1
  164. package/plugins/pmx-canvas/tools.json +601 -22
  165. package/skills/pmx-canvas/SKILL.md +216 -42
  166. package/skills/pmx-canvas/evals/evals.json +224 -0
  167. package/skills/pmx-canvas/references/full-reference.md +120 -21
  168. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  169. package/skills/pmx-canvas/references/importing-context.md +132 -0
  170. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  171. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  172. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  173. package/src/cli/agent.ts +2 -0
  174. package/src/cli/commands/ax.ts +12 -0
  175. package/src/cli/commands/boards.ts +88 -11
  176. package/src/cli/commands/edges.ts +28 -0
  177. package/src/cli/commands/groups.ts +7 -0
  178. package/src/cli/commands/nodes.ts +1 -0
  179. package/src/cli/commands/query.ts +34 -2
  180. package/src/cli/commands/record.ts +199 -48
  181. package/src/cli/index.ts +6 -1
  182. package/src/client/App.tsx +177 -70
  183. package/src/client/canvas/AttentionHistory.tsx +0 -2
  184. package/src/client/canvas/BarHint.tsx +40 -2
  185. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  186. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  187. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  188. package/src/client/canvas/BoardPinButton.tsx +33 -0
  189. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  190. package/src/client/canvas/CanvasNode.tsx +360 -237
  191. package/src/client/canvas/CanvasViewport.tsx +45 -7
  192. package/src/client/canvas/CommandBar.tsx +29 -23
  193. package/src/client/canvas/CommandPalette.tsx +5 -12
  194. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  195. package/src/client/canvas/ContextChip.tsx +296 -0
  196. package/src/client/canvas/ContextMenu.tsx +63 -5
  197. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  198. package/src/client/canvas/EdgeLayer.tsx +339 -76
  199. package/src/client/canvas/EmptyState.tsx +33 -18
  200. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  201. package/src/client/canvas/ExternalWriters.tsx +57 -42
  202. package/src/client/canvas/HomeGraphView.tsx +942 -0
  203. package/src/client/canvas/HomeMapView.tsx +641 -0
  204. package/src/client/canvas/HomeReviewView.tsx +167 -0
  205. package/src/client/canvas/HomeView.tsx +687 -160
  206. package/src/client/canvas/IntentLayer.tsx +19 -12
  207. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  208. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  209. package/src/client/canvas/LifecycleFields.tsx +91 -0
  210. package/src/client/canvas/LifecyclePanel.tsx +543 -0
  211. package/src/client/canvas/MemoryInspector.tsx +104 -0
  212. package/src/client/canvas/Minimap.tsx +3 -3
  213. package/src/client/canvas/NearPinLayer.tsx +187 -0
  214. package/src/client/canvas/NodeContextMark.tsx +175 -97
  215. package/src/client/canvas/Presentation.tsx +23 -57
  216. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  217. package/src/client/canvas/RelationsPanel.tsx +82 -0
  218. package/src/client/canvas/SelectionBar.tsx +3 -6
  219. package/src/client/canvas/SessionPanel.tsx +90 -50
  220. package/src/client/canvas/SessionReceipt.tsx +311 -39
  221. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  222. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  223. package/src/client/canvas/ToolRail.tsx +367 -192
  224. package/src/client/canvas/TopBar.tsx +354 -124
  225. package/src/client/canvas/canvas-area.ts +10 -3
  226. package/src/client/canvas/graph-geometry.ts +195 -0
  227. package/src/client/canvas/graph-model.ts +254 -0
  228. package/src/client/canvas/memory-layout.ts +106 -0
  229. package/src/client/canvas/use-chrome-height.ts +20 -0
  230. package/src/client/canvas/use-focus-trap.ts +31 -5
  231. package/src/client/canvas/use-memory-graph.ts +58 -0
  232. package/src/client/canvas/use-node-drag.ts +2 -1
  233. package/src/client/canvas/use-pan-zoom.ts +64 -7
  234. package/src/client/icons.tsx +0 -63
  235. package/src/client/mcp-app/style.css +0 -6
  236. package/src/client/nodes/AxStepControls.tsx +74 -50
  237. package/src/client/nodes/BoardNode.tsx +5 -1
  238. package/src/client/nodes/ContextNode.tsx +18 -11
  239. package/src/client/nodes/ExtAppFrame.tsx +6 -3
  240. package/src/client/nodes/FileNode.tsx +46 -42
  241. package/src/client/nodes/HtmlNode.tsx +32 -0
  242. package/src/client/nodes/ImageNode.tsx +1 -1
  243. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  244. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  245. package/src/client/nodes/MarkdownNode.tsx +377 -109
  246. package/src/client/nodes/McpAppNode.tsx +50 -20
  247. package/src/client/nodes/MdFormatBar.tsx +21 -18
  248. package/src/client/nodes/PromptNode.tsx +1 -1
  249. package/src/client/nodes/ResponseNode.tsx +1 -1
  250. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  251. package/src/client/state/attention-bridge.ts +64 -15
  252. package/src/client/state/boards-store.ts +193 -39
  253. package/src/client/state/canvas-store.ts +99 -12
  254. package/src/client/state/context-chip-store.ts +70 -0
  255. package/src/client/state/context-status-store.ts +53 -0
  256. package/src/client/state/human-store.ts +13 -7
  257. package/src/client/state/intent-bridge.ts +51 -30
  258. package/src/client/state/knowledge-store.ts +43 -0
  259. package/src/client/state/near-pin-store.ts +115 -6
  260. package/src/client/state/presence-store.ts +7 -10
  261. package/src/client/state/presentation.ts +91 -0
  262. package/src/client/state/relation-store.ts +81 -0
  263. package/src/client/state/session-store.ts +144 -18
  264. package/src/client/state/sse-bridge.ts +235 -32
  265. package/src/client/state/workspace-settings.ts +11 -0
  266. package/src/client/theme/global.css +2724 -476
  267. package/src/client/types.ts +12 -0
  268. package/src/client/utils/keyboard.ts +10 -0
  269. package/src/client/utils/time-ago.ts +14 -0
  270. package/src/json-render/renderer/index.tsx +31 -0
  271. package/src/mcp/canvas-app.ts +12 -2
  272. package/src/mcp/context-uri-template.ts +7 -4
  273. package/src/mcp/server.ts +63 -41
  274. package/src/mcp/workbench-app.ts +39 -6
  275. package/src/server/agent-presence.ts +128 -28
  276. package/src/server/ax-context.ts +27 -13
  277. package/src/server/board-export.ts +12 -1
  278. package/src/server/canvas-db.ts +331 -42
  279. package/src/server/canvas-operations.ts +195 -83
  280. package/src/server/canvas-provenance.ts +53 -22
  281. package/src/server/canvas-serialization.ts +2 -0
  282. package/src/server/canvas-state.ts +950 -122
  283. package/src/server/canvas-validation.ts +9 -2
  284. package/src/server/context-brief.ts +345 -38
  285. package/src/server/context-reads.ts +178 -30
  286. package/src/server/demo-state.json +355 -24
  287. package/src/server/edit-summary.ts +74 -0
  288. package/src/server/html-surface.ts +8 -1
  289. package/src/server/index.ts +181 -11
  290. package/src/server/knowledge-lifecycle.ts +304 -0
  291. package/src/server/lifecycle-status.ts +232 -0
  292. package/src/server/memory-graph.ts +166 -0
  293. package/src/server/mutation-history.ts +18 -6
  294. package/src/server/operations/composites.ts +32 -11
  295. package/src/server/operations/http.ts +7 -2
  296. package/src/server/operations/index.ts +4 -0
  297. package/src/server/operations/mcp.ts +26 -1
  298. package/src/server/operations/ops/ax-presence.ts +1 -1
  299. package/src/server/operations/ops/ax-read.ts +57 -6
  300. package/src/server/operations/ops/ax-timeline.ts +28 -2
  301. package/src/server/operations/ops/backup.ts +5 -3
  302. package/src/server/operations/ops/batch.ts +35 -4
  303. package/src/server/operations/ops/boards.ts +188 -47
  304. package/src/server/operations/ops/canvas-wire.ts +31 -1
  305. package/src/server/operations/ops/edges.ts +36 -2
  306. package/src/server/operations/ops/groups.ts +6 -1
  307. package/src/server/operations/ops/lifecycle.ts +262 -0
  308. package/src/server/operations/ops/memory-graph.ts +45 -0
  309. package/src/server/operations/ops/nodes.ts +107 -24
  310. package/src/server/operations/ops/query.ts +281 -68
  311. package/src/server/operations/ops/snapshots.ts +73 -1
  312. package/src/server/operations/ops/validate.ts +5 -1
  313. package/src/server/operations/ops/viewport.ts +97 -3
  314. package/src/server/operations/registry.ts +74 -25
  315. package/src/server/placement.ts +29 -0
  316. package/src/server/scope-fence.ts +78 -6
  317. package/src/server/server.ts +115 -16
  318. package/src/server/spatial-analysis.ts +48 -23
  319. package/src/server/tour-control.ts +82 -0
  320. package/src/server/wiki-links.ts +137 -0
  321. package/src/server/workbench-auth.ts +11 -0
  322. package/src/shared/agent-context.ts +3 -0
  323. package/src/shared/agent-presence.ts +15 -15
  324. package/src/shared/auto-arrange.ts +2 -0
  325. package/src/shared/boards.ts +4 -4
  326. package/src/shared/knowledge-lifecycle.ts +22 -0
  327. package/src/shared/knowledge-presets.ts +80 -0
  328. package/src/shared/lifecycle-status.ts +26 -0
  329. package/src/shared/memory-graph.ts +74 -0
  330. package/src/shared/relations.ts +98 -0
  331. package/src/shared/semantic-attention.ts +15 -1
  332. package/src/shared/tour.ts +23 -5
  333. package/src/shared/workspace-settings.ts +13 -0
  334. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  335. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  336. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  337. package/dist/types/server/agent-context.d.ts +0 -22
  338. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -1,16 +1,22 @@
1
- import { useEffect, useState } from 'preact/hooks';
2
- import { toggleContextPin } from '../state/canvas-store';
1
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
2
+ import { BarHint } from './BarHint';
3
+ import { nodes, removeContextPins } from '../state/canvas-store';
4
+ import { activeBoardId, boardList, openBoard } from '../state/boards-store';
3
5
  import {
4
- activityLensNodeIds,
6
+ activityLens,
5
7
  dismissSessionReceipt,
6
8
  type ReceiptNode,
9
+ receiptNodeKey,
7
10
  type SessionContextActivity,
11
+ sendSteering,
8
12
  sessionReceipt,
9
13
  setActivityLens,
14
+ undoSessionEdits,
10
15
  } from '../state/session-store';
11
16
  import { IconClose, IconPin } from '../icons';
12
17
  import { GLYPHS } from './NodeContextMark';
13
18
  import { workbenchFetch } from '../state/workbench-transport';
19
+ import { agentSessionsEnabled } from '../state/workspace-settings';
14
20
 
15
21
  /**
16
22
  * Session receipt (rail-chrome-v2 phase 5, design item 2): a dismissible card
@@ -42,12 +48,58 @@ export function summarizeDiff(diff: unknown): DiffSummary | null {
42
48
 
43
49
  const titles = (nodes: ReceiptNode[]) => nodes.map((node) => node.title).join(', ');
44
50
 
51
+ function clock(iso: string): string {
52
+ const at = new Date(iso);
53
+ return Number.isNaN(at.getTime())
54
+ ? ''
55
+ : `${String(at.getHours()).padStart(2, '0')}:${String(at.getMinutes()).padStart(2, '0')}`;
56
+ }
57
+
45
58
  /**
46
59
  * What the session did with context (docs/design/AgentContext.dc.html): an
47
60
  * amber line for pins it read that changed since, one row per kind of touch,
48
61
  * and the lens switch that dims every node the session did not touch.
49
62
  */
50
- function ReceiptContext({ context, label }: { context: SessionContextActivity; label: string }) {
63
+ function ReceiptContext({
64
+ context,
65
+ label,
66
+ snapshotId,
67
+ }: {
68
+ context: SessionContextActivity;
69
+ label: string;
70
+ /** The pre-session snapshot: what Undo on an edit restores from (single sessions only). */
71
+ snapshotId: string | null;
72
+ }) {
73
+ const [told, setTold] = useState(false);
74
+ const [restoredIds, setRestoredIds] = useState<string[]>([]);
75
+ const [keptEdits, setKeptEdits] = useState(false);
76
+ const [openId, setOpenId] = useState<string | null>(null);
77
+ const [undoing, setUndoing] = useState(false);
78
+ const rowUndo =
79
+ context.edited.length > 0 && context.edited.every((node) => restoredIds.includes(node.id))
80
+ ? 'undone'
81
+ : keptEdits
82
+ ? 'kept'
83
+ : null;
84
+ useEffect(() => {
85
+ setRestoredIds([]);
86
+ setKeptEdits(false);
87
+ setOpenId(null);
88
+ }, [context]);
89
+ const undo = async (edits: ReceiptNode[]) => {
90
+ if (!snapshotId || undoing) return;
91
+ setUndoing(true);
92
+ try {
93
+ const result = await undoSessionEdits(
94
+ snapshotId,
95
+ edits.filter((node) => !restoredIds.includes(node.id)),
96
+ );
97
+ setRestoredIds((previous) => [...previous, ...result.restored]);
98
+ return result;
99
+ } finally {
100
+ setUndoing(false);
101
+ }
102
+ };
51
103
  const rows: Array<{ key: keyof SessionContextActivity; name: string; glyph: keyof typeof GLYPHS | 'pin' }> = [
52
104
  { key: 'read', name: 'Read', glyph: 'eye' },
53
105
  { key: 'pinned', name: 'Pinned', glyph: 'pin' },
@@ -56,7 +108,7 @@ function ReceiptContext({ context, label }: { context: SessionContextActivity; l
56
108
  ];
57
109
  const present = rows.filter((row) => context[row.key].length > 0);
58
110
  if (present.length === 0 && context.changedSinceRead.length === 0) return null;
59
- const lensOn = activityLensNodeIds.value !== null;
111
+ const lensOn = activityLens.value?.kind === 'receipt';
60
112
  return (
61
113
  <div class="session-receipt-context" data-testid="session-receipt-context">
62
114
  {context.changedSinceRead.length > 0 && (
@@ -68,6 +120,19 @@ function ReceiptContext({ context, label }: { context: SessionContextActivity; l
68
120
  {titles(context.changedSinceRead)} changed after {label} read{' '}
69
121
  {context.changedSinceRead.length === 1 ? 'it' : 'them'}.
70
122
  </span>
123
+ <button
124
+ type="button"
125
+ class="session-receipt-mini"
126
+ disabled={told}
127
+ onClick={() => {
128
+ const them = context.changedSinceRead.length === 1 ? 'it' : 'them';
129
+ void sendSteering(
130
+ `${context.changedSinceRead.map((node) => `“${node.title}”`).join(', ')} changed after you read ${them} — read ${them} again before relying on ${them}.`,
131
+ ).then((ok) => setTold(ok));
132
+ }}
133
+ >
134
+ {told ? 'Told' : `Tell ${label}`}
135
+ </button>
71
136
  </div>
72
137
  )}
73
138
  {present.map((row) => (
@@ -86,19 +151,60 @@ function ReceiptContext({ context, label }: { context: SessionContextActivity; l
86
151
  <button
87
152
  type="button"
88
153
  class="session-receipt-mini"
154
+ disabled={context.pinned.some((node) => node.boardId && node.boardId !== activeBoardId.value)}
155
+ onClick={() => removeContextPins(context.pinned.map((node) => node.id))}
156
+ >
157
+ Unpin
158
+ </button>
159
+ )}
160
+ {row.key === 'edited' && snapshotId && (
161
+ <button
162
+ type="button"
163
+ class="session-receipt-mini"
164
+ disabled={
165
+ rowUndo !== null ||
166
+ undoing ||
167
+ context.edited.some((node) => node.boardId && node.boardId !== activeBoardId.value)
168
+ }
89
169
  onClick={() => {
90
- for (const node of context.pinned) toggleContextPin(node.id);
170
+ void undo(context.edited).then((result) => {
171
+ if (result?.ok) setKeptEdits(true);
172
+ });
91
173
  }}
92
174
  >
93
- Unpin
175
+ {rowUndo === 'undone' ? '✓ Undone' : rowUndo === 'kept' ? 'Kept your edits' : 'Undo'}
94
176
  </button>
95
177
  )}
96
178
  </div>
97
- <div class="session-receipt-context-items">
98
- {row.key === 'pinned'
99
- ? context.pinned.map((node) => (node.reason ? `${node.title} — “${node.reason}”` : node.title)).join('; ')
100
- : titles(context[row.key])}
101
- </div>
179
+ {row.key === 'edited' && rowUndo && (
180
+ <div class="session-receipt-context-items">
181
+ {rowUndo === 'undone' ? 'every card restored' : 'cards you edited since were left alone'}
182
+ </div>
183
+ )}
184
+ {row.key === 'edited' ? (
185
+ <ul class="session-receipt-edits">
186
+ {context.edited.map((node) => (
187
+ <EditLine
188
+ key={receiptNodeKey(node)}
189
+ node={node}
190
+ canUndo={snapshotId !== null}
191
+ undoing={undoing}
192
+ open={openId === receiptNodeKey(node)}
193
+ onToggle={() => setOpenId(openId === receiptNodeKey(node) ? null : receiptNodeKey(node))}
194
+ undone={restoredIds.includes(node.id)}
195
+ onUndo={() => void undo([node])}
196
+ />
197
+ ))}
198
+ </ul>
199
+ ) : (
200
+ <div class="session-receipt-context-items">
201
+ {row.key === 'pinned'
202
+ ? context.pinned
203
+ .map((node) => (node.reason ? `${node.title} — “${node.reason}”` : node.title))
204
+ .join('; ')
205
+ : titles(context[row.key])}
206
+ </div>
207
+ )}
102
208
  </div>
103
209
  ))}
104
210
  <button
@@ -117,16 +223,160 @@ function ReceiptContext({ context, label }: { context: SessionContextActivity; l
117
223
  );
118
224
  }
119
225
 
226
+ /** Align the bounded excerpts by word, preserving punctuation and whitespace verbatim. */
227
+ function changedWords(before: string, after: string) {
228
+ const tokens = (text: string) => text.match(/\s+|[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu) ?? [];
229
+ const a = tokens(before);
230
+ const b = tokens(after);
231
+ const lengths = Array.from({ length: a.length + 1 }, () => new Uint16Array(b.length + 1));
232
+ for (let i = a.length - 1; i >= 0; i--) {
233
+ for (let j = b.length - 1; j >= 0; j--) {
234
+ lengths[i][j] = a[i] === b[j] ? lengths[i + 1][j + 1] + 1 : Math.max(lengths[i + 1][j], lengths[i][j + 1]);
235
+ }
236
+ }
237
+ const was: Array<{ text: string; changed: boolean }> = [];
238
+ const now: Array<{ text: string; changed: boolean }> = [];
239
+ let i = 0;
240
+ let j = 0;
241
+ while (i < a.length || j < b.length) {
242
+ if (i < a.length && j < b.length && a[i] === b[j]) {
243
+ was.push({ text: a[i++], changed: false });
244
+ now.push({ text: b[j++], changed: false });
245
+ } else if (i < a.length && (j === b.length || lengths[i + 1][j] >= lengths[i][j + 1])) {
246
+ was.push({ text: a[i++], changed: true });
247
+ } else {
248
+ now.push({ text: b[j++], changed: true });
249
+ }
250
+ }
251
+ return [was, now];
252
+ }
253
+
254
+ /** SeeChange.dc.html: one stacked, word-marked change, with Undo for the whole card. */
255
+ function EditLine({
256
+ node,
257
+ canUndo,
258
+ undoing,
259
+ open,
260
+ onToggle,
261
+ undone,
262
+ onUndo,
263
+ }: {
264
+ node: ReceiptNode;
265
+ canUndo: boolean;
266
+ undoing: boolean;
267
+ open: boolean;
268
+ onToggle: () => void;
269
+ undone: boolean;
270
+ onUndo: () => void;
271
+ }) {
272
+ const onBoard = !node.boardId || node.boardId === activeBoardId.value;
273
+ const current = onBoard ? nodes.value.get(node.id) : undefined;
274
+ const board = boardList.value.find((entry) => entry.id === node.boardId);
275
+ const sides = useMemo(
276
+ () => (open ? changedWords(node.before ?? '', node.after ?? '') : []),
277
+ [open, node.before, node.after],
278
+ );
279
+ const pair = useRef<HTMLSpanElement>(null);
280
+ useLayoutEffect(() => {
281
+ // Long excerpts retain context above the edit; start each scroll box at
282
+ // the first changed word rather than a screenful of identical preamble.
283
+ for (const text of pair.current?.querySelectorAll<HTMLElement>('.session-receipt-diff-text') ?? []) {
284
+ const change = text.querySelector('del, ins');
285
+ if (change) text.scrollTop = change.getBoundingClientRect().top - text.getBoundingClientRect().top - 24;
286
+ }
287
+ }, [sides]);
288
+ // Your own later edit wins: Undo would leave it, so it is not offered.
289
+ const yours = current?.lastEditedBy?.actor === 'human';
290
+ return (
291
+ <li class="session-receipt-edit">
292
+ <span class="session-receipt-edit-line">
293
+ <span>
294
+ {node.title}
295
+ {node.change ? <span class="session-receipt-edit-change"> — {node.change}</span> : null}
296
+ </span>
297
+ {node.before !== undefined && (
298
+ <button type="button" class="session-receipt-link" aria-expanded={open} onClick={onToggle}>
299
+ {open ? 'Hide change' : 'See change'}
300
+ </button>
301
+ )}
302
+ </span>
303
+ {open && (
304
+ <span ref={pair} class="session-receipt-diff-pair">
305
+ {sides.map((words, side) => (
306
+ <span class="session-receipt-diff-side" key={side}>
307
+ <span class="session-receipt-diff-label">{side === 0 ? 'Before' : 'After'}</span>
308
+ <span class="session-receipt-diff-text">
309
+ {words.length
310
+ ? words.map(({ text, changed }, index) =>
311
+ text === '…' && (index === 0 || index === words.length - 1) ? (
312
+ <span key={index} class="session-receipt-diff-cut">
313
+ {text}
314
+ </span>
315
+ ) : changed ? (
316
+ side === 0 ? (
317
+ <del key={index}>{text}</del>
318
+ ) : (
319
+ <ins key={index}>{text}</ins>
320
+ )
321
+ ) : (
322
+ text
323
+ ),
324
+ )
325
+ : 'Empty'}
326
+ </span>
327
+ </span>
328
+ ))}
329
+ {canUndo && (
330
+ <span class="session-receipt-undo">
331
+ {current ? (
332
+ <button
333
+ type="button"
334
+ class="session-receipt-mini"
335
+ disabled={undone || yours || undoing}
336
+ onClick={onUndo}
337
+ >
338
+ {undone ? '✓ Undone' : yours ? 'You edited it since' : 'Undo this card'}
339
+ </button>
340
+ ) : board && !onBoard ? (
341
+ <button type="button" class="session-receipt-mini" onClick={() => void openBoard(board.id)}>
342
+ Open {board.name} to undo
343
+ </button>
344
+ ) : (
345
+ <span class="session-receipt-edit-change">Card no longer on this board</span>
346
+ )}
347
+ <span class="session-receipt-edit-change">
348
+ {current
349
+ ? undone
350
+ ? 'the card shows Before again'
351
+ : yours
352
+ ? 'undo would lose your edit'
353
+ : node.before?.includes('…') || node.after?.includes('…')
354
+ ? 'undoes the whole card'
355
+ : 'puts back the text from before'
356
+ : board && !onBoard
357
+ ? `it is on ${board.name}`
358
+ : ''}
359
+ </span>
360
+ </span>
361
+ )}
362
+ </span>
363
+ )}
364
+ </li>
365
+ );
366
+ }
367
+
120
368
  export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => void }) {
121
369
  const receipt = sessionReceipt.value;
122
370
  const [diff, setDiff] = useState<DiffSummary | null>(null);
123
371
  const [loadingDiff, setLoadingDiff] = useState(false);
124
372
  // A new or merged receipt: the old diff no longer describes it.
125
373
  useEffect(() => setDiff(null), [receipt]);
126
- if (!receipt) return null;
374
+ if (!agentSessionsEnabled.value || !receipt) return null;
127
375
 
376
+ const onSnapshotBoard = receipt.snapshot?.boardId === activeBoardId.value;
377
+ const snapshotBoard = boardList.value.find((board) => board.id === receipt.snapshot?.boardId);
128
378
  const viewDiff = async () => {
129
- if (!receipt.snapshot) return;
379
+ if (!receipt.snapshot || !onSnapshotBoard) return;
130
380
  setLoadingDiff(true);
131
381
  try {
132
382
  const response = await workbenchFetch(`/api/canvas/snapshots/${encodeURIComponent(receipt.snapshot.id)}/diff`, {
@@ -141,10 +391,16 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
141
391
  }
142
392
  };
143
393
 
144
- const ended = new Date(receipt.endedAt);
145
- const endedLabel = Number.isNaN(ended.getTime())
146
- ? ''
147
- : `${String(ended.getHours()).padStart(2, '0')}:${String(ended.getMinutes()).padStart(2, '0')}`;
394
+ const endedLabel = clock(receipt.endedAt);
395
+ const startedLabel = receipt.startedAt ? clock(receipt.startedAt) : '';
396
+ const endedBy =
397
+ receipt.endedBy === 'human'
398
+ ? 'ended by you'
399
+ : receipt.endedBy === 'idle-timeout'
400
+ ? 'ended on idle timeout'
401
+ : receipt.endedBy === 'agent'
402
+ ? 'ended by the agent'
403
+ : '';
148
404
 
149
405
  return (
150
406
  <div class="session-receipt" data-testid="session-receipt" role="status">
@@ -152,15 +408,8 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
152
408
  <span class="session-receipt-dot" aria-hidden="true" />
153
409
  <span class="session-receipt-title">
154
410
  {receipt.sessions > 1
155
- ? `${receipt.sessions} sessions ended`
156
- : receipt.endedBy === 'human'
157
- ? 'Session ended by you'
158
- : receipt.endedBy === 'idle-timeout'
159
- ? 'Session ended — idle timeout'
160
- : receipt.endedBy === 'agent'
161
- ? 'Session ended by the agent'
162
- : 'Session ended'}
163
- {endedLabel ? ` · ${endedLabel}` : ''}
411
+ ? `${receipt.sessions} sessions ended${endedLabel ? ` · ${endedLabel}` : ''}`
412
+ : `What ${receipt.labels[0]} did`}
164
413
  </span>
165
414
  <button
166
415
  type="button"
@@ -171,7 +420,20 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
171
420
  <IconClose />
172
421
  </button>
173
422
  </div>
174
- {receipt.sessions > 1 && <div class="session-receipt-who">{receipt.labels.join(', ')}</div>}
423
+ {receipt.sessions > 1 ? (
424
+ <div class="session-receipt-who">{receipt.labels.join(', ')}</div>
425
+ ) : (
426
+ // AgentContext.dc.html: "This session · 14:02–14:20 · saved as a snapshot".
427
+ <div class="session-receipt-who" data-testid="session-receipt-span">
428
+ {[
429
+ `This session · ${startedLabel ? `${startedLabel}–` : ''}${endedLabel}`,
430
+ receipt.snapshot ? 'saved as a snapshot' : '',
431
+ endedBy,
432
+ ]
433
+ .filter(Boolean)
434
+ .join(' · ')}
435
+ </div>
436
+ )}
175
437
  <div class="session-receipt-tiles">
176
438
  <div class="session-receipt-tile">
177
439
  <span class="session-receipt-tile-label">Items</span>
@@ -202,7 +464,11 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
202
464
  </div>
203
465
  )}
204
466
  </div>
205
- <ReceiptContext context={receipt.context} label={receipt.labels.join(', ')} />
467
+ <ReceiptContext
468
+ context={receipt.context}
469
+ label={receipt.labels.join(', ')}
470
+ snapshotId={receipt.sessions === 1 ? (receipt.snapshot?.id ?? null) : null}
471
+ />
206
472
  <div class="session-receipt-note">
207
473
  {receipt.sessions > 1
208
474
  ? 'History has each session’s diff and the snapshot from before it.'
@@ -221,18 +487,24 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
221
487
  {/* No snapshot → no diff will ever exist for this receipt; a forever-
222
488
  disabled button is noise (the note above says why). */}
223
489
  {receipt.snapshot && (
224
- <button type="button" class="session-receipt-primary" disabled={loadingDiff} onClick={() => void viewDiff()}>
225
- {loadingDiff ? 'Comparing…' : 'View diff'}
490
+ <button
491
+ type="button"
492
+ class="session-receipt-primary"
493
+ disabled={loadingDiff || (!onSnapshotBoard && !snapshotBoard)}
494
+ onClick={() => (onSnapshotBoard ? void viewDiff() : snapshotBoard && void openBoard(snapshotBoard.id))}
495
+ >
496
+ {!onSnapshotBoard && snapshotBoard
497
+ ? `Open ${snapshotBoard.name} to compare`
498
+ : loadingDiff
499
+ ? 'Comparing…'
500
+ : 'View diff'}
226
501
  </button>
227
502
  )}
228
- <button
229
- type="button"
230
- class="session-receipt-secondary"
231
- title="Open the History drawer — saved boards and past sessions"
232
- onClick={onOpenSnapshots}
233
- >
234
- History
235
- </button>
503
+ <BarHint label="Open the History drawer" body="Saved boards and past sessions." side="up" align="end">
504
+ <button type="button" class="session-receipt-secondary" onClick={onOpenSnapshots}>
505
+ History
506
+ </button>
507
+ </BarHint>
236
508
  </div>
237
509
  </div>
238
510
  );
@@ -45,7 +45,7 @@ const GROUPS: ShortcutGroup[] = [
45
45
  { keys: 'Drag empty space', desc: 'Lasso select multiple nodes' },
46
46
  { keys: isMac ? 'Delete / \u232b' : 'Delete / Backspace', desc: 'Delete the selection (or the focused node)' },
47
47
  { keys: isMac ? '\u2318Z / \u21e7\u2318Z' : 'Ctrl+Z / Ctrl+Y', desc: 'Undo / redo — agent edits included' },
48
- { keys: 'Esc', desc: 'Clear selection / close overlay' },
48
+ { keys: 'Esc', desc: 'Close overlay first, then clear selection and view filters' },
49
49
  ],
50
50
  },
51
51
  {
@@ -1,3 +1,5 @@
1
+ import { timeAgo } from '../utils/time-ago';
2
+ import { BarHint } from './BarHint';
1
3
  import { isHostedWorkbench } from '../state/workbench-transport';
2
4
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
3
5
  import { IconClose } from '../icons';
@@ -29,17 +31,6 @@ function timeLabel(iso: string): string {
29
31
  return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
30
32
  }
31
33
 
32
- function timeAgo(iso: string): string {
33
- const diff = Date.now() - new Date(iso).getTime();
34
- const mins = Math.floor(diff / 60000);
35
- if (mins < 1) return 'just now';
36
- if (mins < 60) return `${mins}m ago`;
37
- const hrs = Math.floor(mins / 60);
38
- if (hrs < 24) return `${hrs}h ago`;
39
- const days = Math.floor(hrs / 24);
40
- return `${days}d ago`;
41
- }
42
-
43
34
  /** "Before session · Copilot · 14:00" → "Copilot". */
44
35
  function sessionLabel(name: string): string | null {
45
36
  if (!name.startsWith(SESSION_PREFIX)) return null;
@@ -151,7 +142,6 @@ export function SnapshotPanel({
151
142
  type="button"
152
143
  class={`snapshot-action-btn ${confirming.action === 'delete' ? 'snapshot-action-confirm' : 'snapshot-action-restore'}`}
153
144
  onClick={() => (confirming.action === 'delete' ? handleDelete(snap.id) : handleRestore(snap.id))}
154
- title={confirming.action === 'delete' ? 'Confirm delete' : 'Confirm restore'}
155
145
  disabled={restoringId !== null}
156
146
  >
157
147
  {confirming.action === 'delete' ? 'Delete' : restoringId === snap.id ? 'Restoring…' : 'Confirm'}
@@ -160,7 +150,6 @@ export function SnapshotPanel({
160
150
  type="button"
161
151
  class="snapshot-action-btn"
162
152
  onClick={() => setConfirming(null)}
163
- title="Cancel"
164
153
  disabled={restoringId !== null}
165
154
  >
166
155
  Cancel
@@ -168,29 +157,31 @@ export function SnapshotPanel({
168
157
  </>
169
158
  ) : (
170
159
  <>
171
- <button
172
- type="button"
173
- class="snapshot-action-btn snapshot-action-restore"
174
- onClick={() => setConfirming({ id: snap.id, action: 'restore' })}
175
- title={
176
- isHostedWorkbench()
177
- ? 'Restore this snapshot — replaces the current canvas'
178
- : 'Restore this snapshot — replaces the current canvas (undoable)'
179
- }
180
- disabled={restoringId !== null}
160
+ <BarHint
161
+ label="Restore this snapshot"
162
+ body={isHostedWorkbench() ? 'Replaces the current canvas.' : 'Replaces the current canvas (undoable).'}
163
+ align="end"
181
164
  >
182
- {restoringId === snap.id ? 'Restoring…' : restoreLabel}
183
- </button>
184
- <button
185
- type="button"
186
- class="snapshot-action-btn snapshot-action-delete"
187
- onClick={() => setConfirming({ id: snap.id, action: 'delete' })}
188
- title="Delete this snapshot"
189
- aria-label="Delete snapshot"
190
- disabled={restoringId !== null}
191
- >
192
- <IconClose size={14} />
193
- </button>
165
+ <button
166
+ type="button"
167
+ class="snapshot-action-btn snapshot-action-restore"
168
+ onClick={() => setConfirming({ id: snap.id, action: 'restore' })}
169
+ disabled={restoringId !== null}
170
+ >
171
+ {restoringId === snap.id ? 'Restoring…' : restoreLabel}
172
+ </button>
173
+ </BarHint>
174
+ <BarHint label="Delete this snapshot" align="end">
175
+ <button
176
+ type="button"
177
+ class="snapshot-action-btn snapshot-action-delete"
178
+ onClick={() => setConfirming({ id: snap.id, action: 'delete' })}
179
+ aria-label="Delete snapshot"
180
+ disabled={restoringId !== null}
181
+ >
182
+ <IconClose size={14} />
183
+ </button>
184
+ </BarHint>
194
185
  </>
195
186
  );
196
187
 
@@ -200,7 +191,7 @@ export function SnapshotPanel({
200
191
  <span class="snapshot-panel-title">History</span>
201
192
  <span class="snapshot-panel-sub">snapshots + sessions</span>
202
193
  <span class="snapshot-panel-spacer" />
203
- <button type="button" class="snapshot-panel-close" onClick={onClose} title="Close" aria-label="Close history">
194
+ <button type="button" class="snapshot-panel-close" onClick={onClose} aria-label="Close history">
204
195
  <IconClose />
205
196
  </button>
206
197
  </div>