@pellux/goodvibes-tui 2.0.21 → 2.1.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 (388) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +16 -6
  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-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /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
+ }