@pellux/goodvibes-tui 2.0.21 → 2.1.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 (391) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -29
  3. package/docs/README.md +8 -5
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +28 -19
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-reference.ts +2 -2
  52. package/src/input/command-registry.ts +77 -35
  53. package/src/input/commands/acp-runtime.ts +19 -9
  54. package/src/input/commands/channel-runtime.ts +2 -2
  55. package/src/input/commands/ci-runtime.ts +1 -1
  56. package/src/input/commands/codebase-runtime.ts +1 -1
  57. package/src/input/commands/config.ts +7 -1
  58. package/src/input/commands/context-window.ts +25 -19
  59. package/src/input/commands/control-room-runtime.ts +18 -18
  60. package/src/input/commands/conversation-runtime.ts +0 -1
  61. package/src/input/commands/cost-runtime.ts +29 -20
  62. package/src/input/commands/diff-runtime.ts +35 -181
  63. package/src/input/commands/eval.ts +1 -1
  64. package/src/input/commands/experience-runtime.ts +3 -7
  65. package/src/input/commands/fresh-conversation.ts +86 -0
  66. package/src/input/commands/git-runtime.ts +5 -22
  67. package/src/input/commands/graph-runtime.ts +1 -1
  68. package/src/input/commands/guidance-runtime.ts +1 -1
  69. package/src/input/commands/health-runtime.ts +8 -9
  70. package/src/input/commands/hooks-runtime.ts +2 -2
  71. package/src/input/commands/hosted-runtime.ts +3 -3
  72. package/src/input/commands/incident-runtime.ts +3 -3
  73. package/src/input/commands/intelligence-runtime.ts +6 -9
  74. package/src/input/commands/knowledge.ts +2 -2
  75. package/src/input/commands/legacy-panel-command.ts +57 -0
  76. package/src/input/commands/local-auth-runtime.ts +8 -9
  77. package/src/input/commands/local-provider-runtime.ts +7 -4
  78. package/src/input/commands/local-runtime.ts +12 -15
  79. package/src/input/commands/local-setup-review.ts +1 -1
  80. package/src/input/commands/local-setup.ts +13 -19
  81. package/src/input/commands/memory.ts +1 -1
  82. package/src/input/commands/notify-runtime.ts +9 -0
  83. package/src/input/commands/operator-runtime.ts +10 -12
  84. package/src/input/commands/permissions-provenance.ts +3 -3
  85. package/src/input/commands/planning-runtime.ts +7 -7
  86. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  87. package/src/input/commands/platform-services-runtime.ts +1 -1
  88. package/src/input/commands/policy-dispatch.ts +2 -2
  89. package/src/input/commands/policy.ts +3 -3
  90. package/src/input/commands/provider.ts +1 -1
  91. package/src/input/commands/remote-runtime.ts +1 -1
  92. package/src/input/commands/review-runtime.ts +39 -94
  93. package/src/input/commands/rewind-runtime.ts +81 -108
  94. package/src/input/commands/runtime-services.ts +15 -17
  95. package/src/input/commands/services-runtime.ts +2 -2
  96. package/src/input/commands/session-content.ts +21 -5
  97. package/src/input/commands/session-workflow.ts +13 -38
  98. package/src/input/commands/settings-sync-runtime.ts +2 -2
  99. package/src/input/commands/shell-core.ts +19 -4
  100. package/src/input/commands/skills-runtime.ts +2 -2
  101. package/src/input/commands/status-runtime.ts +13 -0
  102. package/src/input/commands/subscription-runtime.ts +1 -1
  103. package/src/input/commands/tasks-runtime.ts +2 -8
  104. package/src/input/commands/work-plan-runtime.ts +4 -4
  105. package/src/input/commands/workstream-attempts.ts +24 -47
  106. package/src/input/commands/workstream-runtime.ts +2 -2
  107. package/src/input/commands/worktree-runtime.ts +3 -3
  108. package/src/input/commands.ts +3 -1
  109. package/src/input/composer-editor.ts +44 -0
  110. package/src/input/concealed-input.ts +1 -1
  111. package/src/input/config-modal-types.ts +20 -8
  112. package/src/input/config-modal.ts +106 -105
  113. package/src/input/confirm-dialog-types.ts +28 -0
  114. package/src/input/confirm-dialog.ts +70 -0
  115. package/src/input/delete-key-policy.ts +3 -3
  116. package/src/input/feed-context-factory.ts +15 -16
  117. package/src/input/handler-command-route.ts +7 -42
  118. package/src/input/handler-content-actions.ts +16 -15
  119. package/src/input/handler-feed-routes.ts +26 -211
  120. package/src/input/handler-feed.ts +68 -95
  121. package/src/input/handler-interactions.ts +5 -3
  122. package/src/input/handler-modal-routes.ts +215 -216
  123. package/src/input/handler-modal-stack.ts +81 -11
  124. package/src/input/handler-modal-token-routes.ts +4 -0
  125. package/src/input/handler-onboarding.ts +0 -1
  126. package/src/input/handler-picker-routes.ts +65 -107
  127. package/src/input/handler-session-view-route.ts +149 -0
  128. package/src/input/handler-shortcuts.ts +16 -154
  129. package/src/input/handler-types.ts +4 -2
  130. package/src/input/handler-ui-state.ts +45 -15
  131. package/src/input/handler-work-tree-route.ts +123 -0
  132. package/src/input/handler.ts +25 -70
  133. package/src/input/keybindings.ts +16 -71
  134. package/src/input/masked-entry-modal.ts +117 -0
  135. package/src/input/mcp-workspace.ts +48 -17
  136. package/src/input/model-picker-filter.ts +8 -0
  137. package/src/input/model-picker-open.ts +99 -0
  138. package/src/input/model-picker-types.ts +1 -1
  139. package/src/input/model-picker.ts +92 -9
  140. package/src/input/notifications-modal.ts +123 -0
  141. package/src/input/overlay-filter.ts +26 -0
  142. package/src/input/profile-picker-modal.ts +37 -8
  143. package/src/input/search.ts +3 -3
  144. package/src/input/selection-modal-queue.ts +2 -0
  145. package/src/input/selection-modal.ts +28 -16
  146. package/src/input/session-picker-modal.ts +33 -8
  147. package/src/input/settings-modal-activation.ts +7 -2
  148. package/src/input/settings-modal-data.ts +11 -6
  149. package/src/input/settings-modal-types.ts +1 -1
  150. package/src/input/settings-modal.ts +20 -20
  151. package/src/input/submission-intent.ts +1 -1
  152. package/src/input/submission-router.ts +4 -9
  153. package/src/input/surface-modal-host.ts +110 -0
  154. package/src/input/theme-settings-actions.ts +82 -0
  155. package/src/input/usage-modal.ts +183 -0
  156. package/src/input/views.ts +97 -0
  157. package/src/main.ts +175 -165
  158. package/src/permissions/hunk-selection.ts +1 -1
  159. package/src/permissions/prompt-card.ts +511 -0
  160. package/src/permissions/prompt.ts +59 -622
  161. package/src/renderer/agents-modal.ts +310 -0
  162. package/src/renderer/autocomplete-overlay.ts +42 -220
  163. package/src/renderer/back-to-bottom.ts +80 -0
  164. package/src/renderer/block-actions-overlay.ts +30 -48
  165. package/src/renderer/bookmark-modal.ts +48 -79
  166. package/src/renderer/changes-modal.ts +325 -0
  167. package/src/renderer/code-block.ts +184 -106
  168. package/src/renderer/command-palette.ts +93 -0
  169. package/src/renderer/composer-fragments.ts +4 -4
  170. package/src/renderer/composer.ts +146 -0
  171. package/src/renderer/compositor.ts +26 -160
  172. package/src/renderer/config-modal.ts +185 -81
  173. package/src/renderer/confirm-dialog.ts +50 -0
  174. package/src/renderer/context-inspector.ts +116 -154
  175. package/src/renderer/context-status-hint.ts +4 -4
  176. package/src/renderer/conversation-layout.ts +20 -0
  177. package/src/renderer/conversation-overlays.ts +94 -84
  178. package/src/renderer/conversation-surface.ts +19 -38
  179. package/src/renderer/diff-tint.ts +98 -0
  180. package/src/renderer/file-picker-overlay.ts +63 -90
  181. package/src/renderer/footer-targets.ts +38 -0
  182. package/src/renderer/git-status.ts +3 -4
  183. package/src/renderer/header-line.ts +140 -117
  184. package/src/renderer/help-overlay.ts +133 -219
  185. package/src/renderer/history-search-overlay.ts +15 -12
  186. package/src/renderer/lane-graph/bead.ts +800 -0
  187. package/src/renderer/lane-graph/glyphs.ts +134 -0
  188. package/src/renderer/lane-graph/layout.ts +248 -0
  189. package/src/renderer/lane-graph/paint.ts +537 -0
  190. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  191. package/src/renderer/layout-engine.ts +2 -57
  192. package/src/renderer/layout.ts +7 -13
  193. package/src/renderer/markdown-inline.ts +2 -1
  194. package/src/renderer/markdown-table.ts +17 -12
  195. package/src/renderer/markdown.ts +92 -34
  196. package/src/renderer/masked-entry-modal.ts +78 -0
  197. package/src/renderer/mcp-workspace.ts +166 -203
  198. package/src/renderer/model-workspace.ts +381 -436
  199. package/src/renderer/notifications-modal.ts +116 -0
  200. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  201. package/src/renderer/process-view.ts +192 -0
  202. package/src/renderer/profile-picker-modal.ts +58 -110
  203. package/src/renderer/search-overlay.ts +6 -3
  204. package/src/renderer/selection-modal-overlay.ts +87 -327
  205. package/src/renderer/semantic-diff.ts +2 -2
  206. package/src/renderer/session-chips.ts +102 -0
  207. package/src/renderer/session-picker-modal.ts +177 -297
  208. package/src/renderer/settings-modal-connections.ts +14 -23
  209. package/src/renderer/settings-modal-context.ts +367 -0
  210. package/src/renderer/settings-modal-helpers.ts +6 -11
  211. package/src/renderer/settings-modal.ts +322 -726
  212. package/src/renderer/shell-surface.ts +277 -175
  213. package/src/renderer/shortcuts-overlay.ts +217 -0
  214. package/src/renderer/status-line.ts +475 -0
  215. package/src/renderer/surface-compose.ts +174 -0
  216. package/src/renderer/surface-kit-extra.ts +303 -0
  217. package/src/renderer/surface-kit-list.ts +366 -0
  218. package/src/renderer/surface-kit-parts.ts +360 -0
  219. package/src/renderer/surface-kit.ts +568 -0
  220. package/src/renderer/syntax-highlighter.ts +280 -197
  221. package/src/renderer/syntax-theme.ts +51 -0
  222. package/src/renderer/system-message.ts +0 -1
  223. package/src/renderer/term-caps.ts +42 -1
  224. package/src/renderer/terminal-bg-probe.ts +116 -19
  225. package/src/renderer/terminal-palette-probe.ts +322 -0
  226. package/src/renderer/terminal-palette-reader.ts +183 -0
  227. package/src/renderer/terminal-palette.ts +58 -0
  228. package/src/renderer/theme-mode-config.ts +16 -1
  229. package/src/renderer/theme.ts +234 -207
  230. package/src/renderer/thinking.ts +0 -1
  231. package/src/renderer/throbber.ts +188 -0
  232. package/src/renderer/toast-center.ts +119 -0
  233. package/src/renderer/ui-factory.ts +39 -602
  234. package/src/renderer/ui-primitives.ts +8 -25
  235. package/src/renderer/usage-modal.ts +353 -0
  236. package/src/renderer/user-message.ts +58 -0
  237. package/src/renderer/voice-capture-chip.ts +53 -0
  238. package/src/runtime/bootstrap-command-context.ts +0 -5
  239. package/src/runtime/bootstrap-command-parts.ts +38 -89
  240. package/src/runtime/bootstrap-core.ts +13 -15
  241. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  242. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  243. package/src/runtime/bootstrap-shell.ts +16 -50
  244. package/src/runtime/bootstrap.ts +14 -14
  245. package/src/runtime/client/fleet-union.ts +5 -5
  246. package/src/runtime/client/hosted-exit.ts +2 -2
  247. package/src/runtime/client/hosted-sessions.ts +1 -1
  248. package/src/runtime/client/spine-adoption.ts +1 -1
  249. package/src/runtime/context-accounting-source.ts +5 -4
  250. package/src/runtime/index.ts +1 -0
  251. package/src/runtime/notification-dispatch.ts +102 -29
  252. package/src/runtime/recovery-offer-wiring.ts +4 -1
  253. package/src/runtime/recovery-prompt.ts +31 -15
  254. package/src/runtime/runtime-services-types.ts +10 -6
  255. package/src/runtime/services.ts +17 -12
  256. package/src/runtime/session-continuity-hints.ts +2 -5
  257. package/src/runtime/session-inbound-inputs.ts +1 -1
  258. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  259. package/src/runtime/startup-typeahead-gate.ts +103 -0
  260. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  261. package/src/runtime/store/domains/index.ts +0 -8
  262. package/src/runtime/store/selectors/index.ts +0 -28
  263. package/src/runtime/store/state.ts +0 -8
  264. package/src/runtime/ui-services.ts +4 -6
  265. package/src/runtime/usage-tracker.ts +348 -0
  266. package/src/runtime/wrfc-persistence.ts +4 -4
  267. package/src/shell/blocking-input.ts +92 -2
  268. package/src/shell/recovery-input-helpers.ts +1 -1
  269. package/src/shell/session-views.ts +539 -0
  270. package/src/shell/status-report.ts +86 -0
  271. package/src/shell/transcript-scroll.ts +68 -0
  272. package/src/shell/ui-openers.ts +84 -172
  273. package/src/shell/view-openers.ts +205 -0
  274. package/src/tools/wrfc-agent-guard.ts +22 -2
  275. package/src/utils/format-duration.ts +4 -8
  276. package/src/utils/splash-lines.ts +9 -1
  277. package/src/utils/terminal-width.ts +56 -0
  278. package/src/verification/live-verifier.ts +0 -10
  279. package/src/version.ts +1 -1
  280. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  281. package/src/{panels → views}/builtin-modals.ts +22 -21
  282. package/src/views/builtin-views.ts +135 -0
  283. package/src/{panels → views}/confirm-state.ts +9 -9
  284. package/src/{panels → views}/diff-review-model.ts +10 -3
  285. package/src/{panels → views}/eval-registry.ts +1 -1
  286. package/src/{panels → views}/fleet-acts.ts +49 -85
  287. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  288. package/src/{panels → views}/fleet-gateway.ts +5 -5
  289. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  290. package/src/{panels → views}/fleet-read-model.ts +9 -9
  291. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  292. package/src/{panels → views}/fleet-spawn.ts +33 -49
  293. package/src/{panels → views}/fleet-steer.ts +12 -12
  294. package/src/{panels → views}/fleet-stop.ts +4 -4
  295. package/src/{panels → views}/fleet-transcript.ts +26 -27
  296. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  297. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  298. package/src/views/modal-surface-registry.ts +41 -0
  299. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  300. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  301. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  302. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  303. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  304. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  305. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  306. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  307. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  308. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  309. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  310. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  311. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  312. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  313. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  314. package/src/{panels → views}/modals/security-modal.ts +22 -16
  315. package/src/{panels → views}/modals/services-modal.ts +3 -3
  316. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  317. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  318. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  319. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  320. package/src/views/notifications-feed.ts +314 -0
  321. package/src/{panels → views}/polish-core.ts +38 -36
  322. package/src/{panels → views}/polish-tables.ts +35 -26
  323. package/src/{panels → views}/polish.ts +101 -95
  324. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  325. package/src/{panels → views}/provider-health-routes.ts +14 -14
  326. package/src/{panels → views}/search-focus.ts +8 -8
  327. package/src/{panels → views}/session-maintenance.ts +11 -11
  328. package/src/views/skills-discovery.ts +159 -0
  329. package/src/views/view-deps.ts +80 -0
  330. package/src/views/view-panel-adapter.ts +53 -0
  331. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  332. package/src/core/conversation-fold.ts +0 -101
  333. package/src/core/conversation-turn-structure.ts +0 -464
  334. package/src/core/conversation-user-receipts.ts +0 -42
  335. package/src/input/commands/operator-panel-runtime.ts +0 -186
  336. package/src/input/panel-integration-actions.ts +0 -33
  337. package/src/input/panel-mouse-geometry.ts +0 -97
  338. package/src/input/panel-paste-flood-guard.ts +0 -86
  339. package/src/panels/base-panel.ts +0 -301
  340. package/src/panels/builtin/agent.ts +0 -27
  341. package/src/panels/builtin/development.ts +0 -92
  342. package/src/panels/builtin/knowledge.ts +0 -12
  343. package/src/panels/builtin/operations.ts +0 -177
  344. package/src/panels/builtin/session.ts +0 -81
  345. package/src/panels/builtin/shared.ts +0 -357
  346. package/src/panels/builtin-panels.ts +0 -28
  347. package/src/panels/cost-tracker-panel.ts +0 -746
  348. package/src/panels/diff-panel.ts +0 -797
  349. package/src/panels/diff-review-panel.ts +0 -395
  350. package/src/panels/expandable-list-panel.ts +0 -189
  351. package/src/panels/fleet-deep-link.ts +0 -31
  352. package/src/panels/fleet-panel.ts +0 -799
  353. package/src/panels/git-panel.ts +0 -800
  354. package/src/panels/hosted-session-panel.ts +0 -221
  355. package/src/panels/index.ts +0 -21
  356. package/src/panels/local-auth-panel.ts +0 -478
  357. package/src/panels/notifications-feed.ts +0 -127
  358. package/src/panels/notifications-panel.ts +0 -160
  359. package/src/panels/panel-confirm-overlay.ts +0 -72
  360. package/src/panels/panel-manager.ts +0 -791
  361. package/src/panels/plugins-panel.ts +0 -371
  362. package/src/panels/scrollable-list-panel.ts +0 -498
  363. package/src/panels/skills-panel.ts +0 -454
  364. package/src/panels/token-budget-panel.ts +0 -688
  365. package/src/panels/types.ts +0 -191
  366. package/src/panels/wrfc-panel-format.ts +0 -133
  367. package/src/renderer/diff-view.ts +0 -134
  368. package/src/renderer/fleet-tab-strip.ts +0 -56
  369. package/src/renderer/footer-tips.ts +0 -53
  370. package/src/renderer/fullscreen-primitives.ts +0 -146
  371. package/src/renderer/fullscreen-workspace.ts +0 -194
  372. package/src/renderer/hint-grammar.ts +0 -52
  373. package/src/renderer/modal-factory.ts +0 -480
  374. package/src/renderer/overlay-box.ts +0 -154
  375. package/src/renderer/overlay-viewport.ts +0 -104
  376. package/src/renderer/panel-composite.ts +0 -145
  377. package/src/renderer/panel-workspace-bar.ts +0 -54
  378. package/src/renderer/process-indicator.ts +0 -163
  379. package/src/renderer/tab-strip.ts +0 -222
  380. package/src/renderer/text-layout.ts +0 -31
  381. package/src/renderer/tool-call.ts +0 -276
  382. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  383. package/src/renderer/tool-result-summary.ts +0 -219
  384. package/src/runtime/diagnostics/panels/index.ts +0 -21
  385. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  386. package/src/runtime/perf/panel-contracts.ts +0 -32
  387. package/src/runtime/store/domains/panels.ts +0 -117
  388. package/src/verification/verification-ledger.ts +0 -707
  389. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  390. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  391. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -0,0 +1,384 @@
