@pellux/goodvibes-tui 2.0.21 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +16 -6
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -0,0 +1,310 @@
1
+ /**
2
+ * renderAgentsModal, every agent, chain, workflow, hosted session and
3
+ * background process in one list (replaces the Fleet view and its aliases).
4
+ *
5
+ * ✦ Agents 3 running · 1 failed · you $0.25 · fleet $0.47 esc
6
+ *
7
+ * ▏Type / to filter agents 6 shown
8
+ *
9
+ * ✦ running ┌ element inset ─────────────┐
10
+ * ◐ engineer Add a maxDelayMs cap 2m · $0.12 engineer
11
+ * ◐ └ tester Edit test/retry.test.ts 48s claude-sonnet · worktree
12
+ * ✦ finished → Read src/net/retry.ts
13
+ * ✕ researcher verify: 2 tests failing ◐ Edit test/retry.test.ts
14
+ *
15
+ * ⏎ open full screen s steer x stop n new agent
16
+ *
17
+ * Levels (Esc goes back one, and never interrupts anything): the list, a
18
+ * full-height view of one agent's transcript, the host-an-agent picker, and
19
+ * the best-of-N candidate picker.
20
+ */
21
+
22
+ import type { Line } from '@pellux/goodvibes-sdk/platform/types';
23
+ import { activeTokens } from './theme.ts';
24
+ import { beginModal, finishModal, clipText, scrollCountText, searchRow, wrapLines, type KitHint, type ModalFrame, type SurfaceCanvas, type SurfaceLayer } from './surface-kit.ts';
25
+ import { drawList, type KitRow } from './surface-kit-list.ts';
26
+ import { inset, type KitInset } from './surface-kit-parts.ts';
27
+ import { drawTextBlock, splitListDetail } from './surface-kit-extra.ts';
28
+
29
+ export type AgentsTone = 'text' | 'muted' | 'faint' | 'brand' | 'success' | 'warning' | 'error';
30
+
31
+ /** A line of plain text in a tone. */
32
+ export interface AgentsText {
33
+ readonly text: string;
34
+ readonly tone: AgentsTone;
35
+ readonly bold?: boolean;
36
+ /** A marker drawn in front (→ ← ◐ …). */
37
+ readonly glyph?: string;
38
+ }
39
+
40
+ /** Content for the detail inset or the full view: text, or prebuilt lines (fleet detail, transcripts). */
41
+ export type AgentsBlock =
42
+ | { readonly kind: 'text'; readonly lines: readonly AgentsText[] }
43
+ /** Prebuilt lines for a width (the fleet's detail block). */
44
+ | { readonly kind: 'lines'; readonly build: (width: number) => readonly Line[] };
45
+
46
+ export interface AgentsDetail {
47
+ readonly title: string;
48
+ readonly meta: string;
49
+ /** Drawn from the top. */
50
+ readonly blocks: readonly AgentsBlock[];
51
+ /** The live tail, drawn from the bottom up (newest last). */
52
+ readonly tail: readonly AgentsText[];
53
+ }
54
+
55
+ /** One option in a sub-level picker (host an agent, pick a winner). */
56
+ export interface AgentsOption {
57
+ readonly label: string;
58
+ readonly detail: string;
59
+ readonly selected: boolean;
60
+ }
61
+
62
+ /** What the renderer reads from the modal. */
63
+ export interface AgentsModalView {
64
+ readonly level: 'list' | 'full' | 'picker';
65
+ readonly crumbs: readonly string[];
66
+ readonly sub: string;
67
+ readonly hints: readonly KitHint[];
68
+ readonly status: AgentsText | null;
69
+ /** An inline input being typed (steer), or null. */
70
+ readonly input: { readonly label: string; readonly draft: string } | null;
71
+ // list
72
+ readonly filter: { readonly query: string; readonly active: boolean; readonly count: string };
73
+ readonly rows: readonly KitRow[];
74
+ readonly detail: AgentsDetail | null;
75
+ // full
76
+ readonly full: {
77
+ readonly meta: string;
78
+ readonly header: readonly AgentsText[];
79
+ /** The transcript for a body of this width and height (newest at the bottom). */
80
+ readonly body: (width: number, height: number) => readonly Line[];
81
+ } | null;
82
+ // picker
83
+ readonly picker: { readonly intro: string; readonly options: readonly AgentsOption[]; readonly side: readonly AgentsText[] } | null;
84
+ readonly scrollOwner: object;
85
+ }
86
+
87
+ function toneColor(tone: AgentsTone): string {
88
+ const t = activeTokens();
89
+ switch (tone) {
90
+ case 'text': return t.text;
91
+ case 'muted': return t.textMuted;
92
+ case 'faint': return t.textFaint;
93
+ case 'brand': return t.brand;
94
+ case 'success': return t.success;
95
+ case 'warning': return t.warning;
96
+ case 'error': return t.error;
97
+ }
98
+ }
99
+
100
+ /** Copy prebuilt cells onto the canvas; cells on the terminal's own background take `bg`. */
101
+ function blitLine(canvas: SurfaceCanvas, x: number, y: number, line: Line, width: number, bg: string): void {
102
+ const row = canvas.lines[y];
103
+ if (!row) return;
104
+ for (let q = 0; q < width && q < line.length; q++) {
105
+ const cell = line[q]!;
106
+ const cx = x + q;
107
+ if (cx < 0 || cx >= canvas.width) continue;
108
+ // Never leave half of a wide glyph at the right edge.
109
+ if (q === width - 1 && cell.char && line[q + 1]?.char === '') {
110
+ row[cx] = { ...cell, char: ' ', bg: cell.bg || bg };
111
+ continue;
112
+ }
113
+ row[cx] = { ...cell, bg: cell.bg || bg };
114
+ }
115
+ }
116
+
117
+ /** Width the prebuilt detail lines are built at before they are wrapped to the inset. */
118
+ const DETAIL_BUILD_WIDTH = 400;
119
+
120
+ /**
121
+ * Wrap one prebuilt cell row to `width`: trailing blanks dropped, breaks at
122
+ * the last space that fits (a hard break only for a word longer than the
123
+ * row), continuation rows indented two past the row's own leading spaces, and
124
+ * a wide glyph never split from its continuation cell. Nothing is cut off.
125
+ */
126
+ function wrapCellLine(line: Line, width: number): Line[] {
127
+ let end = line.length;
128
+ while (end > 0 && line[end - 1]!.char === ' ') end--;
129
+ const cells = line.slice(0, end);
130
+ if (cells.length <= width) return [cells];
131
+ let lead = 0;
132
+ while (lead < cells.length && cells[lead]!.char === ' ') lead++;
133
+ const indent = Math.min(lead + 2, Math.floor(width / 3));
134
+ const blank = { ...cells[0]!, char: ' ' };
135
+ const rows: Line[] = [];
136
+ let start = 0;
137
+ let first = true;
138
+ while (start < cells.length) {
139
+ const avail = first ? width : width - indent;
140
+ const prefix: Line = first ? [] : Array.from({ length: indent }, () => ({ ...blank }));
141
+ if (cells.length - start <= avail) {
142
+ rows.push([...prefix, ...cells.slice(start)]);
143
+ break;
144
+ }
145
+ let cut = start + avail;
146
+ let space = -1;
147
+ for (let q = cut; q > start; q--) if (cells[q]?.char === ' ') { space = q; break; }
148
+ if (space > start) cut = space;
149
+ else if (cells[cut]?.char === '') cut--; // keep a wide glyph whole
150
+ rows.push([...prefix, ...cells.slice(start, cut)]);
151
+ start = cut;
152
+ while (start < cells.length && cells[start]!.char === ' ') start++;
153
+ first = false;
154
+ }
155
+ return rows;
156
+ }
157
+
158
+ /** Draw text lines (wrapped) from row y; returns the row after them. */
159
+ function drawTexts(canvas: SurfaceCanvas, x: number, y: number, width: number, lines: readonly AgentsText[], maxY: number, bg?: string): number {
160
+ let yy = y;
161
+ for (const line of lines) {
162
+ const indent = line.glyph ? 2 : 0;
163
+ const parts = wrapLines(line.text, Math.max(1, width - indent));
164
+ for (let k = 0; k < parts.length; k++) {
165
+ if (yy > maxY) return yy;
166
+ if (k === 0 && line.glyph) canvas.put(x, yy, line.glyph, { fg: toneColor(line.tone), bg });
167
+ canvas.put(x + indent, yy, parts[k]!, { fg: toneColor(line.tone), bold: line.bold, bg });
168
+ yy++;
169
+ }
170
+ }
171
+ return yy;
172
+ }
173
+
174
+ /** Rows a list of texts takes when wrapped to `width`. */
175
+ function textRows(lines: readonly AgentsText[], width: number): number {
176
+ return lines.reduce((n, line) => n + wrapLines(line.text, Math.max(1, width - (line.glyph ? 2 : 0))).length, 0);
177
+ }
178
+
179
+ function drawDetail(canvas: SurfaceCanvas, p: KitInset, detail: AgentsDetail): void {
180
+ const t = activeTokens();
181
+ const width = p.r - p.l + 1;
182
+ let y = p.top;
183
+ canvas.put(p.l, y, clipText(detail.title, width), { fg: t.text, bold: true, bg: p.bg });
184
+ y++;
185
+ if (detail.meta) y = drawTexts(canvas, p.l, y, width, [{ text: detail.meta, tone: 'faint' }], p.bottom, p.bg);
186
+ y++;
187
+ // The tail keeps its newest rows; the detail above it gets what is left.
188
+ const tailRows = Math.min(textRows(detail.tail, width), Math.max(0, p.bottom - y + 1 - 2));
189
+ const detailBottom = detail.tail.length > 0 ? p.bottom - tailRows - 1 : p.bottom;
190
+ for (const block of detail.blocks) {
191
+ if (y > detailBottom) break;
192
+ if (block.kind === 'text') y = drawTexts(canvas, p.l, y, width, block.lines, detailBottom, p.bg);
193
+ else for (const built of block.build(DETAIL_BUILD_WIDTH)) {
194
+ for (const line of wrapCellLine(built, width)) {
195
+ if (y > detailBottom) break;
196
+ blitLine(canvas, p.l, y, line, width, p.bg);
197
+ y++;
198
+ }
199
+ }
200
+ }
201
+ if (detail.tail.length === 0 || tailRows <= 0) return;
202
+ // Draw the tail into a scratch run of rows, keeping only the last tailRows.
203
+ const wrapped: AgentsText[] = [];
204
+ for (const line of detail.tail) {
205
+ const parts = wrapLines(line.text, Math.max(1, width - (line.glyph ? 2 : 0)));
206
+ parts.forEach((part, k) => wrapped.push({ text: part, tone: line.tone, bold: line.bold, glyph: k === 0 ? line.glyph : line.glyph ? ' ' : undefined }));
207
+ }
208
+ const shown = wrapped.slice(-tailRows);
209
+ let ty = p.bottom - shown.length + 1;
210
+ for (const line of shown) {
211
+ if (line.glyph && line.glyph !== ' ') canvas.put(p.l, ty, line.glyph, { fg: toneColor(line.tone), bg: p.bg });
212
+ canvas.put(p.l + (line.glyph ? 2 : 0), ty, line.text, { fg: toneColor(line.tone), bold: line.bold, bg: p.bg });
213
+ ty++;
214
+ }
215
+ }
216
+
217
+ /** The inline input and the status line, at the bottom of the body. Returns the rows they take. */
218
+ function bottomRows(view: AgentsModalView, width: number): number {
219
+ let rows = 0;
220
+ if (view.input) rows += wrapLines(`${view.input.label}${view.input.draft}`, width).length;
221
+ if (view.status) rows += wrapLines(view.status.text, width).length;
222
+ return rows > 0 ? rows + 1 : 0;
223
+ }
224
+
225
+ function drawBottom(f: ModalFrame, view: AgentsModalView): void {
226
+ const t = activeTokens();
227
+ const width = f.r - f.l + 1;
228
+ const rows = bottomRows(view, width);
229
+ if (rows === 0) return;
230
+ let y = f.bottom - rows + 2;
231
+ if (view.input) {
232
+ const parts = wrapLines(`${view.input.label}${view.input.draft}`, width);
233
+ parts.forEach((part, k) => {
234
+ const end = f.canvas.put(f.l, y, part, { fg: t.text });
235
+ if (k === parts.length - 1) f.canvas.put(end, y, '▏', { fg: t.brand });
236
+ y++;
237
+ });
238
+ }
239
+ if (view.status) y = drawTexts(f.canvas, f.l, y, width, [view.status], f.bottom);
240
+ }
241
+
242
+ function drawListLevel(f: ModalFrame, view: AgentsModalView): void {
243
+ const t = activeTokens();
244
+ searchRow(f, f.top, view.filter.query, view.filter.active ? '' : 'Type / to filter agents', view.filter.count);
245
+ const bottom = f.bottom - bottomRows(view, f.r - f.l + 1);
246
+ const top = f.top + 2;
247
+ if (view.rows.length === 0) {
248
+ drawTextBlock(f.canvas, f.l, top, f.r - f.l + 1, [{
249
+ text: view.filter.query
250
+ ? `Nothing matches "${view.filter.query}".`
251
+ : 'Nothing is running. Agents, WRFC chains, workflows, hosted sessions and background processes appear here as they start; n hosts a third-party agent. This list starts empty on each launch; every agent\'s event log stays on disk under .goodvibes/tui/sessions.',
252
+ style: { fg: t.textMuted },
253
+ }], bottom);
254
+ drawBottom(f, view);
255
+ return;
256
+ }
257
+ const split = splitListDetail(f.l, f.r, top, bottom, 0.5, 6);
258
+ const result = drawList(f.canvas, { rows: view.rows, top: split.top, bottom: split.bottom, x0: split.x0, x1: split.x1, scrollKey: { owner: view.scrollOwner, name: 'list' } });
259
+ f.hintRight = scrollCountText(result.above, result.below);
260
+ if (view.detail && split.detailH >= 4) {
261
+ const p = inset(f.canvas, split.detailX, split.detailY, split.detailW, split.detailH);
262
+ drawDetail(f.canvas, p, view.detail);
263
+ }
264
+ drawBottom(f, view);
265
+ }
266
+
267
+ function drawFullLevel(f: ModalFrame, view: AgentsModalView): void {
268
+ const full = view.full!;
269
+ const width = f.r - f.l + 1;
270
+ let y = f.top;
271
+ if (full.meta) y = drawTexts(f.canvas, f.l, y, width, [{ text: full.meta, tone: 'faint' }], f.bottom);
272
+ if (full.header.length > 0) y = drawTexts(f.canvas, f.l, y, width, full.header, f.bottom);
273
+ y++;
274
+ const bottom = f.bottom - bottomRows(view, width);
275
+ const height = bottom - y + 1;
276
+ if (height > 0) {
277
+ // The transcript is drawn by the conversation renderer at the width of the
278
+ // fill minus 2 columns per side, so its own bars and fills keep their measurements.
279
+ const bodyWidth = width + 4;
280
+ const lines = full.body(bodyWidth, height);
281
+ const start = Math.max(0, lines.length - height);
282
+ for (let k = start; k < lines.length; k++) blitLine(f.canvas, f.l - 2, y + (k - start), lines[k]!, bodyWidth, activeTokens().backgroundPanel);
283
+ }
284
+ drawBottom(f, view);
285
+ }
286
+
287
+ function drawPickerLevel(f: ModalFrame, view: AgentsModalView): void {
288
+ const t = activeTokens();
289
+ const picker = view.picker!;
290
+ const width = f.r - f.l + 1;
291
+ const y = drawTextBlock(f.canvas, f.l, f.top, width, [{ text: picker.intro, style: { fg: t.textMuted } }], f.bottom) + 1;
292
+ const bottom = f.bottom - bottomRows(view, width);
293
+ const rows: KitRow[] = picker.options.map((option) => ({ label: option.label, desc: option.detail, selected: option.selected }));
294
+ const split = picker.side.length > 0 ? splitListDetail(f.l, f.r, y, bottom, 0.5, 6) : null;
295
+ const result = drawList(f.canvas, { rows, top: y, bottom: split ? split.bottom : bottom, x0: f.l, x1: split ? split.x1 : f.r, scrollKey: { owner: view.scrollOwner, name: 'picker' } });
296
+ f.hintRight = scrollCountText(result.above, result.below);
297
+ if (split && split.detailH >= 4) {
298
+ const p = inset(f.canvas, split.detailX, split.detailY, split.detailW, split.detailH);
299
+ drawTexts(f.canvas, p.l, p.top, p.r - p.l + 1, picker.side, p.bottom, p.bg);
300
+ }
301
+ drawBottom(f, view);
302
+ }
303
+
304
+ export function renderAgentsModal(view: AgentsModalView, screenWidth: number, screenHeight: number): SurfaceLayer {
305
+ const f = beginModal(screenWidth, screenHeight, { title: 'Agents', crumbs: view.crumbs, sub: view.sub, hints: view.hints });
306
+ if (view.level === 'full' && view.full) drawFullLevel(f, view);
307
+ else if (view.level === 'picker' && view.picker) drawPickerLevel(f, view);
308
+ else drawListLevel(f, view);
309
+ return finishModal(f);
310
+ }
@@ -1,44 +1,27 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
- import { wrapWithHangingIndent } from './text-layout.ts';
4
- import type { AutocompleteEngine } from '../input/autocomplete.ts';
5
- import {
6
- createOverlayBorderLine,
7
- createOverlayBoxLayout,
8
- createOverlayContentLine,
9
- DEFAULT_OVERLAY_PALETTE,
10
- putOverlayText,
11
- } from './overlay-box.ts';
12
- import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
13
-
14
- const BORDER_FG = DEFAULT_OVERLAY_PALETTE.borderFg;
15
- const TITLE_FG = DEFAULT_OVERLAY_PALETTE.titleFg;
16
- const BODY_FG = DEFAULT_OVERLAY_PALETTE.bodyFg;
17
- const MUTED_FG = DEFAULT_OVERLAY_PALETTE.mutedFg;
18
- const SELECTED_BG = DEFAULT_OVERLAY_PALETTE.selectedBg;
1
+ /**
2
+ * renderAutocompleteOverlay, the slash-command popup.
3
+ *
4
+ * Anchored to the composer with no dimming (you are still typing): the same
5
+ * width as the composer, a ┃ bar on its left edge, the surface fill, and no
6
+ * frame or hint rows. Command names line up in one column with their
7
+ * descriptions beside them; a description that does not fit on its row is
8
+ * left out rather than cut, and the selected row always shows its full
9
+ * description (wrapped) with "tab complete" on the right. When the list is
10
+ * longer than the popup, its last row carries a muted "12 more ↓" count.
11
+ */
19
12
 
