@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
@@ -1,28 +1,18 @@
1
- import { type Line, type Cell, createEmptyLine, createEmptyCell } from '@pellux/goodvibes-sdk/platform/types';
2
- import { LAYOUT } from './layout.ts';
1
+ import { type Line, type Cell, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
3
2
  import { VERSION } from '../version.ts';
4
- import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText, interpolateColor, joinPrioritizedSegments, type PrioritizedSegment } from '../utils/terminal-width.ts';
3
+ import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
5
4
  import type { GitHeaderInfo } from './git-status.ts';
6
5
  import { renderHeaderLine } from './header-line.ts';
7
- import { renderConversationFragment, renderConversationStatusLine, type ConversationStatusSegment } from './conversation-surface.ts';
8
- import { GLYPHS } from './ui-primitives.ts';
9
- import { activeUiTones } from './theme.ts';
10
- import { formatElapsed } from '../utils/format-elapsed.ts';
11
- import { abbreviateCount } from '../utils/format-number.ts';
12
- import { computeContextUsage } from '../core/context-usage.ts';
13
- import { permissionModeLabel, permissionModeTone } from '../core/permission-mode.ts';
14
- import { SLEEP_DISABLED_CHIP } from '../core/power-status.ts';
6
+ import { renderConversationFragment } from './conversation-surface.ts';
7
+ import { activeTheme, activeTokens, activeUiTones } from './theme.ts';
15
8
  import { renderQueuedMessageList, renderMemoryProvenanceChip, type MemoryProvenanceEntry } from './composer-fragments.ts';
16
- import { calcSessionCost, isModelPriced } from '@pellux/goodvibes-sdk/platform/providers';
17
- import { buildFooterTip, isAgentActive } from './footer-tips.ts';
9
+ import { renderUserMessage } from './user-message.ts';
18
10
  import type { StreamMetrics } from '../core/stream-event-wiring.ts';
19
11
  import { waitingPhrase, type WaitingState } from '@pellux/goodvibes-sdk/platform/presentation';
20
12
 
21
- /** Number of frames before the animated gradient completes one full cycle. */
22
- const GRADIENT_CYCLE_FRAMES = 50;
23
13
  /**
24
14
  * Ms since the last STREAM_DELTA before the whimsical phrase rotation freezes
25
- * and createThinkingFragment shows an honest "stalled Ns" / "reconnecting"
15
+ * and the status line shows an honest "stalled Ns" / "reconnecting"
26
16
  * label instead. Deliberately much shorter than the 30s stream-stall-watchdog
27
17
  * hint threshold (stream-stall-watchdog.ts), that threshold gates a
28
18
  * low-priority system message about a likely-dead connection; this one gates
@@ -43,58 +33,32 @@ export interface ThinkingStallInfo {
43
33
  readonly reconnect?: { readonly attempt: number; readonly maxAttempts: number };
44
34
  }
45
35
 
46
- /** Format a number: up to 999, then 1.0k, 1.0M, 1.0B, 1.0T */
47
- function fmtNum(n: number): string {
48
- return abbreviateCount(n, { bSuffix: true });
49
- }
50
-
51
- /** Format a running USD cost estimate with a precision that suits its magnitude. */
52
- function fmtCost(usd: number): string {
53
- if (!(usd > 0)) return '0.00';
54
- if (usd < 0.01) return usd.toFixed(4);
55
- if (usd < 1) return usd.toFixed(3);
56
- return usd.toFixed(2);
57
- }
58
-
59
36
  /**
60
37
  * UIFactory - Generates standard UI fragments without needing Ink/React overhead.
61
38
  */
