@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
@@ -1,799 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // fleet-panel.ts
3
- //
4
- //, FleetPanel: the live unified observability tree. Renders the
5
- // registered process fleet (agents incl. WRFC roles, WRFC chains/subtasks,
6
- // workflow-tool FSMs, watchers, background processes) as a depth-first tree
7
- // with a selected-row detail region. Session tabs (Part C): Enter on an
8
- // attachable node (agent/wrfc-chain) opens a tab with that process's
9
- // transcript/member summary, see fleet-tabs.ts + fleet-transcript.ts. This
10
- // file owns input dispatch and layout; fleet-deep-link.ts owns the
11
- // cross-panel jump target (item 4).
12
- // ---------------------------------------------------------------------------
13
-
14
- import { readFile } from 'node:fs/promises';
15
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
16
- import type { SteerResult } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
17
- import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
18
- import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
19
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
20
- import { PanelConfirmOverlay } from './panel-confirm-overlay.ts';
21
- import { isPanelSearchBackspace, isPanelSearchCancel, isPanelSearchCommit, isPanelSearchPrintable } from './search-focus.ts';
22
- import { buildKeyboardHints, buildPanelWorkspace, buildSearchInputLine, DEFAULT_PANEL_PALETTE, getPanelWorkspaceContentBudget, type PanelPalette } from './polish.ts';
23
- import {
24
- isBlockedOnUserNode,
25
- isRunningProcessState,
26
- isTerminalProcessState,
27
- type FleetReadModel,
28
- type FleetTreeRow,
29
- } from './fleet-read-model.ts';
30
- import type { FleetActs } from './fleet-acts.ts';
31
- import type { FleetSpawn } from './fleet-spawn.ts';
32
- import {
33
- activeFleetTab,
34
- appendSteerText,
35
- attachFleetTab,
36
- detachActiveFleetTab,
37
- EMPTY_FLEET_TABS_STATE,
38
- isAttachableFleetKind,
39
- stepFleetTab,
40
- type FleetTab,
41
- type FleetTabsState,
42
- type SteerBadge,
43
- } from './fleet-tabs.ts';
44
- import { liveSteerableLabels, reconcileSteerBadges as reconcileSteerBadgesPure, renderSteerBadgeLine, steerRefusalMessage } from './fleet-steer.ts';
45
- import { FleetStopTracker, toggleFleetPause, buildFleetTreeHints, fleetKillConfirmArgs } from './fleet-stop.ts';
46
- import { resolveFleetDeepLinkIndex } from './fleet-deep-link.ts';
47
- import { parseAgentLedger, renderFleetAgentTranscript, renderFleetChainSummary, renderFleetLedgerFallback, renderFleetTranscriptLoading } from './fleet-transcript.ts';
48
- import { renderFleetTabStrip } from '../renderer/fleet-tab-strip.ts';
49
- import { renderFleetDetailLines, renderFleetRowLine } from './fleet-panel-format.ts';
50
-
51
- const C = DEFAULT_PANEL_PALETTE;
52
-
53
- export interface FleetActionCallbacks {
54
- /** Graceful interruption (AgentManager.cancel / trigger-schedule disable, via the registry). */
55
- readonly interrupt: (id: string) => boolean;
56
- /** Re-arm a `paused` node (disabled trigger/schedule or automation job), interrupt()'s inverse. Honest false when not paused / non-resumable. */
57
- readonly resume: (id: string) => boolean;
58
- /** Hard stop, optionally cascading to killable descendants. Returns the node ids acted on. */
59
- readonly kill: (id: string, opts: { readonly cascade: boolean }) => readonly string[];
60
- /**
61
- * Fetch an agent's conversation history, live (running) or
62
- * frozen (just-completed, still in the SDK's retention ring). Empty array
63
- * means "unavailable" (evicted, or never registered), FleetPanel degrades
64
- * to the on-disk ledger fallback in that case, never fabricating content.
65
- */
66
- readonly getConversationSnapshot: (agentId: string) => readonly ConversationMessageSnapshot[];
67
- /** Resolve the on-disk `<agentId>.jsonl` event-ledger path for the degraded fallback view. */
68
- readonly resolveSessionLogPath: (agentId: string) => string;
69
- /**
70
- * Queue a human message for a live in-process agent (or a
71
- * wrfc-subtask's current live member agent), delivered at the target's
72
- * next turn boundary, never mid-token. Honest refusal
73
- * (`{queued:false,reason}`) for anything that cannot take mid-run input
74
- * (terminal nodes, non-agent kinds, the wrfc-chain coordinator itself).
75
- */
76
- readonly steer: (id: string, text: string) => SteerResult;
77
- }
78
-
79
- const NOOP_ACTIONS: FleetActionCallbacks = {
80
- interrupt: (_id: string) => false,
81
- resume: (_id: string) => false,
82
- kill: (_id: string, _opts: { readonly cascade: boolean }) => [],
83
- getConversationSnapshot: (_agentId: string) => [],
84
- resolveSessionLogPath: (agentId: string) => agentId,
85
- steer: (_id: string, _text: string) => ({ queued: false, reason: 'no live registry' }),
86
- };
87
-
88
- export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
89
- private readonly unsub: () => void;
90
- private readonly unsubConsumed: () => void;
91
- private readonly actions: FleetActionCallbacks;
92
- private readonly confirmOverlay: PanelConfirmOverlay;
93
- private follow = false;
94
- /** Tree view mode: the live fleet, or the session archive of finished subtrees. */
95
- private viewMode: 'active' | 'archived' = 'active';
96
- private tabsState: FleetTabsState = EMPTY_FLEET_TABS_STATE;
97
- private tickTimer: ReturnType<typeof setInterval> | null = null;
98
- /**
99
- * The node id the cursor is anchored to, independent of its row index.
100
- * `selectedIndex` is a plain offset into `getItems()`'s current (rebuilt-every-
101
- * snapshot) order, so a stale index silently drifts; `reanchorSelection()`
102
- * re-locates this id each snapshot and repoints selectedIndex. null means "no
103
- * anchor yet" (base class index-0 default until first navigation/tick).
104
- */
105
- private selectedNodeId: string | null = null;
106
-
107
- /** d1: the "stopping…" write-window overlay tracker (see fleet-stop.ts). */
108
- private readonly stopTracker = new FleetStopTracker();
109
-
110
- public constructor(
111
- private readonly readModel: FleetReadModel,
112
- actions: Partial<FleetActionCallbacks> = {},
113
- private readonly configManager: ConfigManager | null = null,
114
- // The waiting-on-human acts (pick / conflict / discard). Null in a runtime
115
- // with no daemon gateway wired, those keys then fall through honestly.
116
- private readonly acts: FleetActs | null = null,
117
- // The ACP spawn affordance ('n' hosts a discovered third-party agent); null with no gateway wired.
118
- private readonly spawn: FleetSpawn | null = null,
119
- ) {
120
- super('fleet', 'Fleet', '⊟', 'runtime-ops');
121
- this.showSelectionGutter = true; // visible ▸ focus indicator
122
- this.actions = { ...NOOP_ACTIONS, ...actions };
123
- this.confirmOverlay = new PanelConfirmOverlay(() => this.markDirty());
124
- this.unsub = readModel.subscribe(() => {
125
- this.reanchorSelection();
126
- this.applyFollow();
127
- this.reconcileSteerBadges();
128
- this.markDirty();
129
- });
130
- // The honest "the agent actually consumed this steer at
131
- // its turn boundary" signal, see fleet-tabs.ts's SteerBadgeStatus doc.
132
- // A read-model without a runtimeBus dep never invokes this (graceful
133
- // no-op), same degrade as steer()/steerable without a messageBus dep.
134
- this.unsubConsumed = readModel.subscribeConsumed((event) => {
135
- let changed = false;
136
- for (const tab of this.tabsState.tabs) {
137
- const badge = tab.steerBadge;
138
- if (!badge || badge.messageId !== event.messageId) continue;
139
- // Consumed-wins: this real signal can still upgrade an already
140
- // 'dropped' (inferred) badge, as long as it hasn't linger-cleared yet.
141
- if (badge.status === 'queued' || badge.status === 'dropped') {
142
- tab.steerBadge = { messageId: badge.messageId, status: 'consumed', queuedAt: badge.queuedAt, resolvedAt: Date.now() };
143
- changed = true;
144
- }
145
- }
146
- if (changed) this.markDirty();
147
- });
148
- }
149
-
150
- public override onActivate(): void {
151
- super.onActivate();
152
- // Time-derived fields (elapsed, follow target) stay live even when no
153
- // registry tick fires while this panel is on screen (cockpit-panel.ts
154
- // STALL_TICK_MS precedent). Also re-anchors defensively in case the
155
- // underlying list changed without a subscribe notification.
156
- if (this.tickTimer === null) {
157
- this.tickTimer = this.registerTimer(setInterval(() => {
158
- this.reanchorSelection();
159
- this.applyFollow();
160
- this.reconcileSteerBadges();
161
- this.markDirty();
162
- }, 1000));
163
- }
164
- }
165
-
166
- public override onDeactivate(): void {
167
- super.onDeactivate();
168
- if (this.tickTimer !== null) {
169
- this.clearTimer(this.tickTimer);
170
- this.tickTimer = null;
171
- }
172
- }
173
-
174
- public override onDestroy(): void {
175
- super.onDestroy();
176
- this.unsub();
177
- this.unsubConsumed();
178
- for (const tab of this.tabsState.tabs) tab.lineCache.clear();
179
- }
180
-
181
- /** True while a session tab (not the root tree) is focused. Replaces the old isDetailFocused() seam. */
182
- public isTabActive(): boolean {
183
- return this.tabsState.activeTabIndex > 0;
184
- }
185
-
186
- /**
187
- * True while the steer composer on the active tab is open,
188
- * every character of a burst (paste, or several fast-typed chars in one
189
- * stdin chunk) must land in the draft one at a time rather than being
190
- * routed away as panel hotkeys or exploded to the main composer. See
191
- * Panel.isCapturingTextBurst's doc comment and git-panel.ts's
192
- * `commitMessage` precedent.
193
- */
194
- public override isCapturingTextBurst(): boolean {
195
- const tab = activeFleetTab(this.tabsState);
196
- return tab !== null && tab.steerDraft !== null;
197
- }
198
-
199
- /** Introspection for tests: the current tab-bar state (read-only). */
200
- public getTabsState(): FleetTabsState {
201
- return this.tabsState;
202
- }
203
-
204
- public isFollowing(): boolean {
205
- return this.follow;
206
- }
207
-
208
- /**
209
- * Ctrl+X (panel-close) collision resolution: the global
210
- * shortcut route (handler-shortcuts.ts) calls this BEFORE closing the
211
- * panel. Detaching the active tab consumes the key; on the root tree tab
212
- * there is nothing to detach, so this returns false and Ctrl+X falls
213
- * through to its ordinary panel-close behavior.
214
- */
215
- public interceptPanelClose(): boolean {
216
- if (this.tabsState.activeTabIndex === 0) return false;
217
- this.tabsState = detachActiveFleetTab(this.tabsState);
218
- this.markDirty();
219
- return true;
220
- }
221
-
222
- protected getItems(): readonly FleetTreeRow[] {
223
- return this.viewMode === 'archived'
224
- ? this.readModel.getArchivedSnapshot().rows
225
- : this.readModel.getSnapshot().rows;
226
- }
227
-
228
- protected override getPalette(): PanelPalette {
229
- return C;
230
- }
231
-
232
- protected override getEmptyStateMessage(): string {
233
- return this.viewMode === 'archived'
234
- ? ' Archive is empty: press a on a finished process (or A for all) to move it here.'
235
- : ' No processes tracked yet.';
236
- }
237
-
238
- /**
239
- * (cross-restart honesty): a restart starts from an empty `AgentManager` Map
240
- * (no bridge from a prior registry), so a completed process from a previous
241
- * session can never reappear here, a real, permanent limitation, documented
242
- * rather than implying "nothing has ever run" or that a restart brings it back.
243
- */
244
- protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
245
- return [
246
- {
247
- command: 'previous sessions',
248
- summary: "not shown here: this list resets on TUI restart; each agent's .goodvibes/tui/sessions/<id>.jsonl event log still persists on disk",
249
- },
250
- ];
251
- }
252
-
253
- private applyFollow(): void {
254
- if (!this.follow) return;
255
- const rows = this.getItems();
256
- if (rows.length === 0) return;
257
- let bestIdx = -1;
258
- let bestStarted = -Infinity;
259
- rows.forEach((row, idx) => {
260
- if (!isRunningProcessState(row.node.state)) return;
261
- const started = row.node.startedAt ?? -Infinity;
262
- if (started >= bestStarted) {
263
- bestStarted = started;
264
- bestIdx = idx;
265
- }
266
- });
267
- if (bestIdx >= 0) {
268
- this.selectedIndex = bestIdx;
269
- this.selectedNodeId = rows[bestIdx]!.node.id;
270
- this.needsRender = true;
271
- }
272
- }
273
-
274
- /** Record the current selection's node id as the anchor for future snapshot updates. */
275
- private captureSelectionAnchor(): void {
276
- const item = this.getSelectedItem();
277
- this.selectedNodeId = item ? item.node.id : null;
278
- }
279
-
280
- /**
281
- * Re-locate `selectedNodeId` in the current snapshot and repoint
282
- * `selectedIndex` at it, so the selection follows a specific process across
283
- * adds/removes/reorders instead of drifting onto whatever row occupies the old
284
- * index. When the anchored node is gone (pruned), or no anchor exists yet,
285
- * falls back to clampSelection() (nearest valid index) and re-anchors there.
286
- */
287
- private reanchorSelection(): void {
288
- const rows = this.getItems();
289
- if (this.selectedNodeId !== null) {
290
- const idx = rows.findIndex((row) => row.node.id === this.selectedNodeId);
291
- if (idx >= 0) {
292
- this.selectedIndex = idx;
293
- return;
294
- }
295
- }
296
- this.clampSelection();
297
- this.captureSelectionAnchor();
298
- }
299
-
300
- /** Find a node's live ProcessNode in the current snapshot, or null (pruned/never registered). */
301
- private findLiveNode(nodeId: string): FleetTreeRow['node'] | null {
302
- return this.getItems().find((row) => row.node.id === nodeId)?.node ?? null;
303
- }
304
-
305
- /** item 4 (see fleet-deep-link.ts). */
306
- public receiveDeepLink(target: { readonly id: string; readonly kind?: string }): void {
307
- const idx = resolveFleetDeepLinkIndex(this.getItems(), target);
308
- if (idx < 0) { this.setError('node no longer present.'); return; }
309
- // A successful reveal must be VISIBLE: with a session tab active, render()
310
- // routes to the tab view and the selection change would be silently
311
- // swallowed (batch refutation finding 1), return to the tree first
312
- // (tabs kept; only the active view switches).
313
- this.tabsState = { tabs: this.tabsState.tabs, activeTabIndex: 0 };
314
- this.clearError(); this.selectedIndex = idx; this.selectedNodeId = target.id; this.markDirty();
315
- }
316
-
317
- /**
318
- * Focus the next node blocked on the operator (awaiting approval/input),
319
- * cycling in display order and wrapping. Resolves against the LIVE fleet
320
- * (blocked nodes never live in the archive) and returns to the tree view from
321
- * any attached tab so the reveal is visible (receiveDeepLink's contract).
322
- * Honest no-op message when nothing waits; always consumes 'b' (returns true).
323
- */
324
- private jumpToNextBlocked(): boolean {
325
- const snapshot = this.readModel.getSnapshot();
326
- const blockedIds = snapshot.blockedNodeIds;
327
- if (blockedIds.length === 0) {
328
- this.setError('Nothing is blocked on you right now.');
329
- this.markDirty();
330
- return true;
331
- }
332
- // Pick the blocked id strictly after the current anchor (wrapping), so
333
- // repeated presses cycle through every waiting node. Anchor on whatever row
334
- // is actually selected right now (the base class defaults the cursor to
335
- // row 0 before any explicit navigation sets selectedNodeId), so the first
336
- // press already advances past a blocked node the cursor happens to sit on.
337
- const anchorId = this.getSelectedItem()?.node.id ?? this.selectedNodeId;
338
- const currentPos = anchorId === null || anchorId === undefined ? -1 : blockedIds.indexOf(anchorId);
339
- const nextId = blockedIds[(currentPos + 1) % blockedIds.length]!;
340
- // Reveal in the live tree: leave the archive view and any active session tab.
341
- this.viewMode = 'active';
342
- this.tabsState = { tabs: this.tabsState.tabs, activeTabIndex: 0 };
343
- const idx = snapshot.rows.findIndex((row) => row.node.id === nextId);
344
- if (idx >= 0) {
345
- this.selectedIndex = idx;
346
- this.selectedNodeId = nextId;
347
- }
348
- this.clearError();
349
- this.markDirty();
350
- return true;
351
- }
352
-
353
- /**
354
- * The "dropped inference" reconciliation: called on every read-model
355
- * snapshot update and on the panel's 1s tick. See fleet-steer.ts's
356
- * reconcileSteerBadges doc for why this is needed (the SDK emits no
357
- * cancelled/expired signal for a queued steer).
358
- */
359
- private reconcileSteerBadges(): void {
360
- if (this.tabsState.tabs.length === 0) return;
361
- const changed = reconcileSteerBadgesPure(this.tabsState.tabs, (id) => this.findLiveNode(id), Date.now());
362
- if (changed) this.markDirty();
363
- }
364
-
365
- /** Kick off (once) the async ledger-fallback load for a tab whose conversation snapshot is unavailable. */
366
- private ensureLedgerLoaded(tab: FleetTab): void {
367
- if (tab.ledgerLoadStarted) return;
368
- tab.ledgerLoadStarted = true;
369
- const path = this.actions.resolveSessionLogPath(tab.agentId);
370
- readFile(path, 'utf-8')
371
- .then((raw) => {
372
- tab.ledgerEntries = parseAgentLedger(raw);
373
- this.markDirty();
374
- })
375
- .catch(() => {
376
- tab.ledgerEntries = [];
377
- this.markDirty();
378
- });
379
- }
380
-
381
- public override handleInput(key: string): boolean {
382
- if (this.lastError !== null) this.clearError();
383
-
384
- // The candidate picker (a flagged pick decision) owns the view + input while active.
385
- if (this.acts?.pickModeActive()) return this.acts.handlePickInput(key);
386
- // The observed-agent steer composer (a drill-in) likewise owns EVERY key until Enter/Esc.
387
- if (this.acts?.observedSteerActive()) return this.acts.handleObservedSteerInput(key);
388
- // The ACP spawn picker (host a third-party agent) likewise owns view + input.
389
- if (this.spawn?.spawnModeActive()) return this.spawn.handleSpawnInput(key);
390
-
391
- // Confirm overlay owns input first (K-armed kill confirm).
392
- if (this.confirmOverlay.handleInput(key)) return true;
393
-
394
- // Once the steer composer is open on the active tab, it
395
- // owns EVERY key (same full-priority-while-composing contract as
396
- // git-panel.ts's commitMessage entry) until Enter (submit) or Esc
397
- // (cancel), a burst/paste and single hotkeys like 'j'/'s'/'[' all land
398
- // in the draft, never as tree/tab navigation (focus rule).
399
- const composingTab = activeFleetTab(this.tabsState);
400
- if (composingTab && composingTab.steerDraft !== null) {
401
- return this.handleSteerInput(composingTab, key);
402
- }
403
-
404
- // Tab switching works from ANY view (root tree or an attached tab) once
405
- // at least one tab is open, [ / ] step through Tree, tab 1, tab 2, ...
406
- // Ctrl+X detach is handled by interceptPanelClose(), not handleInput (it
407
- // must win a global-shortcut race, see that method's doc comment).
408
- if ((key === '[' || key === ']') && this.tabsState.tabs.length > 0) {
409
- this.tabsState = stepFleetTab(this.tabsState, key === ']' ? 1 : -1);
410
- this.markDirty();
411
- return true;
412
- }
413
-
414
- // Jump-to-blocked works from ANYWHERE in the panel (root tree OR an attached
415
- // session tab): it steps to the next node waiting on the operator and
416
- // reveals it in the live tree so the selection is actually visible.
417
- if (key === 'b') {
418
- return this.jumpToNextBlocked();
419
- }
420
-
421
- if (this.tabsState.activeTabIndex > 0) {
422
- if (key === 's') {
423
- return this.tryOpenSteerDraft(activeFleetTab(this.tabsState)!);
424
- }
425
- // A session tab is focused, the tree isn't visible, so tree-only keys
426
- // (navigate/i/K/f/Enter) don't apply here.
427
- return false;
428
- }
429
-
430
- const selected = this.getSelectedItem();
431
-
432
- // Waiting-on-human acts on the selected row: Enter on a flagged pick/conflict
433
- // row, D on a worktree-owning row. Returns false (falls through to attach /
434
- // ordinary handling) for any other row, so Enter still opens an agent tab.
435
- if (this.acts && selected && this.acts.handleTreeKey(key, selected.node)) {
436
- this.captureSelectionAnchor();
437
- return true;
438
- }
439
-
440
- // Host a third-party ACP agent: lists discovered agents, then a known dir,
441
- // then spawns it as an acp-agent row. Quiet when none are discovered.
442
- if (key === 'n' && this.spawn) { void this.spawn.begin(); return true; }
443
-
444
- // Archive controls: v toggles the live fleet <-> session archive view;
445
- // a archives the selected finished subtree (or restores it from the
446
- // archive view); A archives every fully-finished root subtree.
447
- if (key === 'v') {
448
- this.viewMode = this.viewMode === 'active' ? 'archived' : 'active';
449
- this.markDirty();
450
- return true;
451
- }
452
- if (key === 'a') {
453
- if (!selected) return false;
454
- if (this.viewMode === 'archived') {
455
- const restored = this.readModel.unarchive(selected.node.id);
456
- if (restored === 0) this.setError('Nothing restored for this node.');
457
- this.markDirty();
458
- return true;
459
- }
460
- if (!isTerminalProcessState(selected.node.state)) {
461
- this.setError('Only finished processes can be archived.');
462
- return true;
463
- }
464
- const result = this.readModel.archive(selected.node.id);
465
- if (!result.archived) this.setError(result.reason ?? 'Could not archive this process.');
466
- this.markDirty();
467
- return true;
468
- }
469
- if (key === 'A' && this.viewMode === 'active') {
470
- const count = this.readModel.archiveFinished();
471
- if (count === 0) this.setError('No fully-finished processes to archive.');
472
- this.markDirty();
473
- return true;
474
- }
475
-
476
- if (key === 'i') {
477
- // Guard: only consume on a real, non-terminal node (cockpit-panel.ts
478
- // precedent) so the key falls through when there is nothing to act on.
479
- if (!selected || isTerminalProcessState(selected.node.state)) return false;
480
- // Not every kind can be interrupted (only 'agent' nodes ever are, see
481
- // the SDK's fleet adapters: schedule/trigger/watcher/workflow/
482
- // wrfc-chain/wrfc-subtask/background-process all report
483
- // capabilities.interruptible: false unconditionally). Consume the key
484
- // and say so rather than silently calling interrupt() on a node the
485
- // registry has no interrupt route for.
486
- if (!selected.node.capabilities.interruptible) {
487
- this.setError(`${selected.node.kind} does not support interrupt.`);
488
- return true;
489
- }
490
- this.actions.interrupt(selected.node.id);
491
- this.stopTracker.mark(selected.node.id);
492
- this.markDirty();
493
- return true;
494
- }
495
-
496
- if (key === 'K') {
497
- if (!selected || isTerminalProcessState(selected.node.state)) return false;
498
- // Observed foreign agents have killable:false, the refusal below reads
499
- // honestly; goodvibes only observes them, so stop is never offered.
500
- if (!selected.node.capabilities.killable) {
501
- this.setError(`${selected.node.kind} does not support kill.`);
502
- return true;
503
- }
504
- this.confirmOverlay.arm(fleetKillConfirmArgs(selected.node, this.getItems(), {
505
- kill: (id) => this.actions.kill(id, { cascade: true }),
506
- markStopping: (id) => this.stopTracker.mark(id),
507
- }));
508
- return true;
509
- }
510
-
511
- // d2: 'p' toggles pause<->resume by the selected node's state (see
512
- // toggleFleetPause in fleet-stop.ts).
513
- if (key === 'p') {
514
- if (!selected) return false;
515
- return toggleFleetPause(selected.node, {
516
- interrupt: (id) => this.actions.interrupt(id),
517
- resume: (id) => this.actions.resume(id),
518
- setError: (m) => this.setError(m),
519
- markDirty: () => this.markDirty(),
520
- tracker: this.stopTracker,
521
- });
522
- }
523
-
524
- if (key === 'f') {
525
- this.follow = !this.follow;
526
- if (this.follow) this.applyFollow();
527
- this.markDirty();
528
- return true;
529
- }
530
-
531
- if (key === 'enter' || key === 'return') {
532
- if (!selected) return false;
533
- const node = selected.node;
534
- if (!isAttachableFleetKind(node.kind)) {
535
- this.setError(`${node.kind} has no transcript to attach.`);
536
- return true;
537
- }
538
- this.tabsState = attachFleetTab(this.tabsState, node);
539
- this.markDirty();
540
- return true;
541
- }
542
-
543
- if (key === 's') {
544
- // An earlier replay fix: 's' from the TREE was silently dead, steering
545
- // required an undiscoverable Enter-attach first. Attach-and-steer in
546
- // one press for a steerable node; honest refusal otherwise.
547
- if (!selected) return false;
548
- const node = selected.node;
549
- // Observed foreign agents steer as a DRILL-IN only; that 's' is handled by
550
- // acts.handleTreeKey above (opens the composer in the row's detail), so it
551
- // never reaches this attach path.
552
- // Closure replay note: a node that FINISHED in the hint→keypress race
553
- // window must say so, "does not support steer" is misleading for an
554
- // agent whose only problem is being done.
555
- if (isTerminalProcessState(node.state)) {
556
- this.setError(`${node.kind} already finished: nothing to steer.`);
557
- return true;
558
- }
559
- if (!node.capabilities.steerable || !isAttachableFleetKind(node.kind)) {
560
- this.setError(`${node.kind} does not support steer.`);
561
- return true;
562
- }
563
- this.tabsState = attachFleetTab(this.tabsState, node);
564
- this.markDirty();
565
- return this.tryOpenSteerDraft(activeFleetTab(this.tabsState)!);
566
- }
567
-
568
- const consumed = super.handleInput(key);
569
- if (consumed) this.captureSelectionAnchor();
570
- return consumed;
571
- }
572
-
573
- /**
574
- * The `s` gate, mirrors the i/K capability-gating template
575
- * exactly (guard on the live node existing, non-terminal, and the specific
576
- * capability; consume the key and say so rather than silently no-op-ing).
577
- */
578
- private tryOpenSteerDraft(tab: FleetTab): boolean {
579
- const node = this.findLiveNode(tab.nodeId);
580
- if (!node || isTerminalProcessState(node.state)) {
581
- this.setError(`${node?.kind ?? tab.kind} already finished: nothing to steer.`);
582
- return true;
583
- }
584
- if (!node.capabilities.steerable) {
585
- this.setError(`${node.kind} does not support steering.`);
586
- return true;
587
- }
588
- tab.steerDraft = '';
589
- this.markDirty();
590
- return true;
591
- }
592
-
593
- /** Dispatch for the open steer composer, mirrors git-panel.ts's handleCommitMessageInput. */
594
- private handleSteerInput(tab: FleetTab, key: string): boolean {
595
- if (isPanelSearchCancel(key)) {
596
- tab.steerDraft = null;
597
- this.markDirty();
598
- return true;
599
- }
600
- if (isPanelSearchCommit(key)) {
601
- const text = (tab.steerDraft ?? '').trim();
602
- if (text.length === 0) {
603
- tab.steerDraft = null;
604
- this.markDirty();
605
- return true;
606
- }
607
- // Clear the draft ONLY on a confirmed queue. A refusal keeps the composer
608
- // open with the text intact (submitSteer explains why + suggests live targets)
609
- // so the operator never loses what they typed to a just-idle agent.
610
- if (this.submitSteer(tab, text)) tab.steerDraft = null;
611
- this.markDirty();
612
- return true;
613
- }
614
- if (isPanelSearchBackspace(key)) {
615
- tab.steerDraft = (tab.steerDraft ?? '').slice(0, -1);
616
- this.markDirty();
617
- return true;
618
- }
619
- // Ordinary typing AND a pasted multi-line block's line-break characters
620
- // (delivered one at a time through the same burst pipeline, see
621
- // isCapturingTextBurst) both flow through appendSteerText, which
622
- // normalizes \r/\n to a collapsed space instead of corrupting the
623
- // one-line field or silently dropping the content.
624
- if (key.length === 1 && (isPanelSearchPrintable(key) || key === '\r' || key === '\n')) {
625
- tab.steerDraft = appendSteerText(tab.steerDraft ?? '', key);
626
- this.markDirty();
627
- return true;
628
- }
629
- return true; // absorb every other key while composing
630
- }
631
-
632
- /**
633
- * Submit a composed steer message. Returns whether it was queued so the caller
634
- * clears the draft only on a confirmed send (WO item 4): a refusal (target
635
- * went idle while composing) PRESERVES the typed text and suggests which agents
636
- * ARE steerable, instead of silently discarding the draft. On a successful queue
637
- * the ⧗ badge line is the immediate honest acknowledgment.
638
- */
639
- private submitSteer(tab: FleetTab, text: string): boolean {
640
- const result = this.actions.steer(tab.nodeId, text);
641
- if (result.queued) {
642
- // queuedAt drives reconcileSteerBadges' TTL-expiry fallback (fleet-steer.ts).
643
- tab.steerBadge = { messageId: result.messageId, status: 'queued', queuedAt: Date.now() };
644
- this.markDirty();
645
- return true;
646
- }
647
- const siblings = liveSteerableLabels(this.readModel.getSnapshot().rows.map((row) => row.node), tab.nodeId);
648
- this.setError(steerRefusalMessage(result.reason, siblings));
649
- this.markDirty();
650
- return false;
651
- }
652
-
653
- protected renderItem(row: FleetTreeRow, _index: number, _selected: boolean, width: number): Line {
654
- // Display-only overrides (stopping wins, then blocked-on-user) are derived
655
- // here from panel state and passed into the pure row renderer.
656
- const stopping = this.stopTracker.isStopping(row.node.id);
657
- const blocked = !stopping && isBlockedOnUserNode(row.node);
658
- return renderFleetRowLine(row, width, stopping, blocked, this.steerBadgeForNode(row.node.id), C);
659
- }
660
-
661
- /** A node's steer badge, looked up via its attached tab (if any), null when the node has no tab or no active/recent steer. */
662
- private steerBadgeForNode(nodeId: string): SteerBadge | null {
663
- return this.tabsState.tabs.find((t) => t.nodeId === nodeId)?.steerBadge ?? null;
664
- }
665
-
666
- private renderDetail(row: FleetTreeRow, width: number): Line[] {
667
- const node = row.node;
668
- // Mirror the tree row's display-only overrides (stopping wins, then blocked)
669
- // so the detail's literal state text never contradicts the row glyph.
670
- const stopping = this.stopTracker.isStopping(node.id);
671
- const blocked = !stopping && isBlockedOnUserNode(node);
672
- // The drill-in observed-steer draft (when the composer is open on this row)
673
- // renders an input line in the observed detail. For a workstream row, lazily
674
- // fetch its task graph so the edges/pool posture renders in-panel.
675
- this.acts?.ensureGraphFor(node);
676
- return renderFleetDetailLines(node, width, stopping, blocked, C, this.acts?.observedSteerDraftFor(node.id) ?? null, this.acts?.graphFor(node.id) ?? null);
677
- }
678
-
679
- /** Renders the active session tab's content (transcript / chain summary / ledger fallback) in place of the tree. */
680
- private renderTabView(width: number, height: number): Line[] {
681
- const palette = this.getPalette();
682
- const tab = activeFleetTab(this.tabsState);
683
- if (!tab) return this.renderTreeView(width, height); // defensive; should be unreachable
684
- const stripLine = renderFleetTabStrip(this.tabsState, width);
685
-
686
- const snapshotRows = this.readModel.getSnapshot().rows;
687
- const liveNode = snapshotRows.find((row) => row.node.id === tab.nodeId)?.node ?? null;
688
- // The steer composer only ever applies to 'agent' tabs
689
- // (wrfc-chain has no conversation loop of its own, see
690
- // fleet-tabs.ts's FleetAttachableKind doc); the live-node lookup is
691
- // shared with the isTerminal computation below either way.
692
- const canSteer = liveNode !== null && !isTerminalProcessState(liveNode.state) && liveNode.capabilities.steerable;
693
-
694
- // While composing, Enter/Esc replace the tab-switch/detach
695
- // hints (mirrors git-panel.ts's renderCommitCompose footer) so the
696
- // footer never advertises a key the composer itself has absorbed.
697
- const hints: Array<{ keys: string; label: string }> = tab.steerDraft !== null
698
- ? [{ keys: 'Enter', label: 'send' }, { keys: 'Esc', label: 'cancel' }]
699
- : [
700
- { keys: '[ ]', label: 'switch tab' },
701
- { keys: 'Ctrl+X', label: 'detach' },
702
- ...(canSteer ? [{ keys: 's', label: 'steer' }] : []),
703
- ];
704
-
705
- const errorLine = this.renderErrorLine(width);
706
- const composerLines: Line[] = [];
707
- if (tab.steerDraft !== null) {
708
- composerLines.push(buildSearchInputLine(
709
- width,
710
- 'Steer: ',
711
- `${tab.steerDraft}_`,
712
- palette,
713
- { active: true, bg: palette.inputBg, valueColor: palette.info },
714
- ));
715
- } else if (tab.steerBadge && !this.stopTracker.isStopping(tab.nodeId)) {
716
- // A queued-steer promise ("delivers on its next turn") must not render
717
- // while a stop is in flight for this node, there may be no next turn
718
- // (refutation finding 4). The badge reconciles to dropped/consumed once
719
- // the node's terminal state lands.
720
- composerLines.push(renderSteerBadgeLine(tab.steerBadge, width, palette, liveNode?.label));
721
- }
722
-
723
- const footerLines = [
724
- ...(errorLine ? [errorLine] : []),
725
- ...composerLines,
726
- buildKeyboardHints(width, hints, palette),
727
- ];
728
- const contentBudget = getPanelWorkspaceContentBudget(width, height, { footerLines, palette });
729
- const contentHeight = Math.max(1, contentBudget - (stripLine ? 1 : 0));
730
-
731
- let contentLines: Line[];
732
-
733
- if (tab.kind === 'wrfc-chain') {
734
- const memberRows = snapshotRows.filter((row) => row.node.parentId === tab.nodeId);
735
- // d3: a completed chain prunes its wrapper node, so an empty member
736
- // list can mean "finished", not "not started yet". Absent (pruned) OR
737
- // terminal => the honest "completed" wording; present + non-terminal with
738
- // no members yet => the "not started" wording.
739
- const chainDoneOrAbsent = liveNode === null || isTerminalProcessState(liveNode.state);
740
- contentLines = renderFleetChainSummary(memberRows, width, chainDoneOrAbsent);
741
- } else {
742
- const isTerminal = liveNode ? isTerminalProcessState(liveNode.state) : true;
743
- const snapshot = this.actions.getConversationSnapshot(tab.agentId);
744
- const result = renderFleetAgentTranscript(snapshot, isTerminal, tab.lineCache, width, contentHeight, this.configManager);
745
- if (result.mode === 'unavailable') {
746
- this.ensureLedgerLoaded(tab);
747
- contentLines = tab.ledgerEntries !== null
748
- ? renderFleetLedgerFallback(tab.ledgerEntries, width, contentHeight)
749
- : renderFleetTranscriptLoading(width);
750
- } else {
751
- contentLines = result.lines;
752
- }
753
- }
754
-
755
- const sections = [
756
- ...(stripLine ? [{ lines: [stripLine] }] : []),
757
- { lines: contentLines },
758
- ];
759
- return buildPanelWorkspace(width, height, { title: `Fleet: ${tab.label}`, sections, footerLines, palette });
760
- }
761
-
762
- private renderTreeView(width: number, height: number): Line[] {
763
- const selected = this.getSelectedItem();
764
- const footer: Line[] = [];
765
- if (this.confirmOverlay.pending) {
766
- footer.push(...(this.confirmOverlay.renderLines(width) ?? []));
767
- } else if (selected) {
768
- footer.push(...this.renderDetail(selected, width));
769
- }
770
-
771
- // Capability-gated tree hints, only advertise i/K/p when the selected node
772
- // can actually accept them (most kinds are never interruptible; nothing is
773
- // killable once terminal), incl. the d2 state-dependent p pause/resume chip.
774
- // Mirrors the handleInput guards above. See buildFleetTreeHints (fleet-stop.ts).
775
- // Blocked count is always taken from the LIVE fleet (blocked nodes never
776
- // live in the archive), so the 'b' jump hint shows even from the archive view.
777
- const blockedCount = this.readModel.getSnapshot().blockedNodeIds.length;
778
- const hints = buildFleetTreeHints(selected?.node, this.follow, this.tabsState.tabs.length > 0, this.viewMode, blockedCount);
779
-
780
- // Tab strip renders only when tabs exist, omitting it entirely with no
781
- // tabs attached keeps the pre-session-tab root-tree rendering byte-identical.
782
- const stripLine = renderFleetTabStrip(this.tabsState, width);
783
- const header = stripLine ? [stripLine] : undefined;
784
-
785
- return this.renderList(width, height, {
786
- title: this.viewMode === 'archived' ? 'Fleet: Archived' : 'Fleet',
787
- header,
788
- footer,
789
- hints,
790
- });
791
- }
792
-
793
- public override render(width: number, height: number): Line[] {
794
- if (this.acts?.pickModeActive()) return this.acts.renderPickMode(width, height, C);
795
- if (this.spawn?.spawnModeActive()) return this.spawn.renderSpawnMode(width, height, C);
796
- if (this.tabsState.activeTabIndex > 0) return this.renderTabView(width, height);
797
- return this.renderTreeView(width, height);
798
- }
799
- }