20
- interface CellStyle {
21
- fg: string;
22
- bg?: string;
23
- bold?: boolean;
24
- dim?: boolean;
25
- }
13
+ import type { Line } from '@pellux/goodvibes-sdk/platform/types';
14
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
15
+ import type { AutocompleteEngine } from '../input/autocomplete.ts';
16
+ import { activeTokens } from './theme.ts';
17
+ import { renderPopup, POPUP_BAR_X } from './surface-kit-parts.ts';
18
+ import type { KitRow } from './surface-kit-list.ts';
26
19
 
27
- function putText(line: Line, startX: number, maxWidth: number, text: string, style: CellStyle): void {
28
- putOverlayText(line, startX, maxWidth, text, style);
20
+ /** Rows the popup may use at most (it shrinks to its content). */
21
+ function popupRows(viewportHeight: number): number {
22
+ return Math.max(3, Math.min(12, Math.floor(viewportHeight * 0.5)));
29
23
  }
30
24
 
31
- /**
32
- * Render the slash command autocomplete dropdown as Line[] for overlay in the viewport.
33
- *
34
- * A row's description renders inline only when it genuinely fits beside the
35
- * command name; otherwise the FULL description wraps onto its own line(s)
36
- * below the command name, with no line cap, descriptive text is never
37
- * clipped (same fix as selection-modal-overlay.ts's modal rule). Item
38
- * visibility is row-budget aware (a wrapped description can cost more than
39
- * one physical row), always showing the selected item in full and falling
40
- * back to the existing position counter rather than losing text.
41
- */
42
25
  export function renderAutocompleteOverlay(
43
26
  autocomplete: AutocompleteEngine,
44
27
  width: number,
@@ -46,188 +29,27 @@ export function renderAutocompleteOverlay(
46
29
  ): Line[] {
47
30
  const state = autocomplete.getState();
48
31
  if (!state.active || state.results.length === 0) return [];
49
-
50
- const lines: Line[] = [];
51
- const chromeRows = 4;
52
- const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
53
- margin: 2,
54
- maxWidth: 88,
55
- chromeRows,
56
- minContentRows: 6,
57
- // Rows grow with wrapped descriptions (never clipped) rather than
58
- // staying pinned at a small constant, still bounded by the viewport.
59
- maxContentRows: Math.max(10, viewportHeight - chromeRows - 4),
32
+ const t = activeTokens();
33
+ // Text columns inside the popup: fill starts right of the bar, text 2 in.
34
+ const x0 = POPUP_BAR_X + 3;
35
+ const x1 = width - 5;
36
+ const nameW = Math.min(20, Math.max(...state.results.map((r) => getDisplayWidth(r.command.name) + 1)));
37
+ const tabHint = 'tab complete';
38
+ const rows: KitRow[] = state.results.map((result, index) => {
39
+ const selected = index === state.selectedIndex;
40
+ const name = `/${result.command.name}`;
41
+ const pad = ' '.repeat(Math.max(2, nameW + 2 - getDisplayWidth(name)));
42
+ const room = x1 - x0 + 1 - getDisplayWidth(name) - pad.length - (selected ? getDisplayWidth(tabHint) + 2 : 0);
43
+ const description = result.command.description;
44
+ const showDesc = selected || getDisplayWidth(description) <= room;
45
+ return {
46
+ spans: [
47
+ { text: name, fg: t.text, bold: selected },
48
+ ...(showDesc && description ? [{ text: `${pad}${description}`, fg: t.textMuted }] : []),
49
+ ],
50
+ right: selected ? tabHint : undefined,
51
+ selected,
52
+ };
60
53
  });
61
- const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
62
-
63
- lines.push(createOverlayBorderLine(width, layout, '┌', '─', '┐', BORDER_FG));
64
-
65
- const titleLine = createOverlayContentLine(width, layout);
66
- const titleText = ' Commands';
67
- const queryText = state.query ? `/${state.query}` : '/';
68
- const queryWidth = Math.min(Math.floor(layout.innerWidth / 2), Math.max(8, layout.innerWidth - getDisplayWidth(titleText) - 2));
69
- const leftText = fitDisplay(titleText, Math.max(0, layout.innerWidth - queryWidth));
70
- const rightText = truncateDisplay(queryText, queryWidth);
71
- putText(titleLine, layout.margin + 2, layout.innerWidth - queryWidth, leftText, { fg: TITLE_FG, bold: true });
72
- putText(
73
- titleLine,
74
- layout.margin + 2 + layout.innerWidth - queryWidth,
75
- queryWidth,
76
- fitDisplay(rightText, queryWidth),
77
- { fg: TITLE_FG, dim: true },
78
- );
79
- lines.push(titleLine);
80
-
81
- const results = state.results;
82
- const total = results.length;
83
- const rowBudget = Math.max(1, metrics.contentRows);
84
-
85
- const indicatorWidth = 2;
86
- const maxCommandWidth = Math.min(18, Math.max(10, Math.floor(layout.innerWidth * 0.28)));
87
- const gapWidth = 2;
88
- const descWidth = Math.max(0, layout.innerWidth - indicatorWidth - maxCommandWidth - gapWidth);
89
- const wrapWidth = Math.max(8, layout.innerWidth - indicatorWidth);
90
-
91
- // palette curation (item 4): on a bare '/' (query === ''), the results
92
- // list is "common tier" (score 2) followed by "alphabetical rest" (score
93
- // 1), see CommandRegistry.fuzzyMatch. commonCount marks that boundary;
94
- // a one-row separator draws there when it falls inside the visible window.
95
- const hasCommonSeparator = state.query === '' && state.commonCount > 0 && state.commonCount < total;
96
-
97
- function descriptionFitsInline(description: string): boolean {
98
- return descWidth >= 12 && getDisplayWidth(description) <= descWidth;
99
- }
100
-
101
- /** Physical rows a result occupies: 1 for the command-name row, plus one more per wrapped description line when it doesn't fit inline, plus 1 more when the common/rest separator sits immediately before it. */
102
- function rowCostFor(index: number): number {
103
- const { command } = results[index];
104
- const rows = descriptionFitsInline(command.description)
105
- ? 1
106
- : 1 + wrapWithHangingIndent(command.description, wrapWidth, '').length;
107
- const separatorCost = hasCommonSeparator && index === state.commonCount ? 1 : 0;
108
- return rows + separatorCost;
109
- }
110
-
111
- // Row-budget-aware windowing (mirrors selection-modal-overlay.ts's fix): a
112
- // window of results is chosen by their REAL rendered height rather than
113
- // assuming one row per item, always including the selected item in full,
114
- // growing outward (roughly centered) until the budget is spent.
115
- const selectedIdx = Math.max(0, Math.min(state.selectedIndex, total - 1));
116
- let startIdx = selectedIdx;
117
- let endIdx = selectedIdx + 1;
118
- let usedRows = rowCostFor(selectedIdx);
119
- let growBefore = true;
120
- while (startIdx > 0 || endIdx < total) {
121
- const canGrowBefore = growBefore && startIdx > 0;
122
- const canGrowAfter = !growBefore && endIdx < total;
123
- if (canGrowBefore) {
124
- const cost = rowCostFor(startIdx - 1);
125
- if (usedRows + cost > rowBudget) break;
126
- startIdx -= 1;
127
- usedRows += cost;
128
- } else if (canGrowAfter) {
129
- const cost = rowCostFor(endIdx);
130
- if (usedRows + cost > rowBudget) break;
131
- endIdx += 1;
132
- usedRows += cost;
133
- } else if (startIdx > 0) {
134
- const cost = rowCostFor(startIdx - 1);
135
- if (usedRows + cost > rowBudget) break;
136
- startIdx -= 1;
137
- usedRows += cost;
138
- } else if (endIdx < total) {
139
- const cost = rowCostFor(endIdx);
140
- if (usedRows + cost > rowBudget) break;
141
- endIdx += 1;
142
- usedRows += cost;
143
- } else {
144
- break;
145
- }
146
- growBefore = !growBefore;
147
- }
148
-
149
- for (let i = startIdx; i < endIdx; i++) {
150
- if (hasCommonSeparator && i === state.commonCount) {
151
- const sepLine = createOverlayContentLine(width, layout);
152
- putText(sepLine, layout.margin + 2, layout.innerWidth, '─'.repeat(layout.innerWidth), { fg: BORDER_FG, dim: true });
153
- lines.push(sepLine);
154
- }
155
-
156
- const { command } = results[i];
157
- const isSelected = i === state.selectedIndex;
158
- const bg = isSelected ? SELECTED_BG : '';
159
- const indicator = isSelected ? '▸ ' : ' ';
160
- const commandText = fitDisplay(
161
- truncateDisplay(`/${command.name}`, maxCommandWidth),
162
- maxCommandWidth,
163
- );
164
-
165
- const line = createOverlayContentLine(width, layout, BORDER_FG, bg);
166
- let x = layout.margin + 2;
167
- putText(line, x, indicatorWidth, indicator, {
168
- fg: isSelected ? TITLE_FG : MUTED_FG,
169
- bg,
170
- bold: isSelected,
171
- });
172
- x += indicatorWidth;
173
- putText(line, x, maxCommandWidth, commandText, {
174
- fg: isSelected ? TITLE_FG : BODY_FG,
175
- bg,
176
- bold: isSelected,
177
- });
178
- x += maxCommandWidth;
179
-
180
- if (descriptionFitsInline(command.description)) {
181
- putText(line, x, gapWidth, ' ', { fg: BODY_FG, bg });
182
- x += gapWidth;
183
- putText(line, x, descWidth, fitDisplay(command.description, descWidth), {
184
- fg: isSelected ? BODY_FG : MUTED_FG,
185
- bg,
186
- bold: false,
187
- });
188
- lines.push(line);
189
- } else {
190
- // Doesn't fit beside the command name, wrap the FULL description onto
191
- // its own line(s) below at the full row width, rather than clipping it.
192
- lines.push(line);
193
- for (const wrapped of wrapWithHangingIndent(command.description, wrapWidth, '')) {
194
- const descLine = createOverlayContentLine(width, layout, BORDER_FG, bg);
195
- putText(descLine, layout.margin + 2 + indicatorWidth, wrapWidth, fitDisplay(wrapped, wrapWidth), {
196
- fg: isSelected ? BODY_FG : MUTED_FG,
197
- bg,
198
- });
199
- lines.push(descLine);
200
- }
201
- }
202
- }
203
-
204
- if (startIdx > 0 || endIdx < total) {
205
- const scrollLine = createOverlayContentLine(width, layout);
206
- const scrollText = `${state.selectedIndex + 1}/${total}`;
207
- putText(
208
- scrollLine,
209
- layout.margin + 2 + Math.max(0, layout.innerWidth - getDisplayWidth(scrollText)),
210
- getDisplayWidth(scrollText),
211
- scrollText,
212
- { fg: MUTED_FG, dim: true },
213
- );
214
- lines.push(scrollLine);
215
- }
216
-
217
- const footerLine = createOverlayContentLine(width, layout);
218
- // vocab unification: "Run"/"Close" match the /help selection modal's
219
- // hint bar (selection-modal-overlay.ts), both surfaces execute a command
220
- // on Enter, so they now say so the same way.
221
- const hints = '[Tab] Complete [Up/Down] Navigate [Enter] Run [Esc] Close';
222
- putText(
223
- footerLine,
224
- layout.margin + 2,
225
- layout.innerWidth,
226
- fitDisplay(truncateDisplay(hints, layout.innerWidth), layout.innerWidth),
227
- { fg: MUTED_FG, dim: true },
228
- );
229
- lines.push(footerLine);
230
-
231
- lines.push(createOverlayBorderLine(width, layout, '└', '─', '┘', BORDER_FG));
232
- return lines;
54
+ return renderPopup({ width, rows, maxRows: popupRows(viewportHeight), scrollOwner: autocomplete }).lines;
233
55
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * back-to-bottom.ts, the pill that shows while the output is scrolled away
3
+ * from its live bottom, centered just above the input area:
4
+ *
5
+ * ▐ ↓ Back to bottom esc ▌ (drawn as one filled row)
6
+ *
7
+ * fill the element surface, 2 columns of padding inside each end so
8
+ * the text never touches the fill's edge
9
+ * ↓ the brand color, bold
10
+ * words "Back to bottom" in the text color
11
+ * keycap ` esc ` on the keycap fill, 2 columns after the words, drawn
12
+ * only while the next Esc really goes back to the bottom (an
13
+ * empty composer, the keyboard in the input area); otherwise the
14
+ * pill names the place without a key, since the status line says
15
+ * what that Esc does instead
16
+ *
17
+ * It is a fill row, so it keeps one full empty row from any text above it
18
+ * (the output, or the throbber); the input area's ▄ cap gives the half row
19
+ * below it (shell-surface.ts builds that stack). Gone at the bottom.
20
+ */
21
+
22
+ import { type Line, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
23
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
24
+ import { activeTokens } from './theme.ts';
25
+
26
+ /** What the pill shows. */
27
+ export interface BackToBottomState {
28
+ /** The next Esc goes back to the live bottom: the pill carries the esc keycap. */
29
+ readonly escKey: boolean;
30
+ }
31
+
32
+ const ARROW = '↓';
33
+ const WORDS = 'Back to bottom';
34
+ const KEY = 'esc';
35
+ /** Columns of fill between the pill's edge and its text. */
36
+ const PAD = 2;
37
+ /** Columns between the words and the keycap. */
38
+ const KEY_GAP = 2;
39
+
40
+ /** The pill's width in cells (with or without its keycap). */
41
+ function backToBottomWidth(state: BackToBottomState): number {
42
+ const words = getDisplayWidth(ARROW) + 1 + getDisplayWidth(WORDS);
43
+ return PAD + words + (state.escKey ? KEY_GAP + getDisplayWidth(KEY) + 2 : 0) + PAD;
44
+ }
45
+
46
+ /** The pill row, centered on a `width`-cell line (terminal background around it). */
47
+ export function renderBackToBottomPill(width: number, state: BackToBottomState): Line {
48
+ const t = activeTokens();
49
+ const line = createEmptyLine(width);
50
+ for (const cell of line) cell.bg = '';
51
+ // The keycap goes first when the row is short; the pill never runs past
52
+ // the screen's 3-column margins (fills from column 3 to width-3).
53
+ const withKey = state.escKey && backToBottomWidth(state) <= width - 6;
54
+ const shape: BackToBottomState = { escKey: withKey };
55
+ const pillW = Math.min(backToBottomWidth(shape), Math.max(0, width - 6));
56
+ if (pillW < PAD * 2 + 1) return line;
57
+ const x0 = Math.max(3, Math.floor((width - pillW) / 2));
58
+ const x1 = x0 + pillW; // exclusive
59
+ const bg = t.backgroundElement;
60
+ for (let x = x0; x < x1; x++) line[x] = { ...line[x]!, char: ' ', bg };
61
+ let cx = x0 + PAD;
62
+ const put = (text: string, style: { fg: string; bg: string; bold?: boolean }): void => {
63
+ for (const ch of text) {
64
+ const w = getDisplayWidth(ch);
65
+ if (w <= 0) continue;
66
+ if (cx + w > x1 - PAD) return;
67
+ line[cx] = { char: ch, fg: style.fg, bg: style.bg, bold: style.bold ?? false, dim: false, underline: false, italic: false, strikethrough: false };
68
+ if (w === 2 && cx + 1 < line.length) line[cx + 1] = { ...line[cx]!, char: '' };
69
+ cx += w;
70
+ }
71
+ };
72
+ put(ARROW, { fg: t.brand, bg, bold: true });
73
+ cx += 1;
74
+ put(WORDS, { fg: t.text, bg });
75
+ if (withKey) {
76
+ cx += KEY_GAP;
77
+ put(` ${KEY} `, { fg: t.text, bg: t.border, bold: true });
78
+ }
79
+ return line;
80
+ }