62
39
  export class UIFactory {
63
40
  /**
64
- * The header row. Delegates to renderHeaderLine (header-line.ts), extracted
65
- * when the header gained the failover divergence marker and this file was
66
- * already on the 800-line source gate.
67
- *
68
- * `model`/`provider` are the SERVING backend and `note` the divergence
69
- * marker, both resolved by core/active-model-identity.ts so the header and
70
- * the footer answer "who is serving?" from one place. `version` defaults to
71
- * the live build VERSION; tests pass a pinned fixture so golden snapshots
72
- * don't break on every release bump.
41
+ * The header row, see header-line.ts. `model` is the SERVING backend
42
+ * resolved by core/active-model-identity.ts. `version` defaults to the live
43
+ * build VERSION; tests pass a pinned fixture so golden snapshots don't break
44
+ * on every release bump. `modelNote` is the failover marker drawn after the
45
+ * model while serving differs from the configured selection.
73
46
  */
74
- public static createHeader(width: number, model: string, provider: string, title?: string, gitInfo?: GitHeaderInfo, version: string = VERSION, note: string = ''): Line[] {
75
- return renderHeaderLine(width, model, provider, title, gitInfo, version, note);
47
+ public static createHeader(width: number, model: string, title?: string, gitInfo?: GitHeaderInfo, version: string = VERSION, modelNote?: string): Line[] {
48
+ return renderHeaderLine(width, model, title, gitInfo, version, modelNote);
76
49
  }
77
50
 
78
51
  /**
79
- * createMessageBar - Renders a historical user message.
80
- * Logic: Calculates the longest line to create a "hugging" block.
52
+ * createMessageBar, a sent user message: a full-width surface fill with the
53
+ * secondary bar (user-message.ts). A cancelled message passes its own fill,
54
+ * error bar and strikethrough.
81
55
  */
82
56
  public static createMessageBar(
83
57
  width: number, text: string,
84
- bgColor: string = '#2a2a2a', textColor: string = activeUiTones().fg.secondary, prefixStr: string = ' › ',
85
- strikethrough = false
58
+ bgColor: string = activeTokens().backgroundPanel, textColor: string = activeTokens().text, barColor: string = activeTokens().secondary,
59
+ strikethrough = false,
86
60
  ): Line[] {
87
- // A historical user-message pill: it carries its own dark bodyBg, so its fg
88
- // reads fg.secondary/state.reasoning (light-on-dark), this is conversation
89
- // content, not the transparent-terminal chrome, so it is not part of the
90
- // light-terminal chrome flip.
91
- return renderConversationFragment(text, width, {
92
- prefix: prefixStr,
93
- prefixFg: activeUiTones().state.reasoning,
94
- text: textColor,
95
- bodyBg: bgColor,
96
- strikethrough,
97
- });
61
+ return renderUserMessage(text, width, { bar: barColor, bg: bgColor, text: textColor, strikethrough });
98
62
  }
99
63
 
100
64
  /**
@@ -106,8 +70,7 @@ export class UIFactory {
106
70
  prefix: ' (...) ',
107
71
  prefixFg: t.state.reasoning,
108
72
  text: t.fg.dim,
109
- bodyBg: '#1a1a1a',
110
- dim: true,
73
+ bodyBg: activeTheme().collapsedBodyBg,
111
74
  });
112
75
  }
113
76
 
@@ -121,389 +84,11 @@ export class UIFactory {
121
84
  return renderMemoryProvenanceChip(width, count, entries, expanded);
122
85
  }
123
86
 
124
- public static createFooter(
125
- width: number,
126
- prompt: string,
127
- usage: { up: number; down: number; max?: number; fleetCostUsd?: number | null },
128
- showExitNotice: boolean,
129
- lastCopyTime: number,
130
- model?: string,
131
- toolCount?: number,
132
- cursorPos?: number,
133
- workingDir?: string,
134
- provider?: string,
135
- contextWindow?: number,
136
- compactThreshold?: number,
137
- dangerMode?: boolean,
138
- lastInputTokens?: number,
139
- commandArgsHint?: string,
140
- hitlMode?: string,
141
- promptFocused: boolean = true,
142
- composerMode?: string,
143
- composerStatus?: string,
144
- composerFlags?: readonly string[],
145
- composerPendingRisk?: 'none' | 'approval-wait' | 'shell' | 'command' | 'remote',
146
- compact: boolean = false,
147
- // Honest cross-surface spine posture. Defined ONLY in adopted-daemon
148
- // mode (undefined in local-only mode), so the segment is absent otherwise.
149
- sessionSpineStatus?: 'online' | 'offline',
150
- // Session permission mode (config value). Rendered as a leading pill in the
151
- // posture block; undefined suppresses the pill (e.g. bare test callers).
152
- permissionMode?: string,
153
- // The web surface's reachable URL, when that surface is enabled, a
154
- // persistent low-priority context segment; undefined suppresses it.
155
- webSurfaceUrl?: string,
156
- // True while power.keepAwake holds, renders the always-visible
157
- // "sleep disabled" chip in the posture block (the danger-mode idiom: a
158
- // persistent, colored surface indicator, the safety mechanism itself).
159
- powerKeepAwake?: boolean,
160
- // Divergence marker for the model segment (core/active-model-identity.ts):
161
- // set only while the serving backend is not the user's configured
162
- // selection, e.g. "failover from abacusai:route-llm". Empty/undefined
163
- // renders the context-info line exactly as it did before the marker existed.
164
- modelNote?: string,
165
- ): Line[] {
166
- const lines: Line[] = [];
167
- const promptLines = prompt.split('\n');
168
- const TEXT_COLOR = promptFocused ? '252' : '246';
169
- const BG_COLOR = promptFocused ? '#2a2a2a' : '#1f2430';
170
- const BORDER_COLOR = BG_COLOR;
171
- const boxMargin = 2; const boxWidth = width - (boxMargin * 2); const boxStartX = boxMargin;
172
- const createBaseLine = () => {
173
- const l = createEmptyLine(width);
174
- for (let x = 0; x < width; x++) l[x].bg = '';
175
- return l;
176
- };
177
- const topLine = createBaseLine();
178
- for (let x = 0; x < boxWidth; x++) topLine[boxStartX + x] = { char: GLYPHS.surface.top, fg: BORDER_COLOR, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
179
- lines.push(topLine);
180
- promptLines.forEach((text, i) => {
181
- const contentW = boxWidth - 4;
182
- const prefix = i === 0 ? ' › ' : ' ';
183
- // Render text without cursor insertion, cursor is overlaid after
184
- const rawText = `${prefix}${text}`;
185
- const paddedText = fitDisplay(rawText, contentW);
186
- const contentLine = createBaseLine();
187
- for (let x = 0; x < boxWidth; x++) {
188
- const char = (x >= 2 && x < boxWidth - 2) ? paddedText[x - 2] || ' ' : ' ';
189
- contentLine[boxStartX + x] = {
190
- char,
191
- fg: (x < 5 && i === 0) ? (promptFocused ? '135' : '244') : TEXT_COLOR,
192
- bg: BG_COLOR,
193
- bold: false,
194
- dim: !promptFocused,
195
- underline: false,
196
- italic: false,
197
- strikethrough: false,
198
- };
199
- }
200
-
201
- // Overlay cursor only while the prompt owns focus.
202
- if (promptFocused && cursorPos !== undefined) {
203
- let lineStart = 0;
204
- for (let li = 0; li < i; li++) lineStart += promptLines[li].length + 1;
205
- const posInLine = cursorPos - lineStart;
206
- if (posInLine >= 0 && posInLine <= text.length) {
207
- // Cursor column in cell coordinates: prefix width (3) + posInLine + box padding (2)
208
- const cursorX = boxStartX + 2 + prefix.length + posInLine;
209
- if (cursorX < boxStartX + boxWidth - 2) {
210
- const cell = contentLine[cursorX];
211
- // Invert: bright fg on the text bg, swap to make cursor visible
212
- contentLine[cursorX] = {
213
- char: cell.char === ' ' ? GLYPHS.surface.cursor : cell.char,
214
- fg: cell.char === ' ' ? '252' : '#000000',
215
- bg: cell.char === ' ' ? (promptFocused ? BG_COLOR : '#334155') : '#ffffff',
216
- bold: false, dim: false, underline: false, italic: false, strikethrough: false
217
- };
218
- }
219
- }
220
- } else if (promptFocused && i === promptLines.length - 1) {
221
- // No cursorPos provided, show block at end (fallback)
222
- const endX = boxStartX + 2 + prefix.length + text.length;
223
- if (endX < boxStartX + boxWidth - 2) {
224
- contentLine[endX] = { char: GLYPHS.surface.cursor, fg: '252', bg: promptFocused ? BG_COLOR : '#334155', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
225
- }
226
- }
227
-
228
- // focus visibility (item 1c): when the panel workspace owns focus
229
- // and the composer is empty, name the state and the way back, the
230
- // dimmed prompt alone (TEXT_COLOR/BG_COLOR above) told you nothing was
231
- // wrong, but not why keystrokes weren't landing here.
232
- if (!promptFocused && text === '' && i === 0) {
233
- const hintText = 'panel focused: Esc returns';
234
- const hintStartX = boxStartX + 2 + prefix.length;
235
- let hx = hintStartX;
236
- for (const ch of hintText) {
237
- if (hx >= boxStartX + boxWidth - 2) break;
238
- contentLine[hx] = { char: ch, fg: '244', bg: BG_COLOR, bold: false, dim: true, underline: false, italic: false, strikethrough: false };
239
- hx++;
240
- }
241
- }
242
-
243
- // Overlay args hint: dim grey text after cursor on the last prompt line.
244
- // Only shown when a commandArgsHint is provided and cursor is at the end of input.
245
- if (commandArgsHint && i === promptLines.length - 1) {
246
- // Determine where the cursor sits on this line
247
- let cursorColOnLine: number;
248
- if (cursorPos !== undefined) {
249
- let lineStart = 0;
250
- for (let li = 0; li < i; li++) lineStart += promptLines[li].length + 1;
251
- cursorColOnLine = cursorPos - lineStart;
252
- } else {
253
- cursorColOnLine = text.length;
254
- }
255
- // Only show hint when cursor is at end of the last line (no args typed yet)
256
- if (cursorColOnLine >= text.length) {
257
- // Hint starts one cell after the cursor block
258
- const hintStartX = boxStartX + 2 + prefix.length + text.length + 1;
259
- const hintLimit = boxStartX + boxWidth - 2; // exclusive right bound
260
- const avail = Math.max(0, hintLimit - hintStartX);
261
- // Clamp to the available width with an ellipsis so a long arg-spec
262
- // (e.g. /marketplace) ends in "…" rather than a hard mid-word cut. (5b)
263
- let hintText = ' ' + commandArgsHint;
264
- if (hintText.length > avail) {
265
- hintText = avail > 1 ? `${hintText.slice(0, avail - 1)}…` : hintText.slice(0, Math.max(0, avail));
266
- }
267
- let hx = hintStartX;
268
- for (const ch of hintText) {
269
- if (hx >= hintLimit) break;
270
- contentLine[hx] = { char: ch, fg: '238', bg: BG_COLOR, bold: false, dim: true, underline: false, italic: false, strikethrough: false };
271
- hx++;
272
- }
273
- }
274
- }
275
-
276
- lines.push(contentLine);
277
- });
278
- const bottomLine = createBaseLine();
279
- for (let x = 0; x < boxWidth; x++) bottomLine[boxStartX + x] = { char: GLYPHS.surface.bottom, fg: BORDER_COLOR, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
280
- // Multi-line indicator lives inside the bottom border (right-aligned) so
281
- // the footer height stays invariant while the user adds prompt lines.
282
- if (promptLines.length > 1) {
283
- const lineCountTag = ` ${promptLines.length}L `;
284
- let tx = boxStartX + boxWidth - lineCountTag.length - 2;
285
- for (const ch of lineCountTag) {
286
- if (tx >= boxStartX + boxWidth - 1) break;
287
- bottomLine[tx] = { char: ch, fg: '244', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
288
- tx += 1;
289
- }
290
- }
291
- lines.push(bottomLine);
292
- // --- Composer posture block (mode / risk / status / flags) ------------
293
- // Suppressed in compact mode; the ctx-info line no longer duplicates these
294
- // tokens, so this block is the single home for mode/status/flags.
295
- // Footer chrome paints on the transparent terminal bg → chrome.* accents
296
- // (light-terminal-aware); state.info reads on both light and dark terminals.
297
- const t = activeUiTones();
298
- const composerTokens: Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> = [];
299
- // Permission-mode pill, always shown so the active session posture (normal
300
- // / plan / accept-edits / auto) is visible at a glance. Tone: normal reads
301
- // dim/neutral, plan reads info (read-only, safe), accept-edits and auto read
302
- // as caution (raised autonomy). Cycled by Shift+Tab, toggled by /plan.
303
- if (permissionMode) {
304
- const modeTone = permissionModeTone(permissionMode);
305
- const modeFg = modeTone === 'caution' ? t.chrome.warn : modeTone === 'info' ? t.state.info : '244';
306
- composerTokens.push({ text: ` mode:${permissionModeLabel(permissionMode)} `, fg: modeFg, bold: modeTone !== 'neutral', dim: modeTone === 'neutral' });
307
- }
308
- // Context-usage chip, always-visible compaction-pressure indicator so the
309
- // user sees compaction approaching before it happens. Colored by proximity
310
- // to the auto-compact threshold (fraction; falls back to 0.85 when unset).
311
- if (contextWindow && contextWindow > 0) {
312
- const ctxUsage = computeContextUsage(lastInputTokens ?? 0, contextWindow);
313
- const ctxThr = compactThreshold && compactThreshold > 0 ? compactThreshold : 0.85;
314
- const ctxFg = ctxUsage.clampedRatio >= ctxThr ? t.chrome.bad : ctxUsage.clampedRatio >= ctxThr * 0.85 ? t.chrome.warn : '244';
315
- composerTokens.push({ text: ` ctx:${ctxUsage.pct}% `, fg: ctxFg, bold: ctxUsage.clampedRatio >= ctxThr, dim: ctxUsage.clampedRatio < ctxThr * 0.85 });
316
- }
317
- if (composerMode) composerTokens.push({ text: ` ${GLYPHS.status.active} ${composerMode} `, fg: t.state.info, bold: true });
318
- if (composerPendingRisk && composerPendingRisk !== 'none') {
319
- const riskColor = composerPendingRisk === 'approval-wait'
320
- ? t.chrome.warn
321
- : composerPendingRisk === 'shell'
322
- ? t.chrome.bad
323
- : composerPendingRisk === 'remote'
324
- ? t.chrome.remote
325
- : t.chrome.warn;
326
- composerTokens.push({ text: ` risk:${composerPendingRisk} `, fg: riskColor, bold: true });
327
- }
328
- // "sleep disabled" chip, always visible while keep-awake holds, in the
329
- // danger-mode idiom (a persistent warn-tone indicator; the chip is the
330
- // safety mechanism, so there is no timer and no AC-only variant).
331
- if (powerKeepAwake) composerTokens.push({ text: ` ${SLEEP_DISABLED_CHIP} `, fg: t.chrome.warn, bold: true });
332
- if (composerStatus && composerStatus !== 'idle') composerTokens.push({ text: ` state:${composerStatus} `, fg: '244', dim: true });
333
- if (composerFlags && composerFlags.length > 0) composerTokens.push({ text: ` flags:${composerFlags.join(',')} `, fg: '244', dim: true });
334
- if (!compact && composerTokens.length > 0) {
335
- const postureLine = createBaseLine();
336
- let px = 2;
337
- for (const token of composerTokens) {
338
- for (const ch of token.text) {
339
- if (px >= width) break;
340
- postureLine[px] = {
341
- char: ch,
342
- fg: token.fg,
343
- bg: '',
344
- bold: token.bold ?? false,
345
- dim: token.dim ?? false,
346
- underline: false,
347
- italic: false,
348
- strikethrough: false,
349
- };
350
- px += getDisplayWidth(ch);
351
- }
352
- if (px >= width) break;
353
- }
354
- lines.push(postureLine);
355
- } else if (compact && powerKeepAwake) {
356
- // The keep-awake chip is a safety indicator, it survives compact height
357
- // even though the rest of the posture row (mode/ctx/risk/state/flags) is
358
- // dropped at height < 30. Its own compact-surviving slot.
359
- const chipLine = createBaseLine();
360
- let cx = 2;
361
- for (const ch of ` ${SLEEP_DISABLED_CHIP} `) {
362
- if (cx >= width) break;
363
- chipLine[cx] = { char: ch, fg: t.chrome.warn, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false };
364
- cx += getDisplayWidth(ch);
365
- }
366
- lines.push(chipLine);
367
- }
368
- const isRecentlyCopied = Date.now() - lastCopyTime < 2000;
369
- // Token usage line + running ~$ cost estimate (derived from the usage
370
- // object and the active model via cost-utils).
371
- const u = usage as { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; up?: number; down?: number; fleetCostUsd?: number | null };
372
- const inp = u.input ?? u.up ?? 0;
373
- const out = u.output ?? u.down ?? 0;
374
- const cr = u.cacheRead ?? 0;
375
- const cw = u.cacheWrite ?? 0;
376
- const total = inp + out + cr + cw;
377
- const tokenSep = ` ${GLYPHS.navigation.pipeSeparator} `;
378
- // 'n/a' (not 'unpriced') to stay compact in the single-line footer and
379
- // match the existing "no priceable data" convention used elsewhere
380
- // (cockpit-panel formatCost, agent-inspector-shared), the footer has no
381
- // room for a longer marker before truncation kicks in.
382
- const mainCostStr = model
383
- ? isModelPriced(model)
384
- ? `~$${fmtCost(calcSessionCost(inp, out, cr, cw, model))}`
385
- : '~n/a'
386
- : null;
387
- // Honest total = your main session + the delegated fleet. We show a SPLIT
388
- // ("you ~$X · fleet ~$Y") rather than one summed figure: the two costs are
389
- // tracked and attributed to different actors, and the split directly answers
390
- // "where did the money go", the exact confusion in the eval where the footer
391
- // showed only the main session (~$0.046) while the fleet cost ~10x more
392
- // ($0.446). The fleet segment appears only when there is a real fleet cost, so
393
- // the idle single-session footer is unchanged.
394
- const fleetCost = u.fleetCostUsd;
395
- const hasFleetCost = typeof fleetCost === 'number' && fleetCost > 0;
396
- const costSegment = hasFleetCost
397
- ? `${tokenSep}you ${mainCostStr ?? '~n/a'} · fleet ~$${fmtCost(fleetCost)}`
398
- : mainCostStr
399
- ? `${tokenSep}${mainCostStr}`
400
- : '';
401
- // Input tokens are unknown until the first assistant usage lands (often the
402
- // first tool turn). Show "—" rather than a false 0 during that window. (5c)
403
- const inpDisplay = inp > 0 ? fmtNum(inp) : '—';
404
- const tokenLine = ` Token Usage [ Input: ${inpDisplay}${tokenSep}Output: ${fmtNum(out)}${tokenSep}Cache Read: ${fmtNum(cr)}${tokenSep}Cache Write: ${fmtNum(cw)}${tokenSep}Total: ${fmtNum(total)}${costSegment} ]`;
405
- const copiedNotice = isRecentlyCopied ? ` [COPIED] ` : '';
406
- const statsLine = ' ' + tokenLine + ' '.repeat(Math.max(0, width - 4 - getDisplayWidth(tokenLine) - getDisplayWidth(copiedNotice))) + copiedNotice;
407
- lines.push(this.stringToLine(statsLine, width, { fg: isRecentlyCopied ? '81' : '244', bold: isRecentlyCopied }));
408
- // Context usage progress bar, suppressed in compact mode.
409
- if (!compact && contextWindow && contextWindow > 0) {
410
- const ctxTokens = lastInputTokens ?? 0;
411
- const label = ' Context Usage: ';
412
- // "—" until the first input-token count is known, not a false 0. (5c)
413
- const ctxDisplay = ctxTokens > 0 ? fmtNum(ctxTokens) : '—';
414
- const suffix = ` [ ${ctxDisplay} / ${fmtNum(contextWindow)} ]`;
415
- const barWidth = Math.max(10, Math.min(30, width - getDisplayWidth(label) - getDisplayWidth(suffix) - 8));
416
- const ctxPct = computeContextUsage(ctxTokens, contextWindow).clampedRatio;
417
- // Clamp threshold to [0..1]; undefined/0 means no threshold marker.
418
- const thresholdFraction = (compactThreshold !== undefined && compactThreshold > 0)
419
- ? Math.min(1, compactThreshold)
420
- : undefined;
421
- lines.push(this.createProgressBarLine(label, ctxPct, barWidth, width, suffix, thresholdFraction));
422
- }
423
- // Context info line (working dir, model+provider, tools, hitl).
424
- // Suppressed in compact mode. mode/status/flags are intentionally omitted,
425
- // the posture block above owns them, so they are not duplicated here.
426
- if (!compact && (workingDir || model)) {
427
- const home = typeof process !== 'undefined' ? process.env.HOME ?? '' : '';
428
- const displayDir = workingDir && home && workingDir.startsWith(home)
429
- ? '~' + workingDir.slice(home.length)
430
- : workingDir ?? '';
431
- // Segments carry a survival priority (lower = survives longer) so that
432
- // under width pressure whole low-value segments are dropped before any
433
- // segment is character-truncated mid-word. cwd/model are the essential
434
- // orientation segments (priority 0); the spine liveness marker is the
435
- // daemon-honesty signal and must outlive the two decorative segments
436
- // (tool count, notify mode), it is ordered ahead of them too, so a
437
- // narrow line drops "N tools"/"notify:x" whole before spine is at risk.
438
- const ctxParts: PrioritizedSegment[] = [];
439
- if (displayDir) ctxParts.push({ text: displayDir, priority: 0 });
440
- if (model) {
441
- ctxParts.push({ text: model + (provider ? ` (${provider})` : ''), priority: 0 });
442
- }
443
- // Divergence marker, its own segment so a narrow line drops it WHOLE
444
- // (joinPrioritizedSegments breaks priority ties toward the later
445
- // segment) rather than truncating it into a half-word. Priority 0: it
446
- // outranks every decorative segment, and what survives without it is
447
- // still the SERVING backend, never a claim about the configured one.
448
- if (modelNote) ctxParts.push({ text: modelNote, priority: 0 });
449
- // Cross-surface spine posture, plain words, no blame. Adopted mode only.
450
- if (sessionSpineStatus) ctxParts.push({ text: `spine:${sessionSpineStatus}`, priority: 1 });
451
- // Web surface reachability, plain, persistent; dropped first under width pressure.
452
- if (webSurfaceUrl) ctxParts.push({ text: `web:${webSurfaceUrl}`, priority: 2 });
453
- if (toolCount) ctxParts.push({ text: `${toolCount} tools`, priority: 2 });
454
- // Labeled "notify" (not "hitl"), /mode (aliased /hitl) governs UX
455
- // notification verbosity (quiet/balanced/operator), not tool
456
- // auto-approval, so it must not share vocabulary with the DANGER MODE
457
- // risk banner rendered a few lines below. Lowest priority: dropped first.
458
- if (hitlMode) ctxParts.push({ text: `notify:${hitlMode}`, priority: 3 });
459
- const sep = ` ${GLYPHS.navigation.pipeSeparator} `;
460
- const ctxBody = joinPrioritizedSegments(ctxParts, sep, width - 3);
461
- const ctxLine = ' ' + ctxBody;
462
- lines.push(this.stringToLine(ctxLine, width, { fg: '240', dim: true }));
463
- }
464
- if (showExitNotice) {
465
- const notice = ` !!! Press Ctrl+C again to exit !!! `;
466
- lines.push(this.stringToLine(fitDisplay(notice, width), width, { fg: '196', bold: true }));
467
- } else {
468
- // Persistent discoverability tip. Rotates by context (agent-aware): the
469
- // process-monitor tip leads while a turn is in flight. See footer-tips.ts.
470
- const help = ` ${buildFooterTip({ agentActive: isAgentActive(composerStatus) })} `;
471
- const dangerWarn = dangerMode ? `! DANGER MODE - ALL CHANGES AUTO-APPROVED ` : '';
472
- const helpW = getDisplayWidth(help);
473
- const dangerW = getDisplayWidth(dangerWarn);
474
- const spacerW = Math.max(0, width - helpW - dangerW);
475
- const combinedLine = help + ' '.repeat(spacerW) + dangerWarn;
476
- const line = this.stringToLine(truncateDisplay(combinedLine, width), width, { fg: '240', dim: true });
477
- // Overlay the danger warning in red bold
478
- if (dangerMode && dangerW > 0) {
479
- let col = helpW + spacerW;
480
- for (const ch of dangerWarn) {
481
- if (col >= width) break;
482
- const cw = getDisplayWidth(ch);
483
- line[col] = { char: ch, fg: t.chrome.bad, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false };
484
- if (cw === 2 && col + 1 < width) line[col + 1] = { ...line[col], char: '' };
485
- col += cw;
486
- }
487
- }
488
- lines.push(line);
489
- }
490
- return lines;
491
- }
492
-
493
- // The rotating "thinking" phrase pool and the honest waiting-state
494
- // wording (approval/reconnecting/pre-first-token/stalled/thinking) are no
495
- // longer minted here. They come from the SDK presentation contract's
496
- // waitingPhrase() (@pellux/goodvibes-sdk/platform/presentation, already
497
- // adopted by the agent), see createThinkingFragment
498
- // below. This renderer still decides WHICH state applies from its own
499
- // stall/reconnect/approval signals (computeStallInfo/computeRenderStallInfo
500
- // stay renderer-local per the extraction decision record); only the exact
501
- // wording is shared.
502
-
503
- // Gradient colors for thinking text, cyan→purple in dark, teal→purple in
504
- // light (matches splash/brand). Read live per render inside
505
- // createThinkingFragment via activeUiTones() rather than baked into a static
506
- // field, so a mode flip re-resolves without a module reload.
87
+ // The waiting-state wording (approval/reconnecting/pre-first-token/stalled/
88
+ // thinking) comes from the SDK presentation contract's waitingPhrase(),
89
+ // shared with the agent. This renderer still decides WHICH state applies
90
+ // from its own stall/reconnect/approval signals (computeStallInfo /
91
+ // computeRenderStallInfo below, see busyPhrase).
507
92
 
508
93
  /**
509
94
  * Per-frame stall info from stream metrics, computed from lastDeltaAtMs every render (not
@@ -524,7 +109,7 @@ export class UIFactory {
524
109
  * executing. lastDeltaAtMs only tracks STREAM_START/STREAM_DELTA and is
525
110
  * never advanced during tool execution (the model isn't producing tokens
526
111
  * then), so without this gate any tool call longer than
527
- * THINKING_STALL_FREEZE_MS would make the thinking fragment print
112
+ * THINKING_STALL_FREEZE_MS would make the status line print
528
113
  * "Stalled Ns..." directly above the ticking "executing (Ns)" tool row, a
529
114
  * false positive during ordinary tool execution (see stream-event-wiring.ts
530
115
  * TOOL_EXECUTING/TOOL_SUCCEEDED/TOOL_FAILED/TOOL_CANCELLED handlers).
@@ -542,17 +127,15 @@ export class UIFactory {
542
127
  : undefined;
543
128
  }
544
129
 
545
- public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number, elapsedMs?: number, ttftMs?: number, stallInfo?: ThinkingStallInfo, approvalPending?: boolean): Line[] {
546
- // Live thinking row paints on the transparent terminal bg → read the
547
- // mode-resolved chrome tones per render (gradient/brand/tool accents flip).
548
- const tones = activeUiTones();
549
- // Freeze the whimsical phrase rotation once real silence has gone on
550
- // long enough to be misleading (THINKING_STALL_FREEZE_MS). Decide WHICH
551
- // honest waiting state applies (renderer-local, this signal computation
552
- // stays here per the extraction decision record), then defer the exact
553
- // wording to the SDK presentation contract's waitingPhrase() (which
554
- // mirrors the agent's adoption). Precedence matches the contract:
555
- // approval > reconnecting > pre-first-token > stalled > thinking.
130
+ /**
131
+ * The status line's waiting phrase for a running turn. Decides WHICH honest
132
+ * waiting state applies (renderer-local signals), then takes the exact
133
+ * wording from the SDK presentation contract's waitingPhrase(). Precedence:
134
+ * approval > reconnecting > pre-first-token > stalled > thinking. The
135
+ * whimsical rotation freezes once real silence has lasted long enough to be
136
+ * misleading (THINKING_STALL_FREEZE_MS).
137
+ */
138
+ public static busyPhrase(frame: number, outputTokens?: number, stallInfo?: ThinkingStallInfo, approvalPending?: boolean): string {
556
139
  const isStalled = stallInfo !== undefined && stallInfo.msSinceLastDelta >= THINKING_STALL_FREEZE_MS;
557
140
  let state: WaitingState;
558
141
  if (approvalPending) state = 'approval';
@@ -560,163 +143,17 @@ export class UIFactory {
560
143
  else if (isStalled && (outputTokens ?? 0) === 0) state = 'pre-first-token';
561
144
  else if (isStalled) state = 'stalled';
562
145
  else state = 'thinking';
563
- const phrase = waitingPhrase(state, {
146
+ return waitingPhrase(state, {
564
147
  reconnectAttempt: stallInfo?.reconnect?.attempt,
565
148
  reconnectMaxAttempts: stallInfo?.reconnect?.maxAttempts,
566
149
  msSinceLastDelta: stallInfo?.msSinceLastDelta,
567
150
  frame,
568
151
  });
569
- // Token-rate and time-to-first-token readouts are meaningless while waiting on the user, and a
570
- // "tok/s" figure next to an approval prompt reads as if the model were still working, suppress
571
- // them; keep the elapsed timer since "how long the approval has waited" is honest.
572
- const speedSuffix = (!approvalPending && tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
573
- const elapsedSuffix = elapsedMs !== undefined ? ` (${formatElapsed(elapsedMs)})` : '';
574
- const ttftSuffix = (!approvalPending && ttftMs !== undefined && ttftMs > 0) ? ` (first token ${formatElapsed(ttftMs)})` : '';
575
- const text = ` ${spinner} ${phrase}${speedSuffix}${elapsedSuffix}${ttftSuffix} `;
576
-
577
- const textWidth = Math.max(1, getDisplayWidth(text) - 1);
578
- const segments: ConversationStatusSegment[] = Array.from(text).map((char, index) => {
579
- const rawUnwrapped = (index / textWidth) - (frame % GRADIENT_CYCLE_FRAMES) * 0.02;
580
- const raw = ((rawUnwrapped % 1.0) + 1.0) % 1.0;
581
- const gradientPos = raw <= 0.5 ? raw * 2 : (1 - raw) * 2;
582
- return {
583
- text: char,
584
- fg: interpolateColor(tones.accent.gradientStart, tones.accent.gradientEnd, gradientPos),
585
- bold: true,
586
- };
587
- });
588
- if (inputTokens !== undefined || outputTokens !== undefined) {
589
- const inTok = inputTokens ?? 0;
590
- const outTok = outputTokens ?? 0;
591
- segments.push({ text: ` in ${fmtNum(inTok)} `, fg: '243', dim: true });
592
- segments.push({ text: `out ${fmtNum(outTok)}`, fg: tones.accent.brand });
593
- }
594
- const line = createEmptyLine(width);
595
- let col = 1;
596
- for (const segment of segments) {
597
- for (const char of segment.text) {
598
- if (col >= width) break;
599
- const charWidth = getDisplayWidth(char);
600
- if (charWidth <= 0 || col + charWidth > width) break;
601
- line[col] = {
602
- char,
603
- fg: segment.fg,
604
- bg: '',
605
- bold: segment.bold ?? false,
606
- dim: segment.dim ?? false,
607
- underline: false,
608
- italic: segment.italic ?? false,
609
- strikethrough: false,
610
- };
611
- if (charWidth === 2 && col + 1 < width) {
612
- line[col + 1] = { ...line[col], char: '' };
613
- }
614
- col += charWidth;
615
- }
616
- if (col >= width) break;
617
- }
618
-
619
- const lines: Line[] = [
620
- this.stringToLine(' '.repeat(width), width),
621
- line,
622
- ];
623
-
624
- if (toolPreview) {
625
- const previewLine = createEmptyLine(width);
626
- const label = ' tool: ';
627
- let px = 0;
628
- for (const ch of label) {
629
- if (px >= width) break;
630
- previewLine[px] = {
631
- char: ch,
632
- fg: tones.state.info,
633
- bg: '',
634
- bold: true,
635
- dim: false,
636
- underline: false,
637
- italic: false,
638
- strikethrough: false,
639
- };
640
- px += getDisplayWidth(ch);
641
- }
642
- for (const ch of toolPreview) {
643
- if (px >= width) break;
644
- const charWidth = getDisplayWidth(ch);
645
- if (charWidth <= 0 || px + charWidth > width) break;
646
- previewLine[px] = {
647
- char: ch,
648
- fg: '243',
649
- bg: '',
650
- bold: false,
651
- dim: true,
652
- underline: false,
653
- italic: false,
654
- strikethrough: false,
655
- };
656
- if (charWidth === 2 && px + 1 < width) {
657
- previewLine[px + 1] = { ...previewLine[px], char: '' };
658
- }
659
- px += charWidth;
660
- }
661
- lines.push(previewLine);
662
- }
663
-
664
- lines.push(this.stringToLine(' '.repeat(width), width));
665
- return lines;
666
152
  }
667
153
 
668
- /**
669
- * createProgressBarLine - Renders a labeled progress bar line.
670
- * @param label - Left-side label string (padded as-is)
671
- * @param pct - Fill fraction 0..1
672
- * @param barWidth - Number of bar characters
673
- * @param lineWidth - Total terminal width to slice to
674
- * @param suffix - Optional suffix appended after the percentage
675
- * @param compactThreshold - Optional fraction [0..1] at which a threshold marker is drawn
676
- * and the color switches from safe to at-threshold. When omitted, falls back to the
677
- * legacy hardcoded 0.6/0.85 thresholds.
678
- */
679
- private static createProgressBarLine(
680
- label: string,
681
- pct: number,
682
- barWidth: number,
683
- lineWidth: number,
684
- suffix?: string,
685
- compactThreshold?: number,
686
- ): Line {
687
- const pctDisplay = Math.round(pct * 100);
688
- const filled = Math.round(pct * barWidth);
689
-
690
- // Color: when compactThreshold is provided, switch at the threshold;
691
- // otherwise fall back to legacy hardcoded 0.6 (green) / 0.85 (yellow) / red.
692
- let color: string;
693
- if (compactThreshold !== undefined) {
694
- color = pct < compactThreshold ? '82' : pct < 1.0 ? '220' : '196';
695
- } else {
696
- color = pct < 0.6 ? '82' : pct < 0.85 ? '220' : '196';
697
- }
698
-
699
- // Build bar with optional threshold marker.
700
- // The marker ('▸') is placed at the threshold column, replacing an empty cell.
701
- const emptyChar = GLYPHS.meter.empty;
702
- const filledChar = GLYPHS.meter.filled;
703
- const thresholdCol = compactThreshold !== undefined
704
- ? Math.round(compactThreshold * barWidth)
705
- : -1;
706
-
707
- let bar = '';
708
- for (let i = 0; i < barWidth; i++) {
709
- if (i === thresholdCol && i >= filled) {
710
- // Threshold marker sits in the empty region
711
- bar += '▸'; // ▸
712
- } else {
713
- bar += i < filled ? filledChar : emptyChar;
714
- }
715
- }
716
-
717
- const pctStr = ` ${pctDisplay}%`;
718
- const full = label + bar + pctStr + (suffix ?? '');
719
- return this.stringToLine(truncateDisplay(full, lineWidth), lineWidth, { fg: color, dim: true });
154
+ /** The opt-in partial tool preview row shown under the transcript while a turn runs. */
155
+ public static createToolPreviewRow(width: number, preview: string): Line {
156
+ return this.stringToLine(truncateDisplay(` tool: ${preview}`, width), width, { fg: activeTokens().textFaint });
720
157
  }
721
158
 
722
159
  public static stringToLine(text: string, width: number, style: Partial<Cell> = {}): Line {