@pellux/goodvibes-tui 2.0.20 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +59 -4
  5. package/package.json +16 -6
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /package/src/{panels → views}/provider-health-domains.ts +0 -0
package/src/main.ts CHANGED
@@ -3,6 +3,7 @@ import { resolveGoodVibesDaemonHome, resolveGoodVibesHome } from '@pellux/goodvi
3
3
  import { Compositor } from './renderer/compositor.ts';
4
4
  import { type Line } from '@pellux/goodvibes-sdk/platform/types';
5
5
  import { UIFactory } from './renderer/ui-factory.ts';
6
+ import { resolveThrobberActivity } from './renderer/throbber.ts';
6
7
  import { Orchestrator } from '@pellux/goodvibes-sdk/platform/core';
7
8
  import { InputHandler } from './input/handler.ts';
8
9
  import { SelectionManager } from '@pellux/goodvibes-terminal-shell';
@@ -17,31 +18,35 @@ import { handleBrokerApprovalChange, buildFixSessionAffordance, buildFixSessionE
17
18
  import { CommandRegistry } from './input/command-registry.ts';
18
19
  import type { CommandContext } from './input/command-registry.ts';
19
20
  import { requestedEffortLevel } from './providers/reasoning-effort-surface.ts';
20
- import { renderProcessIndicator } from './renderer/process-indicator.ts';
21
+ import { wireWorkTree } from './core/work-tree-wiring.ts';
21
22
  import { registerBuiltinCommands } from './input/commands.ts';
22
23
  import { ScheduleManager } from '@pellux/goodvibes-sdk/platform/tools';
23
24
  import { InputHistory } from './input/input-history.ts';
24
- import { getTierPromptSupplement, getTierForContextWindow } from '@pellux/goodvibes-sdk/platform/providers';
25
+ import { describeContextWindowSource, getTierPromptSupplement, getTierForContextWindow } from '@pellux/goodvibes-sdk/platform/providers';
25
26
  import { GitStatusProvider } from './renderer/git-status.ts';
26
27
  import type { GitHeaderInfo } from './renderer/git-status.ts';
27
28
  import { createShellLayout } from './renderer/layout-engine.ts';
28
- import { buildShellFooter, estimateShellFooterHeight, promptCursorOffset } from './renderer/shell-surface.ts';
29
+ import { buildShellFooter, estimateShellFooterHeight, promptCursorOffset, statusCostText } from './renderer/shell-surface.ts';
30
+ import { HEADER_GAP_ROWS, withHeaderGap } from './renderer/header-line.ts';
31
+ import { TranscriptScroll } from './shell/transcript-scroll.ts';
32
+ import { formatStatusReport } from './shell/status-report.ts';
33
+ import { voiceCaptureDescription } from './renderer/voice-capture-chip.ts';
34
+ import { voiceCaptureRowVisible } from './core/voice-capture-status.ts';
35
+ import { resolveWebSurfaceUrl } from '@pellux/goodvibes-sdk/platform/runtime/feature-announcements';
29
36
  import { createFailoverTurnState, resolveActiveModelDisplay } from './core/active-model-identity.ts';
30
37
  import { computePromptContentWidth } from './renderer/prompt-content-width.ts';
31
- import { buildConversationViewport } from './renderer/conversation-layout.ts';
32
- import { applyConversationOverlays } from './renderer/conversation-overlays.ts';
33
- import { buildPanelCompositeData } from './renderer/panel-composite.ts';
38
+ import { buildConversationViewport, centerViewportContent } from './renderer/conversation-layout.ts';
39
+ import { applyConversationOverlays, buildConversationLayers } from './renderer/conversation-overlays.ts';
34
40
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
35
- import { registerBuiltinPanels } from './panels/builtin-panels.ts';
36
41
  import { bootstrapRuntime } from './runtime/bootstrap.ts';
37
42
  import type { BootstrapContext } from './runtime/bootstrap.ts';
38
43
  import { selfUpdateAtLaunch } from './cli/launch-auto-update.ts';
39
44
  import { buildSharedOrchestratorCoreServices, refreshMemoryRecallSnapshot } from './runtime/orchestrator-core-services.ts';
40
45
  import { createSessionContinuityHintsBuilder } from './runtime/session-continuity-hints.ts';
41
- import { resolveWebSurfaceUrl } from '@pellux/goodvibes-sdk/platform/runtime/feature-announcements';
42
46
  import { readLastSessionPointer } from '@/runtime/index.ts';
43
47
  import { startRecoveryAutosave } from './runtime/recovery-autosave.ts';
44
- import { scheduleRecoveryOffer } from './runtime/recovery-prompt.ts';
48
+ import { startRecoveryOffer } from './runtime/recovery-prompt.ts';
49
+ import { createStartupTypeaheadGate } from './runtime/startup-typeahead-gate.ts';
45
50
  import { buildRecoveryOfferWiring } from './runtime/recovery-offer-wiring.ts';
46
51
  import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
47
52
  import { handleErrorAffordanceKey } from './shell/recovery-input-helpers.ts';
@@ -53,21 +58,21 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
53
58
  import { prepareShellCliRuntime } from './cli/entrypoint.ts';
54
59
  import { applyInitialTuiCliState, reportFatalStartupError } from './cli/tui-startup.ts';
55
60
  import { applyConfiguredHitlMode, applyRuntimeConfigValue, applyTerminalRuntimeConfigDefaults } from '@pellux/goodvibes-terminal-shell';
56
- import { renderToolCallBlock } from './renderer/tool-call.ts';
57
61
  import { installVoiceCapture } from './shell/voice-capture-shell.ts';
58
62
  import { allowTerminalWrite, installFullScreenTerminalOutputGuard } from '@pellux/goodvibes-terminal-shell/terminal-output-guard';
59
63
  import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
60
64
  import { createRenderScheduler } from '@pellux/goodvibes-terminal-shell';
61
65
  import { buildCommandArgsHint } from './input/command-args-hint.ts';
62
66
  import { summarizeRunningAgents } from './renderer/process-summary.ts';
63
- import { footerFleetCost } from './panels/fleet-read-model.ts';
67
+ import { footerFleetCost } from './views/fleet-read-model.ts';
68
+ import { footerTargetRows } from './renderer/footer-targets.ts';
64
69
  import { formatUserFacingErrorLine } from './core/format-user-error.ts';
65
70
  import { wireStreamEventMetrics, createStreamMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
66
71
  import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
67
72
  import { resolveContextStatusHint } from './renderer/context-status-hint.ts';
68
73
  import { isEffectiveDangerMode } from '@pellux/goodvibes-sdk/platform/config';
69
74
  import { applyComposerCapture, applyAtModelDirective } from './input/composer-capture.ts';
70
- import { makeComposerEditorOpener } from './input/composer-editor.ts';
75
+ import { makeComposerEditorOpener, makeFileEditorOpener } from './input/composer-editor.ts';
71
76
  import { evaluateSessionMaintenance } from '@/runtime/index.ts';
72
77
  import { createCancelGeneration } from './core/turn-cancellation.ts';
73
78
  import { wireInteractionSeams, createMemoryProvenanceUi } from './runtime/interaction-seams.ts';
@@ -75,14 +80,15 @@ import { createPowerChipSource } from './core/power-chip-source.ts';
75
80
  import { fetchDaemonPowerState, installKeepAwakeRemoteForward } from './runtime/power-keepawake-remote.ts';
76
81
  import { wrapRequestPermissionWithAlert } from './core/approval-alert.ts';
77
82
  import { createTerminalNotifier } from './core/terminal-notifier.ts';
78
- import { setPanelFrameRequester } from './panels/base-panel.ts';
79
83
  import { createDeferredRender } from './renderer/deferred-render.ts';
80
84
  import { wireSessionAmbience } from './runtime/session-ambience-wiring.ts';
81
85
  import { createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
82
86
 
83
87
  import { ALT_SCREEN_ENTER, ALT_SCREEN_EXIT, MOUSE_ENABLE, MOUSE_DISABLE, CURSOR_HIDE, CURSOR_SHOW, CLEAR_SCREEN, KEYBOARD_EXT_ENABLE, KEYBOARD_EXT_DISABLE, PASTE_ENABLE, PASTE_DISABLE, FOCUS_ENABLE, FOCUS_DISABLE } from './renderer/terminal-escapes.ts';
84
88
  import { installBackgroundThemeProbe } from './renderer/terminal-bg-probe.ts';
89
+ import { registerThemeRefresh } from './renderer/theme.ts';
85
90
  import { VERSION } from './version.ts';
91
+ import { SessionViews } from './shell/session-views.ts';
86
92
 
87
93
  async function main() {
88
94
  const stdout = process.stdout;
@@ -158,12 +164,12 @@ async function main() {
158
164
  // that should compete with real session alerts for attention).
159
165
  for (const line of launchUpdateLines) systemMessageRouter.low(`[Update] ${line}`);
160
166
 
161
- const panelManager = ctx.services.panelManager;
162
- const buildSessionContinuityHints = createSessionContinuityHintsBuilder({ readModels: uiServices.readModels, panelManager });
167
+ const buildSessionContinuityHints = createSessionContinuityHintsBuilder({ readModels: uiServices.readModels });
163
168
 
164
169
  // Callable before the scheduler exists; see deferred-render.ts for why.
165
170
  const deferredRender = createDeferredRender();
166
171
  const render = deferredRender.render;
172
+ const typeahead = createStartupTypeaheadGate(); // keys typed before a startup modal appeared never answer it
167
173
 
168
174
  let pendingPermission: PendingPermissionState | null = null;
169
175
  // One-key jump/attach to a spawned CI fix-session: the affordance ARMS the id; the next 'j' runs the resume so the user never retypes it.
@@ -183,12 +189,12 @@ async function main() {
183
189
  }));
184
190
  refreshFixSessionsFromApprovals(() => approvalBroker.listApprovals(), onFixSessionStarted, onFixSessionError); // catch pre-subscription stamps
185
191
 
186
- let scrollTop = 0;
187
- let scrollLocked = true;
188
- // Cached from the overlay-aware clamp the renderer computes (conversation-layout) each frame so scroll() clamps against exactly what is displayed, not a footer estimate.
189
- let lastMaxScroll: number | null = null;
192
+ // Where the main transcript is scrolled to; each frame's overlay-aware clamp (conversation-layout) is fed back so a scroll clamps against exactly what is displayed (shell/transcript-scroll.ts).
193
+ const transcript = new TranscriptScroll();
190
194
  // Stream and tool-timer state; mutated by wireStreamEventMetrics handlers, read during render.
191
195
  const streamMetrics: StreamMetrics = createStreamMetrics();
196
+ // When the running compaction was first seen (the throbber's elapsed time); undefined while none runs.
197
+ let compactingSinceMs: number | undefined;
192
198
  // Live failover record, written by the failover path, read every frame so the header and footer agree.
193
199
  const failoverState = createFailoverTurnState();
194
200
 
@@ -196,33 +202,37 @@ async function main() {
196
202
  // Live-microphone footer row (push-to-talk or the wake detector); assigned once voice capture is wired below, null until then so pre-wiring frames size correctly.
197
203
  let voiceCaptureStatus: () => import('./core/voice-capture-status.ts').VoiceCaptureIndicatorState | null = () => null;
198
204
 
205
+ // Agents and background processes opened full screen (shell/session-views.ts); set once the input handler exists.
206
+ let sessionViews: SessionViews | null = null;
207
+ let lastHeaderModel = runtime.model;
199
208
  const getViewportHeight = (): number => {
200
209
  if (input.onboardingWizard.active) return stdout.rows || 24;
201
210
  const promptLines: number = input.getVisiblePromptLineCount(getPromptContentWidth());
202
211
  const currentModel = providerRegistry.getCurrentModel();
203
212
  const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
204
213
  const rows = stdout.rows || 24;
205
- // Compact threshold must match buildShellFooter's `compact: height < 30` posture below, else estimateShellFooterHeight's cached-height fast path answers with the wrong mode.
206
- return rows - 2 - estimateShellFooterHeight(promptLines, contextWindow, rows < 30, voiceCaptureStatus());
214
+ // The header row (+ the session chips), and on the main screen the empty row under them (a view's body brings its own).
215
+ return rows - (sessionViews?.headerRows() ?? 1) - (sessionViews?.active ? 0 : HEADER_GAP_ROWS) - estimateShellFooterHeight(promptLines);
207
216
  };
208
217
 
209
218
  const scroll = (delta: number) => {
210
- // Prefer the last clamp the renderer computed (overlay- and real-footer-aware).
211
- // The footer estimate is only a fallback for the pre-first-render frame.
212
- const maxScroll = lastMaxScroll ?? Math.max(0, conversation.history.getLineCount() - getViewportHeight());
213
- scrollTop = Math.max(0, Math.min(scrollTop + delta, maxScroll));
214
- // Re-lock if user scrolled to bottom, otherwise unlock
215
- scrollLocked = scrollTop >= maxScroll;
219
+ if (sessionViews?.active) { sessionViews.scroll(-delta); return; } // a view scrolls its own lines (up is positive there)
220
+ // The footer estimate is only a fallback for the pre-first-render frame; reaching the bottom re-locks.
221
+ transcript.scrollBy(delta, () => Math.max(0, conversation.history.getLineCount() - getViewportHeight()));
216
222
  };
217
223
 
218
- const scrollToEnd = (vHeight: number) => {
219
- // Respect a manual scroll-up by the user: only auto-follow the tail when parked at the
220
- // bottom (scrollLocked). submitInput re-locks on new input so turns resume following.
221
- if (!scrollLocked) return;
222
- scrollTop = Math.max(0, conversation.history.getLineCount() - vHeight);
224
+ // Back to the live bottom: re-lock, and the next frame follows the tail again.
225
+ const scrollToLiveBottom = () => {
226
+ transcript.toBottom();
227
+ render();
223
228
  };
224
229
 
230
+ // Respect a manual scroll-up: only follow the tail while parked at the bottom (submitInput re-locks on new input).
231
+ const scrollToEnd = (vHeight: number) => transcript.followTail(conversation.history.getLineCount(), vHeight);
232
+
225
233
  const unsubs: Array<() => void> = [];
234
+ // The work tree's live facts: call/turn timings, agent lanes, the call a permission prompt holds, fold persistence (work-tree-wiring.ts).
235
+ unsubs.push(...wireWorkTree({ conversation, events: uiServices.events, agentManager, listChains: () => ctx.services.wrfcController.listChains(), fleetNodes: () => ctx.services.processRegistry.query().nodes, pendingCallId: () => pendingPermission?.callId, turnActive: () => orchestrator.isThinking, sessionsDir: ctx.services.surface.sessionsDir, sessionId: () => runtime.sessionId, requestRender: () => render() }).unsubs);
226
236
  let recoveryInterval: ReturnType<typeof setInterval> | null = null;
227
237
  let stopSpokenOutputForExit: (() => Promise<void>) | null = null;
228
238
  // The optional "used N memories" provenance chip (default OFF), see interaction-seams.ts.
@@ -273,7 +283,7 @@ async function main() {
273
283
  unsubs.push(...ambience.unsubs);
274
284
  const submitInput = (text: string, content?: ContentPart[], options: { readonly spokenOutput?: boolean } = {}) => {
275
285
  input.clearModalStack();
276
- scrollLocked = true; // Re-lock on any user input
286
+ transcript.toBottom(); // Re-lock on any user input
277
287
  conversation.dismissSplash(); // owner rule: any submission retires the splash for the run
278
288
  let processedText = applyAtModelDirective(text, {
279
289
  providerRegistry, runtime, configManager, notify: (m) => systemMessageRouter.high(m),
@@ -311,16 +321,14 @@ async function main() {
311
321
  render();
312
322
  return;
313
323
  }
314
- scrollLocked = false;
315
- scrollTop = Math.max(0, line);
324
+ transcript.jumpTo(line);
316
325
  render();
317
326
  };
318
327
 
319
328
  const scrollToLine = (line: number) => {
320
329
  conversation.getDisplayBlocks();
321
330
  const maxScroll = Math.max(0, conversation.history.getLineCount() - getViewportHeight());
322
- scrollLocked = false;
323
- scrollTop = Math.max(0, Math.min(line, maxScroll));
331
+ transcript.jumpTo(line, maxScroll);
324
332
  render();
325
333
  };
326
334
 
@@ -352,12 +360,12 @@ async function main() {
352
360
  ...buildPendingPermissionExtras(request, resolve, approvalBroker),
353
361
  };
354
362
  render();
355
- }), { focusTracker: ctx.services.focusTracker, configGet: (k: string) => configManager.get(k as Parameters<typeof configManager.get>[0]), webhookNotifier: ctx.services.webhookNotifier, terminalNotifier });
363
+ }), { focusTracker: ctx.services.focusTracker, configGet: (k: string) => configManager.get(k as Parameters<typeof configManager.get>[0]), webhookNotifier: ctx.services.webhookNotifier, terminalNotifier, conversation });
356
364
 
357
365
  const input: InputHandler = new InputHandler(
358
366
  () => render(),
359
367
  selection,
360
- () => scrollTop,
368
+ () => transcript.top,
361
369
  getViewportHeight,
362
370
  () => conversation.history,
363
371
  scroll,
@@ -390,7 +398,6 @@ async function main() {
390
398
  shell: {
391
399
  bookmarkManager: ctx.services.bookmarkManager,
392
400
  keybindingsManager: ctx.services.keybindingsManager,
393
- panelManager,
394
401
  processManager,
395
402
  profileManager: ctx.services.profileManager,
396
403
  },
@@ -409,9 +416,19 @@ async function main() {
409
416
  );
410
417
 
411
418
  orchestratorRefs.getViewportHeight = getViewportHeight; orchestratorRefs.scrollToEnd = scrollToEnd;
419
+ const views = new SessionViews({
420
+ conversation, agentManager, processManager, fleetNodes: () => ctx.services.processRegistry.query().nodes,
421
+ steer: (id, text) => ctx.views.fleet.actions.steer(id, text), killAgent: (id) => ctx.views.fleet.actions.kill(id, { cascade: true }),
422
+ mainBusy: () => orchestrator.isThinking, mainModel: () => lastHeaderModel, promptText: () => input.prompt, requestRender: () => render(),
423
+ });
424
+ sessionViews = views; input.sessionView = views; unsubs.push(() => views.dispose());
425
+ // Esc while the main transcript is scrolled back returns to the live bottom and never interrupts the turn (handler-modal-stack.ts).
426
+ input.transcriptScroll = { scrolledBack: () => transcript.scrolledBack && !views.active, toBottom: scrollToLiveBottom };
427
+ commandContext.openSessionView = (target) => views.open(target);
412
428
 
413
429
  input.setCommandRegistry(commandRegistry, commandContext);
414
430
  commandContext.openComposerEditor = makeComposerEditorOpener({ buffer: input, stdin, stdout, writeGuard: allowTerminalWrite, repaint: () => { compositor.resetDiff(); render(); }, cwd: workingDir, env: process.env, notify: (m) => systemMessageRouter.high(m) });
431
+ commandContext.openFileInEditor = makeFileEditorOpener({ stdin, stdout, writeGuard: allowTerminalWrite, repaint: () => { compositor.resetDiff(); render(); }, cwd: workingDir, env: process.env, notify: (m) => systemMessageRouter.high(m) });
415
432
  input.setConversationManager(conversation);
416
433
  input.setContentWidth(getPromptContentWidth()); input.filePicker.setOnUpdate(() => render());
417
434
  // retirement: agentDetailModal/processModal setOnRefresh wiring removed,
@@ -437,26 +454,37 @@ async function main() {
437
454
 
438
455
  // Cache the current model for consistent values across the entire render frame
439
456
  const currentModel = providerRegistry.getCurrentModel();
440
- // Resolve the effective context window (provider_api / configured_cap overrides) once,
441
- // so the footer meter, footer-height, and context inspector agree with the Tokens panel.
442
- const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
457
+ // Resolve the context window once, so the footer meter, the status hint and
458
+ // the context inspector agree. Only a window some source states is shown:
459
+ // null means unknown (a guess, or disproven by a larger accepted request),
460
+ // and every consumer below reads 0 as "no window to measure against".
461
+ const knownContextWindow = providerRegistry.getKnownContextWindowForModel(currentModel);
462
+ const contextWindow = knownContextWindow ?? 0;
443
463
  const sessionSnapshot = uiServices.readModels.session.getSnapshot();
444
464
  const agentSnapshot = uiServices.readModels.agents.getSnapshot();
445
465
 
446
466
  const activeModel = resolveActiveModelDisplay({ serving: currentModel, configuredRegistryKey: configManager.get('provider.model') as string, configuredLabel: runtime.model, configuredProvider: runtime.provider, failover: failoverState.current() });
447
- const headerLines = UIFactory.createHeader(width, activeModel.headerModel, activeModel.headerProvider, conversation.title || undefined, lastGitInfoRef.value, undefined, activeModel.divergenceNote);
467
+ lastHeaderModel = activeModel.headerModel;
468
+ const viewFrame = views.frame(width); // an agent or process open full screen, or null in main
469
+ const headerLines = viewFrame ? [viewFrame.header] : UIFactory.createHeader(width, activeModel.headerModel, conversation.title || undefined, lastGitInfoRef.value, undefined, activeModel.divergenceNote);
470
+ const chipsRow = views.chips(width);
471
+ if (chipsRow) headerLines.push(chipsRow);
472
+ // Scrolled away from the live bottom: the back-to-bottom pill, with the esc keycap only while the next Esc goes there.
473
+ const promptEmpty = input.prompt.length === 0 && !input.commandMode;
474
+ const backToBottom = viewFrame
475
+ ? (views.scrolledBack() ? { escKey: views.escGoesToBottom() } : null)
476
+ : (transcript.scrolledBack && !conversation.isSplashShowing() ? { escKey: promptEmpty && !conversation.workTree.focused && !input.indicatorFocused } : null);
448
477
  const managerAgents = agentManager.list().filter((a) => a.status === 'running' || a.status === 'pending');
449
478
  const runtimeAgents = agentSnapshot.active;
450
479
  const runningAgentSummary = summarizeRunningAgents(managerAgents, runtimeAgents, ctx.services.wrfcController.listChains());
451
480
  const runningAgentCount = runningAgentSummary.count;
452
- const runningProcessCount = processManager.list().filter((p) => !p.status.startsWith('done')).length;
481
+ const runningProcessCount = processManager.list().filter((p) => !p.done).length;
453
482
  const cw = getPromptContentWidth();
454
483
  const promptInfo = input.getWrappedPromptInfo(cw);
455
484
  const commandArgsHint = buildCommandArgsHint(input.prompt, commandRegistry);
456
485
  const composerState = deriveComposerState({
457
486
  text: input.prompt,
458
487
  commandMode: input.commandMode,
459
- panelFocused: input.panelFocused,
460
488
  pendingApproval: pendingPermission !== null,
461
489
  hasAttachments: input.getImageAttachments().size > 0, turnState: sessionSnapshot.turnState,
462
490
  });
@@ -464,84 +492,86 @@ async function main() {
464
492
  evaluate: (args) => evaluateSessionMaintenance({ configManager, ...args, sessionMemoryCount: ctx.services.sessionMemoryStore.list().length }),
465
493
  currentTokens: orchestrator.lastInputTokens, contextWindow,
466
494
  });
495
+ // What main is doing while it works (a turn, a compaction): the throbber row above the input area.
496
+ const compacting = ctx.services.contextAccountingHolder.getSource()?.getCompactionState().isCompacting === true;
497
+ if (compacting && compactingSinceMs === undefined) compactingSinceMs = Date.now();
498
+ else if (!compacting) compactingSinceMs = undefined;
499
+ const throbberNow = Date.now();
500
+ const activeToolCallId = streamMetrics.activeToolCallId;
501
+ // An ask brokered for a background agent carries an attribution; only main's own ask is main's activity.
502
+ const mainApproval = pendingPermission && pendingPermission.attribution === undefined ? pendingPermission : null;
503
+ const throbberActivity = resolveThrobberActivity({
504
+ turnActive: orchestrator.isThinking,
505
+ compacting,
506
+ compactingSinceMs,
507
+ pendingApproval: mainApproval ? { name: mainApproval.tool, args: mainApproval.args } : null,
508
+ activeTool: streamMetrics.activeToolName !== undefined
509
+ ? { name: streamMetrics.activeToolName, args: activeToolCallId !== undefined ? streamMetrics.toolArgsByCallId.get(activeToolCallId) : undefined, startedAtMs: streamMetrics.activeToolStartedAtMs }
510
+ : null,
511
+ modelPhrase: UIFactory.busyPhrase(orchestrator.thinkingFrame, orchestrator.streamingOutputTokens, UIFactory.computeRenderStallInfo(streamMetrics, throbberNow), mainApproval !== null),
512
+ turnStartMs: streamMetrics.startTime,
513
+ ttftMs: streamMetrics.ttftMs,
514
+ tokenSpeed: (configManager.get('display.showTokenSpeed') as boolean) ? streamMetrics.tokenSpeed : undefined,
515
+ now: throbberNow,
516
+ });
517
+ const throbber = throbberActivity ? { spinner: orchestrator.getSpinner(), frame: orchestrator.thinkingFrame, activity: throbberActivity } : null;
467
518
  const footerLines = buildShellFooter({
468
519
  width,
469
520
  promptText: promptInfo.visibleLines.join('\n'),
470
521
  promptLineCount: promptInfo.visibleLines.length,
471
522
  promptCursorPos: promptCursorOffset(promptInfo),
472
- usage: { up: orchestrator.usage.input, down: orchestrator.usage.output, fleetCostUsd: footerFleetCost(() => ctx.services.processRegistry.query().nodes, runningAgentCount > 0) },
523
+ usage: { up: orchestrator.usage.input, down: orchestrator.usage.output, cacheRead: orchestrator.usage.cacheRead, cacheWrite: orchestrator.usage.cacheWrite, fleetCostUsd: footerFleetCost(() => ctx.services.processRegistry.query().nodes, runningAgentCount > 0) },
473
524
  showExitNotice: input.showExitNotice,
474
525
  lastCopyTime: input.lastCopyTime,
475
- model: activeModel.footerModel, modelNote: activeModel.divergenceNote,
476
- toolCount: toolRegistry.list().length,
477
- workingDir,
478
- provider: activeModel.footerProvider,
479
- contextWindow,
526
+ model: activeModel.footerModel, // prices the cost; the header names the model
527
+ workingDir, homeDirectory, view: viewFrame?.footer ?? null,
528
+ branch: lastGitInfoRef.value?.branch,
529
+ contextWindow: knownContextWindow,
480
530
  contextStatusHint,
481
531
  retryHint: retryAffordanceHint(retryAffordance),
482
532
  scriptableStatusLine: scriptableStatusline.current(),
483
- // Compact footer posture on short terminals so the shell stays usable.
484
- compact: height < 30,
485
533
  // behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
486
- // the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
534
+ // the bar expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
487
535
  compactThreshold: Math.min(1, Math.max(0, (configManager.get('behavior.autoCompactThreshold') as number) / 100)),
488
536
  dangerMode: isEffectiveDangerMode(configManager),
489
537
  lastInputTokens: orchestrator.lastInputTokens,
490
538
  commandArgsHint,
491
- hitlMode: modeManager.getHITLMode(),
492
- // Cross-surface spine posture segment (adopted-daemon mode only).
493
- sessionSpineStatus: (() => { const s = uiServices.platform.externalServices?.inspect(); return s?.sessionSpineActive && s.sessionSpineStatus && s.sessionSpineStatus !== 'unknown' ? s.sessionSpineStatus : undefined; })(), runningAgentCount, runningProcessCount,
494
- webSurfaceUrl: configManager.get('web.enabled') ? resolveWebSurfaceUrl(configManager) : undefined,
539
+ runningAgentCount, runningProcessCount,
495
540
  // Always-visible "sleep disabled" chip, topology-aware: the DAEMON's state in adopted-external mode, the in-process manager otherwise (power-chip-source.ts).
496
541
  powerKeepAwake: powerChipSource.get().keepAwake,
497
- // Composer must not read as focused while the panel/process indicator owns keyboard focus.
498
- promptFocused: !input.panelFocused && !input.indicatorFocused,
542
+ // Composer must not read as focused while the process indicator owns keyboard focus.
543
+ promptFocused: !input.indicatorFocused && !conversation.workTree.focused && viewFrame?.footer.disabledReason === undefined, workTreeFocused: conversation.workTree.focused && !viewFrame,
499
544
  indicatorFocused: input.indicatorFocused,
500
545
  runningAgentProgress: runningAgentSummary.progress,
501
- composerMode: composerState.modeLabel,
502
- composerStatus: composerState.statusLabel,
503
546
  composerFlags: composerState.flags,
504
547
  composerPendingRisk: composerState.pendingRisk, permissionMode: configManager.get('permissions.mode') as string, voiceCapture: voiceCaptureStatus(),
548
+ throbber, turnRunning: orchestrator.isThinking, backToBottom,
505
549
  }).lines;
506
550
 
507
551
  const onboardingOwnsScreen = input.onboardingWizard.active;
508
- const shellHeaderLines = onboardingOwnsScreen ? [] : headerLines;
552
+ // The main screen keeps one empty row under the header; a view's body starts with its own.
553
+ const shellHeaderLines = onboardingOwnsScreen ? [] : viewFrame ? headerLines : withHeaderGap(headerLines, width);
554
+ input.bodyTopRow = shellHeaderLines.length; // mouse rows map to transcript rows from here
509
555
  const shellFooterLines = onboardingOwnsScreen ? [] : footerLines;
510
- const panelWidth = !onboardingOwnsScreen && panelManager.isVisible() && panelManager.getAllOpen().length > 0
511
- ? panelManager.getRightWidth(width)
512
- : 0;
556
+ input.footerTargets = footerTargetRows(shellFooterLines, height - shellFooterLines.length); // clickable usage rows open Usage
513
557
  const shellLayout = createShellLayout({
514
558
  width,
515
559
  height,
516
560
  headerHeight: shellHeaderLines.length,
517
561
  footerHeight: shellFooterLines.length,
518
- panelWidth,
519
562
  });
520
- input.setPanelMouseLayout(shellLayout.panel
521
- ? {
522
- x: shellLayout.panel.x,
523
- y: shellLayout.panel.y,
524
- width: shellLayout.panel.width,
525
- height: shellLayout.panel.height,
526
- hasBottomPane: panelManager.isBottomPaneVisible() && panelManager.getBottomPane().panels.length > 0,
527
- verticalSplitRatio: panelManager.getVerticalSplitRatio(),
528
- }
529
- : null);
530
563
  const vHeight = shellLayout.body.height;
531
564
  const conversationWidth = shellLayout.conversation.width;
532
565
  activeConversationWidth = conversationWidth;
533
- const hasPanelWorkspace = !onboardingOwnsScreen && panelManager.isVisible() && panelManager.getAllOpen().length > 0;
534
- conversation.setSplashSuppressed(hasPanelWorkspace);
535
566
 
536
567
  // Flush pending renders after updating the width provider and splash posture
537
568
  // so the transcript and splash rebuild against the current shell layout.
569
+ conversation.workTree.tickLive(); // running beads spin and count up
538
570
  conversation.getDisplayBlocks();
539
571
  if (conversation.consumeSplashTransition()) compositor.requestFullRepaint(); // splash → transcript: repaint the whole viewport once
540
572
 
541
573
  // Calculate how many rows are consumed by overlays (thinking, permissions, queue, file picker)
542
574
  let overlayRows = 0;
543
- if (orchestrator.isThinking) overlayRows += 2; // spinner + blank
544
- if (pendingPermission) overlayRows += PermissionPromptUI.getPromptHeight(pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded, pendingPermission.requestedBy, PermissionPromptUI.promptViewState(pendingPermission, conversationWidth, approvalBroker));
545
575
  overlayRows += orchestrator.messageQueue.length * 3; // queued messages
546
576
  // File picker and model picker overlay rows computed from actual rendered line count below
547
577
  // Selection modal overlay rows are computed from actual rendered line count below
@@ -553,102 +583,83 @@ async function main() {
553
583
  conversation,
554
584
  width: conversationWidth,
555
585
  viewportHeight: vHeight,
556
- scrollTop,
557
- scrollLocked,
586
+ scrollTop: transcript.top,
587
+ scrollLocked: transcript.locked,
558
588
  overlayRows,
559
589
  });
560
- scrollTop = conversationViewport.nextScrollTop;
561
- lastMaxScroll = conversationViewport.maxScroll;
562
- let viewport = conversationViewport.viewport;
563
-
564
- if (orchestrator.isThinking) {
565
- const showSpeed = configManager.get('display.showTokenSpeed') as boolean;
566
- const showPreview = configManager.get('display.showToolPreview') as boolean;
567
- const partialToolPreview = showPreview ? sessionSnapshot.streamToolPreview : undefined;
568
- // Elapsed from turn start (stream or tool execution), used for the thinking indicator timer.
569
- const turnElapsedMs = streamMetrics.startTime > 0 ? Date.now() - streamMetrics.startTime : undefined;
570
- // Suppressed while a tool executes, its ticking timer is the honest indicator then.
571
- const stallInfo = UIFactory.computeRenderStallInfo(streamMetrics, Date.now());
572
- const thinking = UIFactory.createThinkingFragment(
573
- conversationWidth,
574
- orchestrator.getSpinner(),
575
- orchestrator.thinkingFrame,
576
- showSpeed ? streamMetrics.tokenSpeed : undefined,
577
- showPreview ? partialToolPreview : undefined,
578
- orchestrator.streamingInputTokens > 0 ? orchestrator.streamingInputTokens : undefined,
579
- orchestrator.streamingOutputTokens > 0 ? orchestrator.streamingOutputTokens : undefined,
580
- turnElapsedMs,
581
- streamMetrics.ttftMs,
582
- stallInfo,
583
- pendingPermission !== null,
584
- );
585
- viewport.push(...thinking);
586
- // Live tool timer: render the currently executing tool row with ticking elapsed.
587
- if (streamMetrics.activeToolName !== undefined && streamMetrics.activeToolStartedAtMs !== undefined) {
588
- const liveToolCall = { id: streamMetrics.activeToolCallId ?? 'live', name: streamMetrics.activeToolName, arguments: {} };
589
- viewport.push(...renderToolCallBlock(liveToolCall, 'executing', undefined, conversationWidth, undefined, undefined, undefined, streamMetrics.activeToolStartedAtMs));
590
- }
590
+ // Unlocked but already at the bottom (a jump that landed there, output that
591
+ // shrank) is the live bottom: it follows again, and the pill drawn this frame goes.
592
+ if (transcript.settle(conversationViewport.nextScrollTop, conversationViewport.maxScroll) && backToBottom && !viewFrame) render();
593
+ const scrollTop = transcript.top;
594
+ let viewport = viewFrame ? viewFrame.body(vHeight) : conversation.isSplashShowing()
595
+ ? centerViewportContent(conversationViewport.viewport, conversationViewport.effectiveHeight, conversationWidth)
596
+ : conversationViewport.viewport;
597
+
598
+ if (orchestrator.isThinking && !viewFrame) {
599
+ // The spinner and phrase are the throbber's (above the input area); the
600
+ // opt-in partial tool preview keeps its own faint row under the transcript.
601
+ const partialToolPreview = (configManager.get('display.showToolPreview') as boolean) ? sessionSnapshot.streamToolPreview : undefined;
602
+ if (partialToolPreview) viewport.push(UIFactory.createToolPreviewRow(conversationWidth, partialToolPreview));
591
603
  }
592
604
 
593
- if (pendingPermission) {
594
- viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded, pendingPermission.requestedBy, PermissionPromptUI.promptViewState(pendingPermission, conversationWidth, approvalBroker)));
595
- }
596
-
597
- viewport.push(...UIFactory.createQueuedMessageList(conversationWidth, orchestrator.listQueuedMessages()));
598
- viewport.push(...memoryProvenanceUi.renderChip(conversationWidth, configManager));
599
-
600
- viewport = applyConversationOverlays(viewport, {
601
- input,
602
- conversation,
603
- commandRegistry,
604
- keybindingsManager: ctx.services.keybindingsManager,
605
- conversationWidth,
606
- viewportHeight: vHeight,
607
- contextWindow,
608
- });
605
+ if (!viewFrame) viewport.push(...UIFactory.createQueuedMessageList(conversationWidth, orchestrator.listQueuedMessages()), ...memoryProvenanceUi.renderChip(conversationWidth, configManager));
609
606
 
610
- // Panel composite data
611
- const panelComposite = onboardingOwnsScreen
612
- ? { panelData: undefined, panelWidth: 0 }
613
- : buildPanelCompositeData(
614
- panelManager,
615
- input,
616
- shellLayout.panel?.width ?? 0,
617
- shellLayout.panel?.height ?? vHeight,
618
- );
607
+ const overlayContext = { input, conversation, commandRegistry, keybindingsManager: ctx.services.keybindingsManager, contextWindow };
608
+ viewport = applyConversationOverlays(viewport, { ...overlayContext, conversationWidth, viewportHeight: vHeight });
619
609
 
620
610
  compositor.composite({
621
611
  width, height,
622
612
  header: shellHeaderLines,
623
613
  viewport,
624
614
  footer: shellFooterLines,
625
- selection: onboardingOwnsScreen ? undefined : {
615
+ selection: onboardingOwnsScreen || viewFrame ? undefined : {
626
616
  isCellSelected: (col, row) => selection.isCellSelected(col, row),
627
617
  scrollTop,
628
618
  lineCount: conversation.history.getLineCount(),
629
619
  },
630
- search: !onboardingOwnsScreen && input.searchManager.active ? {
620
+ search: !onboardingOwnsScreen && !viewFrame && input.searchManager.active ? {
631
621
  manager: input.searchManager,
632
622
  scrollTop,
633
623
  viewportStartY: shellHeaderLines.length,
634
624
  } : undefined,
635
- panel: panelComposite.panelData,
636
- panelWidth: panelComposite.panelWidth,
637
- });
625
+ layers: buildConversationLayers({ ...overlayContext, screenWidth: width, screenHeight: height, headerRows: shellHeaderLines.length, footerRows: shellFooterLines.length, permission: pendingPermission ? PermissionPromptUI.renderPromptModal(width, height, pendingPermission, pendingPermission, approvalBroker) : null }),
626
+ }); typeahead.framePainted(); // a startup modal counts as seen from its first painted frame
638
627
  };
639
628
  const renderScheduler = createRenderScheduler(renderNow, undefined, () => lifecycle.isTerminalRestored()); // coalescer; no frames after terminal restore
640
629
  deferredRender.set(() => renderScheduler.schedule()); // from here on, render() actually schedules
641
630
  const terminalOutputGuard = installFullScreenTerminalOutputGuard({ stdout, stderr: process.stderr, onCapture: (total) => { commandContext.session.runtime.terminalWritesIntercepted = total; render(); } });
642
631
 
643
632
  setRenderRequest(() => renderScheduler.flushNow()); // bootstrap's 16ms coalescer composites via the (restore-gated) scheduler
644
- setPanelFrameRequester(render); // live panels repaint when idle (a replay finding: fleet sat stale until keypress)
645
633
  orchestratorRefs.requestRender = render;
646
634
  commandContext.renderRequest = render;
635
+ // /status: what the footer rows used to show, from the same sources (shell/status-report.ts).
636
+ commandContext.describeStatus = () => {
637
+ const serving = providerRegistry.getCurrentModel();
638
+ const active = resolveActiveModelDisplay({ serving, configuredRegistryKey: configManager.get('provider.model') as string, configuredLabel: runtime.model, configuredProvider: runtime.provider, failover: failoverState.current() });
639
+ const spine = uiServices.platform.externalServices?.inspect();
640
+ const voice = voiceCaptureStatus();
641
+ const git = lastGitInfoRef.value;
642
+ return formatStatusReport({
643
+ workingDirectory: workingDir, branch: git?.branch, dirty: git?.dirty,
644
+ model: active.footerModel, provider: active.footerProvider, modelNote: active.divergenceNote,
645
+ permissionMode: configManager.get('permissions.mode') as string, toolCount: toolRegistry.list().length, notifyMode: modeManager.getHITLMode(),
646
+ usage: orchestrator.usage,
647
+ cost: statusCostText({ up: orchestrator.usage.input, down: orchestrator.usage.output, cacheRead: orchestrator.usage.cacheRead, cacheWrite: orchestrator.usage.cacheWrite, fleetCostUsd: footerFleetCost(() => ctx.services.processRegistry.query().nodes, true) }, active.footerModel),
648
+ contextTokens: orchestrator.lastInputTokens, contextWindow: providerRegistry.getKnownContextWindowForModel(serving) ?? 0, contextWindowSource: describeContextWindowSource(serving),
649
+ compactFraction: Math.min(1, Math.max(0, (configManager.get('behavior.autoCompactThreshold') as number) / 100)),
650
+ sessionSpine: spine?.sessionSpineActive && spine.sessionSpineStatus && spine.sessionSpineStatus !== 'unknown' ? spine.sessionSpineStatus : undefined,
651
+ webSurfaceUrl: configManager.get('web.enabled') ? resolveWebSurfaceUrl(configManager) : undefined,
652
+ autoApprove: isEffectiveDangerMode(configManager), keepAwake: powerChipSource.get().keepAwake,
653
+ microphone: voice && voiceCaptureRowVisible(voice) ? voiceCaptureDescription(voice) : null,
654
+ runningAgents: agentManager.list().filter((a) => a.status === 'running' || a.status === 'pending').length,
655
+ runningProcesses: processManager.list().filter((p) => !p.done).length,
656
+ });
657
+ };
647
658
  wireShellUiOpeners({
648
659
  commandContext,
649
660
  input,
650
- panelManager,
651
- conversation,
661
+ views: ctx.views,
662
+ viewPanelAdapter: ctx.services.panelManager,
652
663
  configManager,
653
664
  providerRegistry,
654
665
  runtime,
@@ -667,7 +678,7 @@ async function main() {
667
678
  render, trustPromptRef,
668
679
  });
669
680
 
670
- const { refreshGit, unsubs: turnUnsubs } = wireTurnEventHandlers({
681
+ const { refreshGit, unsubs: turnUnsubs, continueTurnAfterFailover } = wireTurnEventHandlers({
671
682
  events: uiServices.events,
672
683
  conversation,
673
684
  runtime,
@@ -709,7 +720,7 @@ async function main() {
709
720
  const streamResult: WireStreamEventMetricsResult = wireStreamEventMetrics({
710
721
  events: uiServices.events, orchestrator, providerRegistry,
711
722
  systemMessageRouter, render, metrics: streamMetrics,
712
- providerOptimizer: ctx.services.providerOptimizer, costLookup: providerRegistry, retryTurn,
723
+ providerOptimizer: ctx.services.providerOptimizer, costLookup: providerRegistry, retryTurn, onFailoverRetry: continueTurnAfterFailover,
713
724
  failoverState, getConfiguredRegistryKey: () => configManager.get('provider.model') as string | undefined,
714
725
  // The REQUESTED level, through the one helper every remap site reads from.
715
726
  getConfiguredReasoningEffort: () => requestedEffortLevel(configManager),
@@ -729,15 +740,16 @@ async function main() {
729
740
  // --- Terminal setup ---
730
741
  stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf8');
731
742
  allowTerminalWrite(() => stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE + FOCUS_ENABLE));
732
- // forced dark/light applies before first paint; auto (TTY only) fires the
733
- // OSC 11 probe and repaints once if light wins. filterInput strips the reply from stdin.
734
- const themeProbe = installBackgroundThemeProbe({ configManager, isTTY: Boolean(stdout.isTTY), env: process.env, writeQuery: (b) => allowTerminalWrite(() => stdout.write(b)), requestRepaint: () => { compositor.resetDiff(); render(); } });
743
+ // forced dark/light applies before first paint; auto (TTY only) repaints once if the OSC 11 probe says light. probePalette adds OSC 10 + OSC 4;0..15
744
+ // and retries until the terminal answers (terminal-palette-reader.ts); the `system` theme re-themes when it arrives. filterInput strips replies from stdin.
745
+ registerThemeRefresh(() => conversation.clearLineCache()); // theme/mode change: cached transcript lines re-render in the new colours
746
+ const themeProbe = installBackgroundThemeProbe({ configManager, isTTY: Boolean(stdout.isTTY), writeQuery: (b) => allowTerminalWrite(() => stdout.write(b)), requestRepaint: () => { compositor.resetDiff(); render(); }, probePalette: true, forwardInput: (b) => routeInput(b), subscribeResize: (l) => { stdout.on('resize', l); } });
735
747
 
736
748
  // continueRecovery lets --continue/bare --resume check the target session for a live crash snapshot newer than its store before resuming (see tui-startup.ts).
737
- applyInitialTuiCliState({ cli, input, commandRegistry, commandContext, shellPaths: ctx.services.shellPaths, surface: ctx.services.surface, render, continueRecovery: { sessionManager: ctx.services.sessionManager, runtime, conversation, writeLastSessionPointer, receipt: (line) => systemMessageRouter.userReceipt(line) } });
749
+ const typeaheadHooks = { onModalShown: () => typeahead.arm(), onFlowSettled: () => { const held = typeahead.release(); if (held.length > 0) routeInput(held); } };
750
+ applyInitialTuiCliState({ cli, input, commandRegistry, commandContext, shellPaths: ctx.services.shellPaths, surface: ctx.services.surface, render, continueRecovery: { sessionManager: ctx.services.sessionManager, runtime, conversation, writeLastSessionPointer, receipt: (line) => systemMessageRouter.userReceipt(line), typeahead: typeaheadHooks } });
738
751
 
739
- stdin.on('data', (raw: string) => {
740
- const data = themeProbe.filterInput(raw); if (data.length === 0) return;
752
+ const routeInput = (data: string): void => {
741
753
  const blocking = handleBlockingShellInput({
742
754
  data, pendingPermission, render,
743
755
  abortTurn: () => orchestrator.abort(),
@@ -760,7 +772,8 @@ async function main() {
760
772
  }
761
773
 
762
774
  input.feed(data);
763
- });
775
+ };
776
+ stdin.on('data', (raw: string) => { const data = typeahead.filter(themeProbe.filterInput(raw)); if (data.length > 0) routeInput(data); });
764
777
  process.on('SIGINT', sigintHandler); process.on('unhandledRejection', unhandledRejectionHandler); stdout.on('resize', resizeHandler);
765
778
 
766
779
  // State restores happen ONLY when the user explicitly asks, a CLI flag
@@ -769,16 +782,13 @@ async function main() {
769
782
  // deliberately no unconditional auto-restore here: a bare launch never
770
783
  // loads a saved conversation on its own (owner ruling).
771
784
 
772
- // Initial render
773
- conversation.rebuildHistory();
774
- render();
775
-
776
- // Crash-recovery snapshot: asked, never assumed. Scheduled after the first
777
- // frame so the question is drawn rather than posed at a blank terminal.
778
- scheduleRecoveryOffer(buildRecoveryOfferWiring({
785
+ // Crash-recovery snapshot: asked, never assumed, and opened before the first frame so the composer is never live under it;
786
+ // keys typed before it was painted are held and replayed into the composer once it is answered (startup-typeahead-gate.ts).
787
+ void startRecoveryOffer(buildRecoveryOfferWiring({
779
788
  surface: ctx.services.surface, sessionManager: ctx.services.sessionManager, runtime, conversation, commandContext,
780
- writeLastSessionPointer, receipt: (line) => systemMessageRouter.userReceipt(line), render,
789
+ writeLastSessionPointer, receipt: (line) => systemMessageRouter.userReceipt(line), render, typeahead: typeaheadHooks,
781
790
  }));
791
+ conversation.rebuildHistory(); render(); // initial render
782
792
 
783
793
  // Auto-save to recovery file every 60s + multi-instance liveness-marker
784
794
  // refresh, see runtime/recovery-autosave.ts.