@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,699 @@
1
+ /**
2
+ * agents-modal.ts, the Agents modal (/agents, F2, ctrl+o, ctrl+p "agents" and
3
+ * every old console name: fleet, cockpit, tasks, wrfc, inspector, ops, …).
4
+ *
5
+ * One list of everything running or finished this session (agents, WRFC
6
+ * chains and their members, workflows, watchers, schedules, background
7
+ * processes, third-party agents hosted over ACP, observed foreign sessions)
8
+ * plus the daemon-hosted conversation this terminal is attached to. The right
9
+ * side tails the selected one live.
10
+ *
11
+ * Keys on the list: ↑↓ move; Enter opens the selected agent (or background
12
+ * process) full screen, outside the modal, with its own composer (or acts on
13
+ * a row waiting on you: pick a winner, resolve a conflict); other kinds with
14
+ * a transcript open full height inside the modal; s steers
15
+ * (inline input); x stops it and its descendants (asks first); i interrupts;
16
+ * p pauses or resumes; n hosts a third-party agent; D discards a worktree;
17
+ * b jumps to the next thing waiting on you; f follows the newest running
18
+ * agent; v switches to the archive, a archives or restores, A archives
19
+ * everything finished; / filters. In the full view ctrl+x stops (asks first),
20
+ * [ ] steps between opened agents, and Esc goes back up a level and never
21
+ * interrupts anything.
22
+ */
23
+
24
+ import { readFile } from 'node:fs/promises';
25
+ import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
26
+ import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
27
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
28
+ import type { ProcessNode, SteerResult } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
29
+ import type { Line } from '@pellux/goodvibes-sdk/platform/types';
30
+ import type { SurfaceModal, SurfaceModalHost } from './surface-modal-host.ts';
31
+ import type { KitHint, SurfaceLayer } from '../renderer/surface-kit.ts';
32
+ import type { KitRow } from '../renderer/surface-kit-list.ts';
33
+ import { activeTokens } from '../renderer/theme.ts';
34
+ import { renderAgentsModal, type AgentsDetail, type AgentsModalView, type AgentsText } from '../renderer/agents-modal.ts';
35
+ import { formatUsd } from '../renderer/usage-modal.ts';
36
+ import {
37
+ fleetAttentionText, fleetNodeAttention, fleetStallMarker, isBlockedOnUserNode, isRunningProcessState, isTerminalProcessState,
38
+ type FleetReadModel, type FleetTreeRow,
39
+ } from '../views/fleet-read-model.ts';
40
+ import type { FleetActs } from '../views/fleet-acts.ts';
41
+ import type { FleetSpawn } from '../views/fleet-spawn.ts';
42
+ import {
43
+ activeFleetTab, attachFleetTab, EMPTY_FLEET_TABS_STATE, isAttachableFleetKind, stepFleetTab, type FleetTab, type FleetTabsState,
44
+ } from '../views/fleet-session-tabs.ts';
45
+ import { liveSteerableLabels, reconcileSteerBadges, steerBadgeGlyph, steerRefusalMessage } from '../views/fleet-steer.ts';
46
+ import { FleetStopTracker, fleetKillConfirmArgs, fleetStateDisplay, toggleFleetPause } from '../views/fleet-stop.ts';
47
+ import { formatFleetCost, renderFleetDetailLines } from '../views/fleet-format.ts';
48
+ import { hasFleetCost } from '../views/fleet-read-model.ts';
49
+ import { parseAgentLedger, renderFleetAgentTranscript, renderFleetChainSummary, renderFleetLedgerFallback, renderFleetTranscriptLoading } from '../views/fleet-transcript.ts';
50
+ import { isObservedExternalNode } from '../views/fleet-observed-render.ts';
51
+ import type { HostedSessionFeed } from '../views/hosted-session-feed.ts';
52
+ import { formatElapsed } from '../utils/format-elapsed.ts';
53
+ import { isTextBackspace } from './delete-key-policy.ts';
54
+ import type { ConfirmOptions } from './confirm-dialog.ts';
55
+ import type { ViewTarget } from './views.ts';
56
+ import { hostedBody, hostedHeaderTexts, hostedRowTexts, transcriptTail } from './agents-modal-text.ts';
57
+
58
+ /** What the modal can do to a process (the process registry, through the fleet read model). */
59
+ export interface FleetActionCallbacks {
60
+ readonly interrupt: (id: string) => boolean;
61
+ readonly resume: (id: string) => boolean;
62
+ readonly kill: (id: string, opts: { readonly cascade: boolean }) => readonly string[];
63
+ /** An agent's conversation, live or just finished; empty when unavailable. */
64
+ readonly getConversationSnapshot: (agentId: string) => readonly ConversationMessageSnapshot[];
65
+ /** The agent's on-disk event log, the fallback when its conversation is gone. */
66
+ readonly resolveSessionLogPath: (agentId: string) => string;
67
+ readonly steer: (id: string, text: string) => SteerResult;
68
+ }
69
+
70
+ export interface AgentsModalDeps {
71
+ readonly readModel: FleetReadModel;
72
+ readonly actions: FleetActionCallbacks;
73
+ readonly acts?: FleetActs | null;
74
+ readonly spawn?: FleetSpawn | null;
75
+ readonly configManager?: ConfigManager | null;
76
+ readonly hosted?: HostedSessionFeed;
77
+ /** Send a line to the hosted session (/hosted say). */
78
+ readonly steerHosted?: (text: string) => void;
79
+ /** This session's own cost (the "you" half of the header), or null when unpriced. */
80
+ readonly sessionCost?: () => number | null;
81
+ readonly confirm?: (options: ConfirmOptions) => Promise<boolean>;
82
+ readonly requestRender: () => void;
83
+ /** Tick for elapsed times and steer badges (ms; 0 disables, for tests). */
84
+ readonly tickMs?: number;
85
+ /** Open an agent or background process full screen (shell/session-views.ts); false when it is not known here. */
86
+ readonly openSessionView?: (target: { readonly kind: 'agent' | 'process'; readonly id: string }) => boolean;
87
+ }
88
+
89
+ const HOSTED_ID = 'hosted-session';
90
+
91
+ type ListEntry = { readonly kind: 'node'; readonly row: FleetTreeRow } | { readonly kind: 'hosted' };
92
+
93
+ /** One key in the vocabulary the fleet controllers (acts, spawn) take. */
94
+ function keyName(token: InputToken): string | null {
95
+ if (token.type === 'text') return token.value.length === 1 ? token.value : null;
96
+ if (token.type === 'key') return token.logicalName ?? null;
97
+ return null;
98
+ }
99
+
100
+ export class AgentsModal implements SurfaceModal {
101
+ readonly name = 'agents';
102
+ view: 'active' | 'archived' = 'active';
103
+ query = '';
104
+ filtering = false;
105
+ selectedId: string | null = null;
106
+ follow = false;
107
+ tabs: FleetTabsState = EMPTY_FLEET_TABS_STATE;
108
+ /** The full-height view of the hosted session is open. */
109
+ hostedFull = false;
110
+ steer: { readonly id: string; draft: string } | null = null;
111
+ status: AgentsText | null = null;
112
+ readonly stopTracker = new FleetStopTracker();
113
+ private readonly unsubs: Array<() => void> = [];
114
+ private tickTimer: ReturnType<typeof setInterval> | null = null;
115
+
116
+ constructor(private readonly deps: AgentsModalDeps) {
117
+ this.unsubs.push(deps.readModel.subscribe(() => { this.onFleetChange(); deps.requestRender(); }));
118
+ this.unsubs.push(deps.readModel.subscribeConsumed((event) => {
119
+ for (const tab of this.tabs.tabs) {
120
+ const badge = tab.steerBadge;
121
+ if (badge && badge.messageId === event.messageId && (badge.status === 'queued' || badge.status === 'dropped')) {
122
+ tab.steerBadge = { messageId: badge.messageId, status: 'consumed', queuedAt: badge.queuedAt, resolvedAt: Date.now() };
123
+ }
124
+ }
125
+ deps.requestRender();
126
+ }));
127
+ if (deps.hosted) this.unsubs.push(deps.hosted.subscribe(() => deps.requestRender()));
128
+ const tickMs = deps.tickMs ?? 1_000;
129
+ if (tickMs > 0) this.tickTimer = setInterval(() => { this.onFleetChange(); deps.requestRender(); }, tickMs);
130
+ }
131
+
132
+ onClose(): void {
133
+ for (const unsub of this.unsubs) unsub();
134
+ this.unsubs.length = 0;
135
+ if (this.tickTimer !== null) clearInterval(this.tickTimer);
136
+ this.tickTimer = null;
137
+ for (const tab of this.tabs.tabs) tab.lineCache.clear();
138
+ }
139
+
140
+ private onFleetChange(): void {
141
+ this.applyFollow();
142
+ if (this.tabs.tabs.length > 0) reconcileSteerBadges(this.tabs.tabs, (id) => this.findNode(id), Date.now());
143
+ }
144
+
145
+ // ── Data ───────────────────────────────────────────────────────────────────
146
+
147
+ private snapshotRows(): readonly FleetTreeRow[] {
148
+ return this.view === 'archived' ? this.deps.readModel.getArchivedSnapshot().rows : this.deps.readModel.getSnapshot().rows;
149
+ }
150
+
151
+ private findNode(id: string): ProcessNode | null {
152
+ return this.deps.readModel.getSnapshot().rows.find((row) => row.node.id === id)?.node ?? null;
153
+ }
154
+
155
+ private hostedAttached(): boolean {
156
+ return this.view === 'active' && Boolean(this.deps.hosted?.getState().record);
157
+ }
158
+
159
+ private matches(row: FleetTreeRow): boolean {
160
+ const q = this.query.trim().toLowerCase();
161
+ if (!q) return true;
162
+ const node = row.node;
163
+ return [node.label, node.task ?? '', node.kind, node.id, node.currentActivity?.text ?? '', node.headline?.text ?? '']
164
+ .some((text) => text.toLowerCase().includes(q));
165
+ }
166
+
167
+ /** The list in display order, grouped: hosted, running (whole subtrees), finished. */
168
+ private groups(): Array<{ readonly name: string; readonly entries: ListEntry[] }> {
169
+ const rows = this.snapshotRows().filter((row) => this.matches(row));
170
+ if (this.view === 'archived') return rows.length > 0 ? [{ name: 'Archived', entries: rows.map((row) => ({ kind: 'node', row })) }] : [];
171
+ const groups: Array<{ name: string; entries: ListEntry[] }> = [];
172
+ const hostedMatches = !this.query.trim() || 'hosted'.includes(this.query.trim().toLowerCase()) || (this.deps.hosted?.getState().record?.title ?? '').toLowerCase().includes(this.query.trim().toLowerCase());
173
+ if (this.hostedAttached() && hostedMatches) groups.push({ name: 'Hosted', entries: [{ kind: 'hosted' }] });
174
+ // A root and everything under it stays together, in the group of its liveliest node.
175
+ const all = this.snapshotRows();
176
+ const rootOf = new Map<string, string>();
177
+ let currentRoot = '';
178
+ for (const row of all) {
179
+ if (row.depth === 0) currentRoot = row.node.id;
180
+ rootOf.set(row.node.id, currentRoot);
181
+ }
182
+ const liveRoots = new Set<string>();
183
+ for (const row of all) if (!isTerminalProcessState(row.node.state)) liveRoots.add(rootOf.get(row.node.id) ?? row.node.id);
184
+ const running = rows.filter((row) => liveRoots.has(rootOf.get(row.node.id) ?? row.node.id));
185
+ const finished = rows.filter((row) => !liveRoots.has(rootOf.get(row.node.id) ?? row.node.id));
186
+ if (running.length > 0) groups.push({ name: 'Running', entries: running.map((row) => ({ kind: 'node', row })) });
187
+ if (finished.length > 0) groups.push({ name: 'Finished', entries: finished.map((row) => ({ kind: 'node', row })) });
188
+ return groups;
189
+ }
190
+
191
+ private entries(): ListEntry[] {
192
+ return this.groups().flatMap((group) => group.entries);
193
+ }
194
+
195
+ private entryId(entry: ListEntry): string {
196
+ return entry.kind === 'hosted' ? HOSTED_ID : entry.row.node.id;
197
+ }
198
+
199
+ private selectedEntry(): ListEntry | null {
200
+ const entries = this.entries();
201
+ if (entries.length === 0) return null;
202
+ return entries.find((entry) => this.entryId(entry) === this.selectedId) ?? entries[0]!;
203
+ }
204
+
205
+ selectedNode(): ProcessNode | null {
206
+ const entry = this.selectedEntry();
207
+ return entry?.kind === 'node' ? entry.row.node : null;
208
+ }
209
+
210
+ private move(delta: number): void {
211
+ const entries = this.entries();
212
+ if (entries.length === 0) return;
213
+ const current = entries.findIndex((entry) => this.entryId(entry) === this.selectedId);
214
+ const next = Math.max(0, Math.min(entries.length - 1, (current < 0 ? 0 : current) + delta));
215
+ this.selectedId = this.entryId(entries[next]!);
216
+ this.follow = false;
217
+ }
218
+
219
+ private applyFollow(): void {
220
+ if (!this.follow) return;
221
+ let best: FleetTreeRow | null = null;
222
+ for (const row of this.deps.readModel.getSnapshot().rows) {
223
+ if (!isRunningProcessState(row.node.state)) continue;
224
+ if (!best || (row.node.startedAt ?? 0) >= (best.node.startedAt ?? 0)) best = row;
225
+ }
226
+ if (best) this.selectedId = best.node.id;
227
+ }
228
+
229
+ /** Select a specific process (a deep link from a notification, the work plan, /agents --target). */
230
+ reveal(target: ViewTarget): boolean {
231
+ const rows = this.deps.readModel.getSnapshot().rows;
232
+ // A tool target (the keybindings modal's Tools tab) names a tool, not a
233
+ // process: select the first process whose current step is that tool.
234
+ const row = target.kind === 'tool'
235
+ ? rows.find((r) => r.node.currentActivity?.toolName === target.id)
236
+ : rows.find((r) => r.node.id === target.id && (!target.kind || r.node.kind === target.kind));
237
+ this.view = 'active';
238
+ this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 0 };
239
+ this.hostedFull = false;
240
+ this.query = '';
241
+ if (!row) {
242
+ this.status = target.kind === 'tool'
243
+ ? { text: `Nothing is running ${target.id} right now.`, tone: 'muted' }
244
+ : { text: `${target.id} is no longer running here.`, tone: 'warning' };
245
+ return false;
246
+ }
247
+ this.selectedId = row.node.id;
248
+ this.status = null;
249
+ return true;
250
+ }
251
+
252
+ /** Open the hosted session's full view (after /hosted new or attach). */
253
+ showHosted(): void {
254
+ this.view = 'active';
255
+ this.selectedId = HOSTED_ID;
256
+ this.hostedFull = this.hostedAttached();
257
+ this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 0 };
258
+ }
259
+
260
+ private level(): 'list' | 'full' | 'picker' {
261
+ if (this.deps.acts?.pickModeActive() || this.deps.spawn?.spawnModeActive()) return 'picker';
262
+ if (this.hostedFull || this.tabs.activeTabIndex > 0) return 'full';
263
+ return 'list';
264
+ }
265
+
266
+ // ── Actions ────────────────────────────────────────────────────────────────
267
+
268
+ private say(text: string, tone: AgentsText['tone'] = 'muted'): void {
269
+ this.status = { text, tone };
270
+ }
271
+
272
+ private openFull(host: SurfaceModalHost): void {
273
+ const entry = this.selectedEntry();
274
+ if (!entry) return;
275
+ if (entry.kind === 'hosted') { this.hostedFull = true; return; }
276
+ const node = entry.row.node;
277
+ if (this.deps.acts?.handleTreeKey('enter', node)) return;
278
+ // An agent or a background process opens full screen, outside the modal.
279
+ const target = node.kind === 'agent' ? { kind: 'agent' as const, id: node.id }
280
+ : node.kind === 'background-process' ? { kind: 'process' as const, id: node.id } : null;
281
+ if (target && this.deps.openSessionView?.(target)) {
282
+ host.close(this, 'done');
283
+ return;
284
+ }
285
+ if (!isAttachableFleetKind(node.kind)) {
286
+ this.say(`${node.kind} has no transcript to open; its details are on the right.`, 'faint');
287
+ return;
288
+ }
289
+ this.tabs = attachFleetTab(this.tabs, node);
290
+ }
291
+
292
+ private beginSteer(): void {
293
+ const entry = this.selectedEntry();
294
+ if (!entry) return;
295
+ if (entry.kind === 'hosted') {
296
+ if (!this.deps.steerHosted) { this.say('Steering the hosted session is not available here.', 'error'); return; }
297
+ this.steer = { id: HOSTED_ID, draft: '' };
298
+ return;
299
+ }
300
+ const node = entry.row.node;
301
+ if (isObservedExternalNode(node)) { this.deps.acts?.handleTreeKey('s', node); return; }
302
+ if (isTerminalProcessState(node.state)) { this.say(`${node.label} already finished: nothing to steer.`, 'faint'); return; }
303
+ if (!node.capabilities.steerable || !isAttachableFleetKind(node.kind)) { this.say(`${node.kind} does not take steering.`, 'faint'); return; }
304
+ this.steer = { id: node.id, draft: '' };
305
+ }
306
+
307
+ private submitSteer(): void {
308
+ const steer = this.steer;
309
+ if (!steer) return;
310
+ const text = steer.draft.trim();
311
+ if (!text) { this.steer = null; return; }
312
+ if (steer.id === HOSTED_ID) {
313
+ this.steer = null;
314
+ this.deps.steerHosted?.(text);
315
+ this.say('Sent to the hosted session.');
316
+ return;
317
+ }
318
+ const result = this.deps.actions.steer(steer.id, text);
319
+ if (!result.queued) {
320
+ // The draft stays so nothing typed is lost.
321
+ const siblings = liveSteerableLabels(this.deps.readModel.getSnapshot().rows.map((row) => row.node), steer.id);
322
+ this.say(steerRefusalMessage(result.reason, siblings), 'error');
323
+ return;
324
+ }
325
+ const node = this.findNode(steer.id);
326
+ if (node) {
327
+ const keep = this.tabs.activeTabIndex;
328
+ this.tabs = attachFleetTab(this.tabs, node);
329
+ if (this.level() !== 'full' || keep === 0) this.tabs = { tabs: this.tabs.tabs, activeTabIndex: keep };
330
+ const tab = this.tabs.tabs.find((t) => t.nodeId === steer.id);
331
+ if (tab) tab.steerBadge = { messageId: result.messageId, status: 'queued', queuedAt: Date.now() };
332
+ }
333
+ this.steer = null;
334
+ this.say(`Steer queued for ${node?.label ?? steer.id}: it arrives at its next turn (⧗ until then).`);
335
+ }
336
+
337
+ /** x / ctrl+x: stop the node and its descendants, after asking. */
338
+ private stopSelected(nodeId?: string): void {
339
+ const node = nodeId ? this.findNode(nodeId) : this.selectedNode();
340
+ if (!node || isTerminalProcessState(node.state)) { if (node) this.say(`${node.label} already finished.`, 'faint'); return; }
341
+ if (!node.capabilities.killable) { this.say(`${node.kind} cannot be stopped from here.`, 'faint'); return; }
342
+ const args = fleetKillConfirmArgs(node, this.snapshotRows(), {
343
+ kill: (id) => this.deps.actions.kill(id, { cascade: true }),
344
+ markStopping: (id) => this.stopTracker.mark(id),
345
+ });
346
+ if (!this.deps.confirm) { this.say('Stopping needs a confirm dialog, which is not available here.', 'error'); return; }
347
+ void this.deps.confirm({
348
+ title: `Stop ${node.label}?`,
349
+ body: `This stops ${args.label} and everything it started. It cannot be resumed.\nPress ctrl+x again to stop it; Esc keeps it running.`,
350
+ confirmLabel: 'Stop',
351
+ tone: 'danger',
352
+ confirmChord: { key: 'x', ctrl: true },
353
+ }).then((ok) => {
354
+ if (ok) { args.onConfirm(); this.say(`Stopping ${node.label}…`, 'warning'); }
355
+ this.deps.requestRender();
356
+ });
357
+ }
358
+
359
+ private interruptSelected(): void {
360
+ const node = this.selectedNode();
361
+ if (!node || isTerminalProcessState(node.state)) return;
362
+ if (!node.capabilities.interruptible) { this.say(`${node.kind} does not support interrupt.`, 'faint'); return; }
363
+ this.deps.actions.interrupt(node.id);
364
+ this.stopTracker.mark(node.id);
365
+ this.say(`Interrupting ${node.label}…`, 'warning');
366
+ }
367
+
368
+ private jumpToBlocked(): void {
369
+ const blocked = this.deps.readModel.getSnapshot().blockedNodeIds;
370
+ if (blocked.length === 0) { this.say('Nothing is waiting on you right now.', 'faint'); return; }
371
+ const at = this.selectedId ? blocked.indexOf(this.selectedId) : -1;
372
+ this.view = 'active';
373
+ this.query = '';
374
+ this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 0 };
375
+ this.hostedFull = false;
376
+ this.selectedId = blocked[(at + 1) % blocked.length]!;
377
+ this.status = null;
378
+ }
379
+
380
+ private archiveSelected(all: boolean): void {
381
+ if (all) {
382
+ if (this.view !== 'active') return;
383
+ const count = this.deps.readModel.archiveFinished();
384
+ this.say(count > 0 ? `Archived ${count} finished process${count === 1 ? '' : 'es'}.` : 'Nothing has fully finished to archive.', 'faint');
385
+ return;
386
+ }
387
+ const node = this.selectedNode();
388
+ if (!node) return;
389
+ if (this.view === 'archived') {
390
+ const restored = this.deps.readModel.unarchive(node.id);
391
+ this.say(restored > 0 ? `Restored ${node.label}.` : 'Nothing restored for this one.', 'faint');
392
+ return;
393
+ }
394
+ if (!isTerminalProcessState(node.state)) { this.say('Only finished processes can be archived.', 'faint'); return; }
395
+ const result = this.deps.readModel.archive(node.id);
396
+ this.say(result.archived ? `Archived ${node.label}.` : result.reason ?? 'Could not archive this one.', 'faint');
397
+ }
398
+
399
+ // ── Keys ───────────────────────────────────────────────────────────────────
400
+
401
+ escape(): boolean {
402
+ if (this.steer) { this.steer = null; return true; }
403
+ if (this.deps.acts?.observedSteerActive()) { this.deps.acts.handleObservedSteerInput('escape'); return true; }
404
+ if (this.deps.acts?.pickModeActive()) { this.deps.acts.handlePickInput('escape'); return true; }
405
+ if (this.deps.spawn?.spawnModeActive()) { this.deps.spawn.handleSpawnInput('escape'); return true; }
406
+ if (this.filtering) { this.filtering = false; return true; }
407
+ if (this.hostedFull) { this.hostedFull = false; return true; }
408
+ if (this.tabs.activeTabIndex > 0) { this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 0 }; return true; }
409
+ if (this.query) { this.query = ''; return true; }
410
+ return false;
411
+ }
412
+
413
+ handleToken(token: InputToken, host: SurfaceModalHost): void {
414
+ if (this.steer) { this.handleSteerToken(token); return; }
415
+ // F2 and ctrl+o open this modal; pressed again here, they close it.
416
+ if (token.type === 'key' && ((token.logicalName === 'f2' && !token.ctrl) || (token.ctrl && token.logicalName === 'o'))) {
417
+ host.close(this, 'done');
418
+ return;
419
+ }
420
+ const key = keyName(token);
421
+ if (this.deps.acts?.observedSteerActive()) {
422
+ if (token.type === 'text') for (const ch of token.value) this.deps.acts.handleObservedSteerInput(ch);
423
+ else if (key) this.deps.acts.handleObservedSteerInput(key);
424
+ return;
425
+ }
426
+ if (this.deps.acts?.pickModeActive()) { if (key) this.deps.acts.handlePickInput(key); return; }
427
+ if (this.deps.spawn?.spawnModeActive()) { if (key) this.deps.spawn.handleSpawnInput(key); return; }
428
+ if (this.filtering) { this.handleFilterToken(token); return; }
429
+ if (this.level() === 'full') { this.handleFullToken(token); return; }
430
+ if (token.type === 'key') {
431
+ const name = token.logicalName ?? '';
432
+ if (token.ctrl && name === 'x') { this.stopSelected(); return; }
433
+ if (name === 'up') this.move(-1);
434
+ else if (name === 'down') this.move(1);
435
+ else if (name === 'pageup') this.move(-10);
436
+ else if (name === 'pagedown') this.move(10);
437
+ else if (name === 'enter') this.openFull(host);
438
+ else if (isTextBackspace(name) && this.query) this.query = this.query.slice(0, -1);
439
+ return;
440
+ }
441
+ if (token.type !== 'text' || token.value.length !== 1) return;
442
+ this.status = null;
443
+ const node = this.selectedNode();
444
+ switch (token.value) {
445
+ case '/': this.filtering = true; break;
446
+ case 'j': this.move(1); break;
447
+ case 'k': this.move(-1); break;
448
+ case 's': this.beginSteer(); break;
449
+ case 'x': case 'K': this.stopSelected(); break;
450
+ case 'i': this.interruptSelected(); break;
451
+ case 'p':
452
+ if (node) toggleFleetPause(node, { interrupt: (id) => this.deps.actions.interrupt(id), resume: (id) => this.deps.actions.resume(id), setError: (m) => this.say(m, 'faint'), markDirty: () => {}, tracker: this.stopTracker });
453
+ break;
454
+ case 'n':
455
+ if (this.deps.spawn) void this.deps.spawn.begin().then(() => this.deps.requestRender());
456
+ else this.say('Hosting a third-party agent needs the daemon, which is not connected.', 'faint');
457
+ break;
458
+ case 'D':
459
+ if (node && !this.deps.acts?.handleTreeKey('D', node)) this.say('This row does not own a worktree.', 'faint');
460
+ break;
461
+ case 'b': this.jumpToBlocked(); break;
462
+ case 'f': this.follow = !this.follow; this.applyFollow(); this.say(this.follow ? 'Following the newest running agent.' : 'Stopped following.', 'faint'); break;
463
+ case 'v': this.view = this.view === 'active' ? 'archived' : 'active'; this.selectedId = null; break;
464
+ case 'a': this.archiveSelected(false); break;
465
+ case 'A': this.archiveSelected(true); break;
466
+ case ']':
467
+ if (this.tabs.tabs.length > 0) this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 1 };
468
+ break;
469
+ case '[':
470
+ if (this.tabs.tabs.length > 0) this.tabs = { tabs: this.tabs.tabs, activeTabIndex: this.tabs.tabs.length };
471
+ break;
472
+ default: break;
473
+ }
474
+ }
475
+
476
+ private handleFilterToken(token: InputToken): void {
477
+ if (token.type === 'text') {
478
+ this.query += [...token.value].filter((ch) => ch >= ' ').join('');
479
+ this.selectedId = null;
480
+ return;
481
+ }
482
+ if (token.type !== 'key') return;
483
+ const name = token.logicalName ?? '';
484
+ if (isTextBackspace(name)) { this.query = this.query.slice(0, -1); return; }
485
+ if (name === 'enter') { this.filtering = false; return; }
486
+ if (name === 'up' || name === 'down') { this.filtering = false; this.move(name === 'up' ? -1 : 1); }
487
+ }
488
+
489
+ private handleFullToken(token: InputToken): void {
490
+ if (token.type === 'key') {
491
+ const name = token.logicalName ?? '';
492
+ if (token.ctrl && name === 'x') {
493
+ const tab = activeFleetTab(this.tabs);
494
+ if (tab) this.stopSelected(tab.nodeId);
495
+ else this.say('The hosted session ends with /hosted kill; detaching keeps or ends it by its policy.', 'faint');
496
+ }
497
+ return;
498
+ }
499
+ if (token.type !== 'text' || token.value.length !== 1) return;
500
+ if (token.value === 's') {
501
+ const tab = activeFleetTab(this.tabs);
502
+ if (tab) this.selectedId = tab.nodeId;
503
+ this.beginSteer();
504
+ } else if ((token.value === ']' || token.value === '[') && !this.hostedFull) {
505
+ const next = stepFleetTab(this.tabs, token.value === ']' ? 1 : -1);
506
+ if (next.activeTabIndex > 0) this.tabs = next;
507
+ }
508
+ }
509
+
510
+ private handleSteerToken(token: InputToken): void {
511
+ const steer = this.steer!;
512
+ if (token.type === 'text') {
513
+ steer.draft += token.value.replace(/[\r\n]+/g, ' ').replace(/[\x00-\x1f\x7f]/g, '');
514
+ return;
515
+ }
516
+ if (token.type !== 'key') return;
517
+ const name = token.logicalName ?? '';
518
+ if (name === 'enter') this.submitSteer();
519
+ else if (isTextBackspace(name)) steer.draft = steer.draft.slice(0, -1);
520
+ }
521
+
522
+ // ── View ───────────────────────────────────────────────────────────────────
523
+
524
+ private badgeFor(nodeId: string): FleetTab['steerBadge'] {
525
+ return this.tabs.tabs.find((tab) => tab.nodeId === nodeId)?.steerBadge ?? null;
526
+ }
527
+
528
+ private nodeRow(row: FleetTreeRow, selected: boolean): KitRow {
529
+ const t = activeTokens();
530
+ const node = row.node;
531
+ const stopping = this.stopTracker.isStopping(node.id);
532
+ const blocked = !stopping && isBlockedOnUserNode(node);
533
+ const disp = fleetStateDisplay(node.state, stopping, blocked);
534
+ const running = isRunningProcessState(node.state) || node.state === 'awaiting-approval';
535
+ const mark = stopping || blocked ? disp.glyph : running ? '◐' : node.state === 'done' ? '✓' : node.state === 'failed' || node.state === 'killed' ? '✕' : '○';
536
+ const markFg = stopping || blocked ? t.warning : running ? t.brand : node.state === 'done' ? t.success : node.state === 'failed' || node.state === 'killed' ? t.error : t.textFaint;
537
+ const badge = this.badgeFor(node.id);
538
+ const attention = fleetNodeAttention(node);
539
+ const stall = fleetStallMarker(node);
540
+ const activity = stopping ? 'stopping…'
541
+ : blocked ? fleetAttentionText(attention ?? { reason: 'approval' })
542
+ : [node.currentActivity?.toolName ? `${node.currentActivity.toolName}: ${node.currentActivity.text}` : node.headline?.text ?? node.currentActivity?.text ?? node.task ?? '', stall ?? ''].filter(Boolean).join(' · ');
543
+ const attempt = node.attemptGroup ? ` attempt ${node.attemptGroup.index + 1}/${node.attemptGroup.total}${node.attemptGroup.held ? ' held' : ''}` : '';
544
+ const kindTag = node.kind === 'agent' ? '' : node.kind === 'acp-agent' ? 'hosted' : node.kind.replace(/-/g, ' ');
545
+ const desc = [kindTag, `${activity}${attempt}`.trim(), badge ? steerBadgeGlyph(badge.status) : ''].filter(Boolean).join(' · ');
546
+ const cost = hasFleetCost(node.costUsd, node.costState) ? formatFleetCost(node.costUsd, node.costState) : '';
547
+ const prefix = row.depth > 0 ? `${'│ '.repeat(row.depth - 1)}└ ` : '';
548
+ return { label: `${prefix}${node.label}`, desc: desc || undefined, right: [formatElapsed(node.elapsedMs), cost].filter(Boolean).join(' · '), mark, markFg, selected };
549
+ }
550
+
551
+ private listRows(): KitRow[] {
552
+ const t = activeTokens();
553
+ const selected = this.selectedEntry();
554
+ const rows: KitRow[] = [];
555
+ for (const group of this.groups()) {
556
+ rows.push({ header: group.name, headerRight: group.name === 'Hosted' ? 'on the daemon' : undefined });
557
+ for (const entry of group.entries) {
558
+ const isSelected = selected !== null && this.entryId(entry) === this.entryId(selected);
559
+ if (entry.kind === 'hosted') {
560
+ const state = this.deps.hosted!.getState();
561
+ const record = state.record!;
562
+ rows.push({ label: record.title.trim() || record.id, desc: `${state.streaming ? 'live' : 'not streaming'} · ${record.status}`, right: `${record.turnCount} turns`, mark: '◈', markFg: t.brandEnd, selected: isSelected });
563
+ } else {
564
+ rows.push(this.nodeRow(entry.row, isSelected));
565
+ }
566
+ }
567
+ }
568
+ return rows;
569
+ }
570
+
571
+ private detail(): AgentsDetail | null {
572
+ const entry = this.selectedEntry();
573
+ if (!entry) return null;
574
+ if (entry.kind === 'hosted') {
575
+ const state = this.deps.hosted!.getState();
576
+ return { title: state.record!.title.trim() || 'Hosted session', meta: 'a conversation running inside the daemon', blocks: [{ kind: 'text', lines: hostedHeaderTexts(state) }], tail: hostedRowTexts(state.rows.slice(-40)) };
577
+ }
578
+ const node = entry.row.node;
579
+ const stopping = this.stopTracker.isStopping(node.id);
580
+ const blocked = !stopping && isBlockedOnUserNode(node);
581
+ this.deps.acts?.ensureGraphFor(node);
582
+ const observedDraft = this.deps.acts?.observedSteerDraftFor(node.id) ?? null;
583
+ const graph = this.deps.acts?.graphFor(node.id) ?? null;
584
+ const meta = [node.kind === 'acp-agent' ? 'hosted over ACP' : node.kind, node.model, node.provider].filter(Boolean).join(' · ');
585
+ const tail = node.kind === 'agent' || node.kind === 'acp-agent' ? transcriptTail(this.deps.actions.getConversationSnapshot(node.id), 30) : [];
586
+ return { title: node.label, meta, blocks: [{ kind: 'lines', build: (width) => renderFleetDetailLines(node, width, stopping, blocked, undefined, observedDraft, graph) }], tail };
587
+ }
588
+
589
+ private fullBody(width: number, height: number): readonly Line[] {
590
+ if (this.hostedFull) return [];
591
+ const tab = activeFleetTab(this.tabs);
592
+ if (!tab) return [];
593
+ const live = this.findNode(tab.nodeId);
594
+ if (tab.kind === 'wrfc-chain') {
595
+ const members = this.deps.readModel.getSnapshot().rows.filter((row) => row.node.parentId === tab.nodeId);
596
+ return renderFleetChainSummary(members, width, live === null || isTerminalProcessState(live.state));
597
+ }
598
+ const isTerminal = live ? isTerminalProcessState(live.state) : true;
599
+ const snapshot = this.deps.actions.getConversationSnapshot(tab.nodeId);
600
+ const result = renderFleetAgentTranscript(snapshot, isTerminal, tab.lineCache, width, height, this.deps.configManager ?? null);
601
+ if (result.mode !== 'unavailable') return result.lines;
602
+ if (!tab.ledgerLoadStarted) {
603
+ tab.ledgerLoadStarted = true;
604
+ readFile(this.deps.actions.resolveSessionLogPath(tab.nodeId), 'utf-8')
605
+ .then((raw) => { tab.ledgerEntries = parseAgentLedger(raw); })
606
+ .catch(() => { tab.ledgerEntries = []; })
607
+ .finally(() => this.deps.requestRender());
608
+ }
609
+ return tab.ledgerEntries !== null ? renderFleetLedgerFallback(tab.ledgerEntries, width, height) : renderFleetTranscriptLoading(width);
610
+ }
611
+
612
+ private sub(): string {
613
+ const snapshot = this.deps.readModel.getSnapshot();
614
+ const failed = snapshot.rows.filter((row) => row.node.state === 'failed').length;
615
+ const blocked = snapshot.blockedNodeIds.length;
616
+ const parts = [`${snapshot.runningCount} running`];
617
+ if (failed > 0) parts.push(`${failed} failed`);
618
+ if (blocked > 0) parts.push(`${blocked} waiting on you`);
619
+ const you = this.deps.sessionCost?.();
620
+ if (you !== undefined) parts.push(`you ${you === null ? 'unpriced' : formatUsd(you)}`);
621
+ parts.push(`fleet ${snapshot.totalCost === null ? '$0.00' : formatUsd(snapshot.totalCost)}`);
622
+ return parts.join(' · ');
623
+ }
624
+
625
+ private hints(level: 'list' | 'full' | 'picker'): KitHint[] {
626
+ if (this.steer) return [['⏎', 'send'], ['esc', 'cancel']];
627
+ if (this.deps.acts?.observedSteerActive()) return [['⏎', 'send to the session'], ['esc', 'cancel']];
628
+ if (level === 'picker') return [['↑↓', 'choose'], ['⏎', this.deps.acts?.pickModeActive() ? 'pick (asks first)' : 'next'], ['esc', 'back']];
629
+ if (this.filtering) return [['⏎', 'done'], ['esc', 'stop filtering']];
630
+ if (level === 'full') {
631
+ if (this.hostedFull) return [['s', 'say'], ['esc', 'back']];
632
+ return [['s', 'steer'], ['ctrl+x', 'stop (asks first)'], ['[ ]', 'other agents'], ['esc', 'back']];
633
+ }
634
+ const node = this.selectedNode();
635
+ const attention = node ? fleetNodeAttention(node) : null;
636
+ const enter = attention?.reason === 'pick' ? 'pick a winner' : attention?.reason === 'conflict' ? 'resolve conflict' : 'open full screen';
637
+ const hints: KitHint[] = [['⏎', enter], ['s', 'steer'], ['ctrl+x', 'stop'], ['n', 'new agent']];
638
+ if (node && !isTerminalProcessState(node.state) && node.capabilities.interruptible) hints.push(['i', 'interrupt']);
639
+ if (node && (node.state === 'paused' ? node.capabilities.resumable : node.capabilities.pausable && !isTerminalProcessState(node.state))) hints.push(['p', node.state === 'paused' ? 'resume' : 'pause']);
640
+ const blocked = this.deps.readModel.getSnapshot().blockedNodeIds.length;
641
+ if (blocked > 0) hints.push(['b', `waiting on you (${blocked})`]);
642
+ hints.push(['v', this.view === 'archived' ? 'live' : 'archive'], ['a', this.view === 'archived' ? 'restore' : 'archive one'], ['f', this.follow ? 'following' : 'follow']);
643
+ return hints;
644
+ }
645
+
646
+ private buildView(): AgentsModalView {
647
+ const level = this.level();
648
+ const tab = activeFleetTab(this.tabs);
649
+ const input = this.steer
650
+ ? { label: this.steer.id === HOSTED_ID ? 'say: ' : `steer ${this.findNode(this.steer.id)?.label ?? ''}: `, draft: this.steer.draft }
651
+ : this.deps.acts?.observedSteerActive() && this.selectedNode()
652
+ ? { label: 'steer the foreign session: ', draft: this.deps.acts.observedSteerDraftFor(this.selectedNode()!.id) ?? '' }
653
+ : null;
654
+ const pick = this.deps.acts?.pickView() ?? null;
655
+ const spawn = this.deps.spawn?.spawnView() ?? null;
656
+ const shown = this.entries().length;
657
+ const total = this.snapshotRows().length + (this.hostedAttached() ? 1 : 0);
658
+ const hostedState = this.deps.hosted?.getState();
659
+ return {
660
+ level,
661
+ crumbs: level === 'full' ? [this.hostedFull ? (hostedState?.record?.title || 'hosted session') : (this.findNode(tab!.nodeId)?.label ?? tab!.label)]
662
+ : level === 'picker' ? [pick ? 'pick a winner' : spawn?.title ?? 'host an agent']
663
+ : this.view === 'archived' ? ['archive'] : [],
664
+ sub: level === 'list' ? this.sub() : '',
665
+ hints: this.hints(level),
666
+ status: this.status,
667
+ input,
668
+ filter: { query: this.query, active: this.filtering, count: this.query ? `${shown} of ${total}` : `${total} shown` },
669
+ rows: level === 'list' ? this.listRows() : [],
670
+ detail: level === 'list' ? this.detail() : null,
671
+ full: level === 'full' ? {
672
+ meta: this.hostedFull ? 'hosted on the daemon' : [tab!.kind, this.findNode(tab!.nodeId)?.state ?? 'finished'].join(' · '),
673
+ header: this.hostedFull && hostedState ? hostedHeaderTexts(hostedState) : [],
674
+ body: this.hostedFull && hostedState ? (width, height) => hostedBody(hostedState.rows, width, height) : (width, height) => this.fullBody(width, height),
675
+ } : null,
676
+ picker: level === 'picker'
677
+ ? pick
678
+ ? { intro: `Best of N: ${pick.title}. Choose the winner; Enter shows its diff and asks before merging.${pick.proposal ? ` The judge proposes ${pick.proposal} (advisory).` : ''}`, options: pick.candidates, side: pick.diff ? diffSide(pick.diff) : [{ text: 'This candidate has no diff to preview.', tone: 'faint' }] }
679
+ : { intro: spawn?.creating ? 'Hosting the agent…' : spawn?.intro ?? '', options: spawn?.options ?? [], side: [] }
680
+ : null,
681
+ scrollOwner: this,
682
+ };
683
+ }
684
+
685
+ render(screenWidth: number, screenHeight: number): SurfaceLayer {
686
+ return renderAgentsModal(this.buildView(), screenWidth, screenHeight);
687
+ }
688
+ }
689
+
690
+ /** The files a candidate diff touches, for the picker's side column. */
691
+ function diffSide(diff: string): AgentsText[] {
692
+ const files = diff.split('\n').filter((line) => line.startsWith('diff --git ')).map((line) => line.replace(/^diff --git a\/.+? b\//, ''));
693
+ const added = diff.split('\n').filter((line) => line.startsWith('+') && !line.startsWith('+++')).length;
694
+ const removed = diff.split('\n').filter((line) => line.startsWith('-') && !line.startsWith('---')).length;
695
+ return [
696
+ { text: `${files.length} file${files.length === 1 ? '' : 's'} · +${added} −${removed}`, tone: 'text', bold: true },
697
+ ...files.map((file) => ({ text: file, tone: 'muted' as const })),
698
+ ];
699
+ }