1
+ /**
2
+ * work-tree-render.ts, drawing one TurnModel into transcript lines.
3
+ *
4
+ * Lays the turn's lanes out (lane-graph/layout.ts), paints every row
5
+ * (lane-graph/paint.ts), opens bodies under their beads, and reports what the
6
+ * rest of the transcript needs: the block registry entries (so copy, save,
7
+ * bookmark, Tab and /expand work on beads), the line each message landed on,
8
+ * the focusable rows, and the gutter a still-streaming answer continues with.
9
+ *
10
+ * Line numbers in the result are relative to the turn's first line.
11
+ */
12
+
13
+ import { createEmptyLine, type Line } from '@pellux/goodvibes-sdk/platform/types';
14
+ import { parseDiffForApply } from '@pellux/goodvibes-sdk/platform/core';
15
+ import { MARKDOWN_TEXT_COL, renderMarkdown, renderMarkdownTracked } from '../renderer/markdown.ts';
16
+ import { renderSystemMessage } from '../renderer/system-message.ts';
17
+ import { renderThinkingBlock } from '../renderer/thinking.ts';
18
+ import { renderConversationFoldedRow } from '../renderer/conversation-surface.ts';
19
+ import { BORDERS } from '../renderer/layout.ts';
20
+ import { UIFactory } from '../renderer/ui-factory.ts';
21
+ import { activeTokens } from '../renderer/theme.ts';
22
+ import { laneGlyphs, type TreeGlyphSetName } from '../renderer/lane-graph/glyphs.ts';
23
+ import { layoutLaneGraph, SPINE, type GutterRow, type LaneEvent } from '../renderer/lane-graph/layout.ts';
24
+ import {
25
+ applyFocus,
26
+ laneColor,
27
+ paintBeadRow,
28
+ paintBody,
29
+ paintFoldedRow,
30
+ paintGutter,
31
+ paintHeadRow,
32
+ paintMergeRow,
33
+ paintSpawnRow,
34
+ shiftUnderGutter,
35
+ textColumn,
36
+ type GraphPaint,
37
+ } from '../renderer/lane-graph/paint.ts';
38
+ import { semanticSummaryFor } from '../renderer/lane-graph/semantic-memo.ts';
39
+ import type { BlockMeta } from './conversation-types.ts';
40
+ import { beadKeyOf, beadMoreKeyOf, type BeadModel, type LaneModel, type TurnModel, type TurnRow } from './work-tree-model.ts';
41
+
42
+ export interface TurnRenderOptions {
43
+ readonly width: number;
44
+ readonly glyphSet: TreeGlyphSetName;
45
+ /** Focused row id, when the keyboard is in the work tree. */
46
+ readonly focusId: string | null;
47
+ readonly lineNumberMode: 'all' | 'code' | 'off';
48
+ readonly collapseThreshold: number;
49
+ /** Code blocks longer than the threshold start collapsed: their default is written here. */
50
+ readonly collapseState: Map<string, boolean>;
51
+ /** Whether a system message at this index is navigable (error navigation). */
52
+ readonly isNavigableSystem: (messageIndex: number) => boolean;
53
+ /** Spinner frame for running beads. */
54
+ readonly frame: number;
55
+ }
56
+
57
+ export interface TurnRender {
58
+ readonly lines: Line[];
59
+ /** Block metas with startLine relative to the turn and blockIndex relative to the turn's first block. */
60
+ readonly blocks: BlockMeta[];
61
+ readonly errorLines: number[];
62
+ /** absolute message index → first relative line. */
63
+ readonly messageLines: ReadonlyMap<number, number>;
64
+ readonly focusIds: ReadonlySet<string>;
65
+ /** The gutter under the turn's last row and the turn's text column, for a streaming continuation. */
66
+ readonly tail: { readonly gutter: GutterRow; readonly gutterWidth: number };
67
+ }
68
+
69
+ /** Focus ids: beads by bead id, lanes and turns by their collapse key. */
70
+ export function laneFocusId(lane: LaneModel): string {
71
+ return lane.key;
72
+ }
73
+
74
+ function eventFor(row: TurnRow, folded: boolean): LaneEvent {
75
+ switch (row.kind) {
76
+ case 'head': return folded ? { kind: 'turn' } : { kind: 'head' };
77
+ case 'prose': return { kind: 'row' };
78
+ case 'bead': return { kind: 'bead', lane: row.bead.lane };
79
+ case 'spawn': return { kind: 'spawn', parent: row.lane.parent, lane: row.lane.id };
80
+ case 'merge': return { kind: 'merge', lane: row.lane.id };
81
+ case 'folded': return { kind: 'folded', lane: row.lane.parent, child: row.lane.id };
82
+ case 'answer': return { kind: 'answer' };
83
+ }
84
+ }
85
+
86
+ /** Prose at the text column: markdown rendered narrower and shifted, code blocks registered. */
87
+ function proseLines(
88
+ content: string,
89
+ messageIndex: number,
90
+ p: GraphPaint,
91
+ options: TurnRenderOptions,
92
+ blocks: BlockMeta[],
93
+ baseLine: number,
94
+ ): Line[] {
95
+ const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
96
+ const width = Math.max(20, p.width - shift);
97
+ const numbered = options.lineNumberMode === 'all';
98
+ const numWidth = numbered ? Math.max(3, String(content.split('\n').length).length) : 0;
99
+ const gutter = numbered ? numWidth + 3 : 0;
100
+ const { lines, codeBlocks } = renderMarkdownTracked(content, width - gutter, { codeBlockLineNumbers: options.lineNumberMode === 'code' });
101
+ for (const cb of codeBlocks) {
102
+ const collapseKey = `code_${messageIndex}_${cb.startOffset}`;
103
+ if (cb.rawContent.split('\n').length > options.collapseThreshold && !options.collapseState.has(collapseKey)) {
104
+ options.collapseState.set(collapseKey, true);
105
+ }
106
+ blocks.push({ blockIndex: blocks.length, collapseKey, type: 'code', startLine: baseLine + cb.startOffset, lineCount: cb.lineCount, rawContent: cb.rawContent });
107
+ }
108
+ if (!numbered) return lines;
109
+ return lines.map((line, i) => {
110
+ const out = createEmptyLine(width);
111
+ const label = UIFactory.stringToLine(`${String(i + 1).padStart(numWidth)} │ `, gutter, { fg: activeTokens().textFaint });
112
+ for (let c = 0; c < gutter; c++) out[MARKDOWN_TEXT_COL + c] = label[c] ?? out[MARKDOWN_TEXT_COL + c]!;
113
+ for (let c = MARKDOWN_TEXT_COL; c < line.length && c + gutter < width; c++) out[c + gutter] = line[c]!;
114
+ return out;
115
+ });
116
+ }
117
+
118
+ function systemLines(content: string, p: GraphPaint): Line[] {
119
+ const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
120
+ return renderSystemMessage(content, Math.max(20, p.width - shift));
121
+ }
122
+
123
+ /** The process id a backgrounded call reported (`process_id` or `processId`, top level or on its first command). */
124
+ export function backgroundProcessId(result: string | undefined): string | undefined {
125
+ if (!result || !/process_?[iI]d/.test(result)) return undefined;
126
+ try {
127
+ const parsed: unknown = JSON.parse(result);
128
+ const find = (value: unknown, depth: number): string | undefined => {
129
+ if (depth > 3 || value === null || typeof value !== 'object') return undefined;
130
+ if (Array.isArray(value)) {
131
+ for (const item of value) { const id = find(item, depth + 1); if (id) return id; }
132
+ return undefined;
133
+ }
134
+ const rec = value as Record<string, unknown>;
135
+ for (const key of ['process_id', 'processId'] as const) if (typeof rec[key] === 'string' && (rec[key] as string).length > 0) return rec[key] as string;
136
+ for (const child of Object.values(rec)) { const id = find(child, depth + 1); if (id) return id; }
137
+ return undefined;
138
+ };
139
+ return find(parsed, 0);
140
+ } catch {
141
+ return /"process_?[iI]d"\s*:\s*"([^"]+)"/.exec(result)?.[1];
142
+ }
143
+ }
144
+
145
+ function beadMeta(bead: BeadModel, startLine: number, lineCount: number, capped: boolean, laneKey: string | undefined): BlockMeta {
146
+ const isDiff = bead.body?.kind === 'diff';
147
+ const raw = bead.result ?? JSON.stringify(bead.call.arguments, null, 2);
148
+ let meta: BlockMeta = {
149
+ blockIndex: 0,
150
+ collapseKey: beadKeyOf(bead.id),
151
+ type: isDiff ? 'diff' : 'tool',
152
+ startLine,
153
+ lineCount,
154
+ rawContent: isDiff && bead.body?.kind === 'diff' ? bead.body.diff : raw,
155
+ toolName: bead.call.name,
156
+ workTree: {
157
+ kind: 'bead', id: bead.id, hasBody: bead.body !== null, open: bead.open, capped, moreKey: beadMoreKeyOf(bead.id), laneKey,
158
+ processId: bead.status === 'bg' ? backgroundProcessId(bead.result) : undefined,
159
+ },
160
+ };
161
+ if (bead.body?.kind === 'diff' && bead.body.numbered) {
162
+ const parsed = parseDiffForApply(bead.body.diff);
163
+ if (parsed.filePath) meta = { ...meta, ...parsed };
164
+ }
165
+ return meta;
166
+ }
167
+
168
+ export function renderTurn(model: TurnModel, options: TurnRenderOptions): TurnRender {
169
+ const glyphs = laneGlyphs(options.glyphSet);
170
+ const folded = model.folded;
171
+ // A turn with nothing for its header to say and no calls or agents is plain
172
+ // prose: no spine, no header row, the text at the text column.
173
+ const bare = model.headerText === '' && model.toolCount === 0 && model.agentCount === 0;
174
+ // A folded turn is its header plus its answer; nothing else draws.
175
+ const rows = bare ? model.rows.filter((r) => r.kind !== 'head') : folded ? model.rows.filter((r) => r.kind === 'head' || r.kind === 'answer') : model.rows;
176
+ const layout = layoutLaneGraph(rows.map((r) => ((folded || bare) && (r.kind === 'answer' || r.kind === 'prose') ? { kind: 'row' } as const : eventFor(r, folded))));
177
+ const p: GraphPaint = { width: options.width, gutterWidth: layout.gutterWidth, glyphs, frame: options.frame };
178
+
179
+ const lines: Line[] = [];
180
+ const blocks: BlockMeta[] = [];
181
+ const errorLines: number[] = [];
182
+ const messageLines = new Map<number, number>();
183
+ const focusIds = new Set<string>();
184
+ const laneOfBead = new Map<string, string>();
185
+ const noteMessage = (index: number | undefined, line: number): void => {
186
+ if (index !== undefined && index >= 0 && !messageLines.has(index)) messageLines.set(index, line);
187
+ };
188
+ const focusIf = (line: Line, id: string): void => {
189
+ focusIds.add(id);
190
+ if (options.focusId === id) applyFocus(line, options.width, glyphs);
191
+ };
192
+
193
+ const resultMembers: Array<{ index: number; key: string }> = [];
194
+ for (const row of model.rows) {
195
+ const bead = row.kind === 'bead' || row.kind === 'spawn' || row.kind === 'folded' ? row.bead : undefined;
196
+ if (bead?.resultIndex !== undefined) resultMembers.push({ index: bead.resultIndex, key: beadKeyOf(bead.id) });
197
+ }
198
+
199
+ rows.forEach((row, i) => {
200
+ const gutter = layout.rows[i]!;
201
+ const cont = layout.cont[i]!;
202
+ // Two prose blocks in a row keep a blank row between them, lanes running past it.
203
+ if ((row.kind === 'prose' || row.kind === 'answer') && rows[i - 1]?.kind === 'prose') {
204
+ const blank = createEmptyLine(options.width);
205
+ if (!folded && !bare) paintGutter(blank, layout.cont[i - 1]!, p);
206
+ lines.push(blank);
207
+ }
208
+ const start = lines.length;
209
+ switch (row.kind) {
210
+ case 'head': {
211
+ const line = paintHeadRow(gutter, model.headerText, folded, p);
212
+ focusIf(line, model.turnKey);
213
+ lines.push(line);
214
+ noteMessage(model.headIndex, start);
215
+ blocks.push({
216
+ blockIndex: 0,
217
+ collapseKey: model.turnKey,
218
+ type: 'assistant_turn',
219
+ startLine: start,
220
+ lineCount: 1,
221
+ rawContent: `assistant turn: ${model.headerText}`,
222
+ // Result messages behind closed beads, paired with the bead keys that reveal them.
223
+ groupMemberIndexes: resultMembers.map((m) => m.index),
224
+ groupMemberKeys: resultMembers.map((m) => m.key),
225
+ workTree: { kind: 'turn', id: model.turnKey, hasBody: true, open: !folded, capped: false },
226
+ });
227
+ break;
228
+ }
229
+ case 'prose': {
230
+ if (row.role === 'thinking' || row.role === 'summary') {
231
+ const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
232
+ const width = Math.max(20, p.width - shift);
233
+ let body: Line[];
234
+ if (row.role === 'thinking') {
235
+ // Collapsed by default: one row naming the size; the text stays behind the toggle.
236
+ const collapseKey = `msg_${row.messageIndex}_thinking`;
237
+ if (!options.collapseState.has(collapseKey)) options.collapseState.set(collapseKey, true);
238
+ const collapsed = options.collapseState.get(collapseKey) === true;
239
+ const count = row.content.split('\n').length;
240
+ body = collapsed
241
+ ? [renderConversationFoldedRow(width, {
242
+ marker: BORDERS.THINKING.char,
243
+ markerFg: activeTokens().reasoning,
244
+ label: 'thinking',
245
+ labelFg: activeTokens().reasoning,
246
+ detailFg: activeTokens().textMuted,
247
+ }, [{ text: ` ${glyphs.closed} ${count} line${count === 1 ? '' : 's'} `, fg: activeTokens().textFaint }], '')]
248
+ : renderThinkingBlock(row.content, width);
249
+ blocks.push({ blockIndex: 0, collapseKey, type: 'thinking', startLine: start, lineCount: body.length, rawContent: row.content });
250
+ } else {
251
+ body = renderThinkingBlock(row.content, width);
252
+ }
253
+ for (const source of body) lines.push(shiftUnderGutter(source, shift, cont, p));
254
+ break;
255
+ }
256
+ const body = row.role === 'assistant' ? proseLines(row.content, row.messageIndex, p, options, blocks, start) : systemLines(row.content, p);
257
+ const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
258
+ for (const source of body) lines.push(shiftUnderGutter(source, shift, cont, p));
259
+ if (row.role === 'system' && options.isNavigableSystem(row.messageIndex)) errorLines.push(start);
260
+ noteMessage(row.messageIndex, start);
261
+ break;
262
+ }
263
+ case 'answer': {
264
+ const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
265
+ const body = proseLines(row.content, row.messageIndex, p, options, blocks, start);
266
+ const first = Math.max(0, body.findIndex((l) => l.some((c) => c.char !== ' ' && c.char !== '')));
267
+ body.forEach((source, k) => {
268
+ const before = i > 0 ? layout.cont[i - 1]! : cont;
269
+ const g = folded || bare ? null : k === first ? gutter : k < first ? before : cont;
270
+ lines.push(shiftUnderGutter(source, shift, g, p));
271
+ });
272
+ noteMessage(row.messageIndex, start);
273
+ break;
274
+ }
275
+ case 'bead': {
276
+ const bead = row.bead;
277
+ const color = laneColor(layout.laneColor.get(bead.lane) ?? 0);
278
+ const line = paintBeadRow(gutter, {
279
+ status: bead.status,
280
+ name: bead.name,
281
+ arg: bead.arg,
282
+ summary: bead.summary,
283
+ time: bead.time,
284
+ laneColor: color,
285
+ hasBody: bead.body !== null,
286
+ open: bead.open,
287
+ answers: bead.answers,
288
+ }, p);
289
+ focusIf(line, bead.id);
290
+ lines.push(line);
291
+ let capped = false;
292
+ if (bead.open && bead.body) {
293
+ const semantic = bead.body.kind === 'diff' ? semanticSummaryFor(bead.body.diff, bead.body.path) : undefined;
294
+ const drawn = paintBody(bead.body, p, { expanded: bead.expanded, semantic });
295
+ capped = drawn.capped;
296
+ for (const bodyLine of drawn.lines) {
297
+ paintGutter(bodyLine, cont, p);
298
+ lines.push(bodyLine);
299
+ }
300
+ }
301
+ const laneKey = bead.lane === SPINE ? undefined : laneOfBead.get(bead.lane);
302
+ blocks.push(beadMeta(bead, start, lines.length - start, capped || (bead.open && bead.expanded), laneKey));
303
+ noteMessage(bead.scope === '' ? bead.messageIndex : undefined, start);
304
+ noteMessage(bead.resultIndex, start);
305
+ break;
306
+ }
307
+ case 'spawn':
308
+ case 'folded': {
309
+ const lane = row.lane;
310
+ laneOfBead.set(lane.id, lane.key);
311
+ const color = laneColor(layout.laneColor.get(lane.id) ?? 0);
312
+ const line = row.kind === 'spawn'
313
+ ? paintSpawnRow(gutter, { name: lane.name, arg: lane.arg, laneColor: color }, p)
314
+ : paintFoldedRow(gutter, { name: lane.name, arg: lane.arg, laneColor: color, outcome: lane.outcome, summary: lane.foldSummary, time: lane.time }, p);
315
+ focusIf(line, laneFocusId(lane));
316
+ lines.push(line);
317
+ blocks.push({
318
+ blockIndex: 0,
319
+ collapseKey: lane.key,
320
+ type: 'tool',
321
+ startLine: start,
322
+ lineCount: 1,
323
+ rawContent: `${lane.name}: ${lane.arg}\n${lane.mergeText}`,
324
+ toolName: row.bead.call.name,
325
+ workTree: {
326
+ kind: 'lane', id: laneFocusId(lane), hasBody: true, open: row.kind === 'spawn', capped: false, finished: lane.outcome !== 'run', laneKey: lane.key,
327
+ agentId: lane.id, colorIndex: layout.laneColor.get(lane.id) ?? 0,
328
+ },
329
+ });
330
+ noteMessage(row.bead.scope === '' ? row.bead.messageIndex : undefined, start);
331
+ noteMessage(row.bead.resultIndex, start);
332
+ break;
333
+ }
334
+ case 'merge': {
335
+ lines.push(paintMergeRow(gutter, row.lane.mergeText, row.lane.outcome === 'warn' || row.lane.outcome === 'err', p));
336
+ break;
337
+ }
338
+ }
339
+ });
340
+
341
+ blocks.forEach((b, k) => { b.blockIndex = k; });
342
+ return {
343
+ lines,
344
+ blocks,
345
+ errorLines,
346
+ messageLines,
347
+ focusIds,
348
+ tail: { gutter: layout.cont[layout.cont.length - 1] ?? [], gutterWidth: layout.gutterWidth },
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Text still streaming into the latest turn: at that turn's text column, with
354
+ * the lanes live under its last row running down the gutter beside it.
355
+ */
356
+ export function renderStreamingContinuation(
357
+ content: string,
358
+ width: number,
359
+ tail: { readonly gutter: GutterRow; readonly gutterWidth: number },
360
+ glyphSet: TreeGlyphSetName,
361
+ ): Line[] {
362
+ const p: GraphPaint = { width, gutterWidth: tail.gutterWidth, glyphs: laneGlyphs(glyphSet), frame: 0 };
363
+ const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
364
+ return renderMarkdown(content, Math.max(20, width - shift), { isStreaming: true }).map((line) => shiftUnderGutter(line, shift, tail.gutter, p));
365
+ }
366
+
367
+ /** One bead with no turn around it (a result whose call is outside the slice). */
368
+ export function renderSingleBead(bead: BeadModel, options: TurnRenderOptions): TurnRender {
369
+ const glyphs = laneGlyphs(options.glyphSet);
370
+ const p: GraphPaint = { width: options.width, gutterWidth: 6, glyphs, frame: options.frame };
371
+ const lines: Line[] = [paintBeadRow([{ role: 'bead', color: 0 }], {
372
+ status: bead.status, name: bead.name, arg: bead.arg, summary: bead.summary, time: bead.time,
373
+ laneColor: laneColor(0), hasBody: bead.body !== null, open: bead.open,
374
+ }, p)];
375
+ const focusIds = new Set([bead.id]);
376
+ if (options.focusId === bead.id) applyFocus(lines[0]!, options.width, glyphs);
377
+ let capped = false;
378
+ if (bead.open && bead.body) {
379
+ const drawn = paintBody(bead.body, p, { expanded: bead.expanded, semantic: bead.body.kind === 'diff' ? semanticSummaryFor(bead.body.diff, bead.body.path) : undefined });
380
+ capped = drawn.capped;
381
+ lines.push(...drawn.lines);
382
+ }
383
+ return { lines, blocks: [beadMeta(bead, 0, lines.length, capped, undefined)], errorLines: [], messageLines: new Map(), focusIds, tail: { gutter: [], gutterWidth: 6 } };
384
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * work-tree-sources.ts, the live facts the conversation work tree reads that
3
+ * the message snapshot does not carry.
4
+ *
5
+ * A message snapshot says which tools were called and what they returned. It
6
+ * does not say when they ran, which agent a call started or how that agent is
7
+ * doing, or which call a permission prompt is holding. Those come from here,
8
+ * read at render time, so the work tree states them without the transcript
9
+ * format changing. Every field is optional: with no sources the work tree
10
+ * still draws, just without times, agent lanes or waiting beads.
11
+ */
12
+
13
+ import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
14
+
15
+ type Message = ConversationMessageSnapshot;
16
+
17
+ export interface CallTiming {
18
+ /** Epoch ms the call started executing. */
19
+ readonly startedAt?: number | undefined;
20
+ /** Set once it settled. */
21
+ readonly durationMs?: number | undefined;
22
+ }
23
+
24
+ /** How a finished turn ended. A completed turn's header says nothing extra; the others say so. */
25
+ export type TurnOutcome = 'completed' | 'failed' | 'cancelled';
26
+
27
+ export interface TurnTiming {
28
+ readonly startedAt: number;
29
+ readonly endedAt?: number | undefined;
30
+ /** Set once the turn ended. */
31
+ readonly outcome?: TurnOutcome | undefined;
32
+ /**
33
+ * userMessageFingerprint() of the user message the turn answered, when
34
+ * known. A record whose fingerprint does not match the message now at its
35
+ * index (the transcript was compacted, reset or replaced) is not this turn's.
36
+ */
37
+ readonly fingerprint?: string | undefined;
38
+ }
39
+
40
+ /**
41
+ * A short identity for a user message: its length and opening text. Enough to
42
+ * tell whether the message at an index is still the one a record was made
43
+ * for, without hashing whole messages on every build.
44
+ */
45
+ export function userMessageFingerprint(message: Message | undefined): string | undefined {
46
+ if (!message || message.role !== 'user') return undefined;
47
+ const text = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
48
+ return `${text.length}:${text.slice(0, 80)}`;
49
+ }
50
+
51
+ /** One phase of a WRFC chain: the agent that ran it and what it found. */
52
+ export interface WrfcPhaseInfo {
53
+ readonly agentId: string;
54
+ readonly role: 'engineer' | 'reviewer' | 'fixer' | 'integrator' | 'verifier' | 'orchestrator' | 'owner';
55
+ readonly task: string;
56
+ readonly status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
57
+ readonly startedAt?: number | undefined;
58
+ readonly completedAt?: number | undefined;
59
+ /** A review's findings (the latest review report's issues), when this phase is that review. */
60
+ readonly findings?: readonly string[] | undefined;
61
+ /** Whether this review passed, when known. */
62
+ readonly passed?: boolean | undefined;
63
+ /** First lines of the phase agent's report. */
64
+ readonly output?: string | undefined;
65
+ }
66
+
67
+ /** What the work tree needs about a spawned agent to draw its lane. */
68
+ export interface AgentLaneInfo {
69
+ readonly id: string;
70
+ /** The template or role name ("engineer", "tester"). */
71
+ readonly name: string;
72
+ readonly task: string;
73
+ readonly status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
74
+ readonly startedAt?: number | undefined;
75
+ readonly completedAt?: number | undefined;
76
+ readonly toolCallCount: number;
77
+ /** Real cost in dollars when priced; undefined when not. */
78
+ readonly costUsd?: number | undefined;
79
+ readonly error?: string | undefined;
80
+ /** The agent's own transcript (its tool calls become beads in its lane). */
81
+ readonly messages: readonly Message[];
82
+ /** Set when this agent owns a WRFC chain: its lane shows the chain's phases. */
83
+ readonly wrfcPhases?: readonly WrfcPhaseInfo[] | undefined;
84
+ /** Whether the chain passed, for a WRFC owner. */
85
+ readonly wrfcPassed?: boolean | undefined;
86
+ /** The harness name for an agent run by a hosted harness ("Claude Code"): it names the lane. */
87
+ readonly hostedLabel?: string | undefined;
88
+ }
89
+
90
+ export interface WorkTreeSources {
91
+ readonly callTiming?: ((callId: string) => CallTiming | undefined) | undefined;
92
+ /** Wall time of the turn answering the user message at this absolute index. */
93
+ readonly turnTiming?: ((userMessageIndex: number) => TurnTiming | undefined) | undefined;
94
+ readonly agent?: ((agentId: string) => AgentLaneInfo | null) | undefined;
95
+ /** Call ids a permission prompt is holding right now. */
96
+ readonly waitingCallIds?: (() => ReadonlySet<string>) | undefined;
97
+ /** Whether the main conversation is working on a turn right now. */
98
+ readonly turnActive?: (() => boolean) | undefined;
99
+ /**
100
+ * How the turn answering the user message at this index ended, when it is
101
+ * known from a live turn-end event or the session's saved record. Checked
102
+ * against the message's fingerprint by the model.
103
+ */
104
+ readonly turnOutcome?: ((userMessageIndex: number) => { readonly outcome: TurnOutcome; readonly fingerprint: string } | undefined) | undefined;
105
+ readonly now?: (() => number) | undefined;
106
+ }
107
+
108
+ /** Most calls whose timing is kept; the oldest go first. */
109
+ const MAX_CALLS = 5000;
110
+ /** Most turns whose timing is kept. */
111
+ const MAX_TURNS = 1000;
112
+
113
+ /**
114
+ * Tool and turn timings, fed by the runtime's tool and turn events. Bounded:
115
+ * a long session keeps the most recent MAX_CALLS calls and MAX_TURNS turns,
116
+ * and a call that falls out simply renders without a time.
117
+ */
118
+ export class WorkTreeTimingStore {
119
+ private readonly calls = new Map<string, { startedAt?: number; durationMs?: number }>();
120
+ private readonly turns = new Map<number, { startedAt: number; endedAt?: number; outcome?: TurnOutcome; fingerprint?: string }>();
121
+ private openTurn: number | undefined;
122
+
123
+ callStarted(callId: string, startedAt: number): void {
124
+ const known = this.calls.get(callId);
125
+ this.calls.delete(callId);
126
+ this.calls.set(callId, { ...known, startedAt });
127
+ this.trim(this.calls, MAX_CALLS);
128
+ }
129
+
130
+ callSettled(callId: string, durationMs: number | undefined, endedAt: number): void {
131
+ const known = this.calls.get(callId);
132
+ const started = known?.startedAt ?? (durationMs !== undefined ? endedAt - durationMs : undefined);
133
+ const duration = durationMs ?? (started !== undefined ? endedAt - started : undefined);
134
+ this.calls.delete(callId);
135
+ this.calls.set(callId, { startedAt: started, durationMs: duration });
136
+ this.trim(this.calls, MAX_CALLS);
137
+ }
138
+
139
+ turnStarted(userMessageIndex: number, startedAt: number, fingerprint?: string): void {
140
+ this.turns.delete(userMessageIndex);
141
+ this.turns.set(userMessageIndex, fingerprint !== undefined ? { startedAt, fingerprint } : { startedAt });
142
+ this.openTurn = userMessageIndex;
143
+ this.trim(this.turns, MAX_TURNS);
144
+ }
145
+
146
+ /**
147
+ * Close the open turn with how it ended. Returns the closed turn's index and
148
+ * record, or undefined when no turn was open.
149
+ */
150
+ turnEnded(endedAt: number, outcome: TurnOutcome = 'completed'): { readonly index: number; readonly timing: TurnTiming } | undefined {
151
+ if (this.openTurn === undefined) return undefined;
152
+ const index = this.openTurn;
153
+ const turn = this.turns.get(index);
154
+ this.openTurn = undefined;
155
+ if (!turn) return undefined;
156
+ if (turn.endedAt === undefined) {
157
+ turn.endedAt = endedAt;
158
+ turn.outcome = outcome;
159
+ }
160
+ return { index, timing: turn };
161
+ }
162
+
163
+ callTiming(callId: string): CallTiming | undefined {
164
+ return this.calls.get(callId);
165
+ }
166
+
167
+ turnTiming(userMessageIndex: number): TurnTiming | undefined {
168
+ return this.turns.get(userMessageIndex);
169
+ }
170
+
171
+ clear(): void {
172
+ this.calls.clear();
173
+ this.turns.clear();
174
+ this.openTurn = undefined;
175
+ }
176
+
177
+ private trim<K, V>(map: Map<K, V>, max: number): void {
178
+ while (map.size > max) {
179
+ const oldest = map.keys().next();
180
+ if (oldest.done) return;
181
+ map.delete(oldest.value);
182
+ }
183
+ }
184
+ }