@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,104 +0,0 @@
1
- import { getSurfaceContentRows } from './surface-layout.ts';
2
-
3
- export type OverlayWidthClass = 'narrow' | 'medium' | 'wide';
4
-
5
- export function getOverlayWidthClass(viewportWidth: number): OverlayWidthClass {
6
- if (viewportWidth < 88) return 'narrow';
7
- if (viewportWidth < 120) return 'medium';
8
- return 'wide';
9
- }
10
-
11
- export interface OverlayViewportBudgetOptions {
12
- readonly chromeRows: number;
13
- readonly minContentRows?: number;
14
- readonly maxContentRows?: number;
15
- readonly targetRatio?: number;
16
- readonly minTotalRows?: number;
17
- readonly maxTotalRows?: number;
18
- }
19
-
20
- export function getOverlayContentBudget(
21
- viewportHeight: number,
22
- options: OverlayViewportBudgetOptions,
23
- ): number {
24
- const safeViewportHeight = Math.max(8, viewportHeight);
25
- const defaultTargetTotalRows = Math.max(12, Math.min(Math.max(12, safeViewportHeight - 3), Math.round(safeViewportHeight * (options.targetRatio ?? 0.45))));
26
- const minTotalRows = options.minTotalRows ?? 12;
27
- const maxTotalRows = options.maxTotalRows ?? Math.max(minTotalRows, Math.min(22, safeViewportHeight - 2));
28
- const targetTotalRows = Math.max(
29
- minTotalRows,
30
- Math.min(maxTotalRows, defaultTargetTotalRows),
31
- );
32
- return getSurfaceContentRows({
33
- viewportHeight,
34
- chromeRows: options.chromeRows,
35
- minContentRows: options.minContentRows,
36
- maxContentRows: options.maxContentRows,
37
- targetRatio: 1,
38
- minTotalRows: targetTotalRows,
39
- maxTotalRows: targetTotalRows,
40
- });
41
- }
42
-
43
- export function getOverlayMaxWidth(
44
- terminalWidth: number,
45
- margin: number,
46
- requestedMaxWidth: number,
47
- ): number {
48
- const safeRequested = Math.max(24, requestedMaxWidth);
49
- const reservedSideSpace = margin * 2;
50
- const viewportBounded = Math.max(24, terminalWidth - reservedSideSpace);
51
- return Math.min(safeRequested, viewportBounded);
52
- }
53
-
54
- export interface OverlaySurfaceMetricsOptions {
55
- readonly margin?: number;
56
- readonly maxWidth?: number;
57
- readonly chromeRows: number;
58
- readonly minContentRows?: number;
59
- readonly maxContentRows?: number;
60
- readonly targetRatio?: number;
61
- readonly minTotalRows?: number;
62
- readonly maxTotalRows?: number;
63
- }
64
-
65
- export interface OverlaySurfaceMetrics {
66
- readonly margin: number;
67
- readonly boxWidth: number;
68
- readonly contentWidth: number;
69
- readonly contentRows: number;
70
- }
71
-
72
- export function getStableOverlayContentRows(
73
- contentRows: number,
74
- minRows = 10,
75
- ): number {
76
- return Math.max(contentRows, minRows);
77
- }
78
-
79
- export function getOverlaySurfaceMetrics(
80
- viewportWidth: number,
81
- viewportHeight: number,
82
- options: OverlaySurfaceMetricsOptions,
83
- ): OverlaySurfaceMetrics {
84
- // Keep overlays comfortably inset from shell panels and avoid wide, low-information modals.
85
- const widthClass = getOverlayWidthClass(viewportWidth);
86
- const margin = options.margin ?? (widthClass === 'wide' ? 6 : 4);
87
- const defaultMaxWidth = widthClass === 'wide' ? 84 : widthClass === 'medium' ? 78 : 72;
88
- const boxWidth = getOverlayMaxWidth(viewportWidth, margin, options.maxWidth ?? defaultMaxWidth);
89
- const contentRows = getOverlayContentBudget(viewportHeight, {
90
- chromeRows: options.chromeRows,
91
- minContentRows: options.minContentRows ?? 8,
92
- maxContentRows: options.maxContentRows ?? 14,
93
- targetRatio: options.targetRatio ?? 0.45,
94
- minTotalRows: options.minTotalRows,
95
- maxTotalRows: options.maxTotalRows,
96
- });
97
-
98
- return {
99
- margin,
100
- boxWidth,
101
- contentWidth: Math.max(1, boxWidth - 4),
102
- contentRows,
103
- };
104
- }
@@ -1,145 +0,0 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import type { Panel } from '../panels/types.ts';
3
- import type { InputHandler } from '../input/handler.ts';
4
- import type { PanelManager } from '../panels/panel-manager.ts';
5
- import type { PanelCompositeData } from './compositor.ts';
6
- import { createSplitPaneLayout } from './layout-engine.ts';
7
- import { renderPanelWorkspaceBar } from './panel-workspace-bar.ts';
8
-
9
- /**
10
- * Per-panel render cache for dirty-flag skipping.
11
- *
12
- * Maintainability hazard, mid-render invalidation race:
13
- *
14
- * If an event listener (e.g. a runtime-bus subscriber) fires DURING a panel's
15
- * `render()` call and mutates state such that `needsRender = true`, the
16
- * trailing `panel.markRendered()` below will clobber that invalidation back
17
- * to `false`, causing the next frame to serve stale cached output until
18
- * something else invalidates the panel.
19
- *
20
- * In practice this is rare because `render()` is synchronous and short, and
21
- * runtime-bus listeners that mutate panel state typically run outside the
22
- * render pass. Panels that DO expect to mutate from within their own render
23
- * path (e.g. deferred lazy-load) should call `this.invalidate()` AFTER the
24
- * trailing work so the next frame picks it up.
25
- *
26
- * The fix (now applied): snapshot `needsRender` before calling `render()` and
27
- * only call `markRendered()` when no concurrent invalidation occurred during
28
- * the render pass, preserving any mid-render invalidation.
29
- */
30
- interface PanelRenderCache {
31
- lines: Line[];
32
- width: number;
33
- height: number;
34
- }
35
- const panelRenderCache = new WeakMap<Panel, PanelRenderCache>();
36
- /**
37
- * Per-panel render-generation counter. Incremented whenever invalidate() fires
38
- * on the panel (tracked here externally since Panel does not expose a version).
39
- * We monkey-patch each panel the first time it enters renderPanel() by wrapping
40
- * its invalidate() to bump the counter stored in this map.
41
- *
42
- * Race-guard: snapshot the generation before render(), compare after. If it
43
- * changed, a mid-render invalidation occurred, leave needsRender=true.
44
- */
45
- const panelRenderGen = new WeakMap<Panel, { gen: number }>();
46
-
47
- function getRenderGenState(panel: Panel): { gen: number } {
48
- let state = panelRenderGen.get(panel);
49
- if (!state) {
50
- state = { gen: 0 };
51
- panelRenderGen.set(panel, state);
52
- // Wrap invalidate() to bump generation counter.
53
- const origInvalidate = panel.invalidate.bind(panel);
54
- panel.invalidate = () => {
55
- state!.gen++;
56
- origInvalidate();
57
- };
58
- }
59
- return state;
60
- }
61
-
62
- /** Render a panel, skipping if nothing changed. Returns cached lines on a skip. */
63
- export function renderPanel(panel: Panel, width: number, height: number): Line[] {
64
- const cached = panelRenderCache.get(panel);
65
- if (cached && !panel.needsRender && cached.width === width && cached.height === height) {
66
- return cached.lines;
67
- }
68
- // Snapshot render-generation counter BEFORE calling render(). If an event
69
- // listener fires during render() and calls panel.invalidate(), the generation
70
- // counter will be bumped. We compare after render to detect mid-render races.
71
- const genState = getRenderGenState(panel);
72
- const genBefore = genState.gen;
73
- const lines = panel.render(width, height);
74
- // Only call markRendered() when no mid-render invalidation occurred.
75
- // If the generation changed during render(), a concurrent invalidate() fired,
76
- // leave needsRender=true so the next frame re-renders with the new state.
77
- if (genState.gen === genBefore) {
78
- panel.markRendered();
79
- }
80
- // If gen changed, needsRender is already true (invalidate() set it); do not
81
- // call markRendered(), the next frame will pick it up.
82
- panelRenderCache.set(panel, { lines, width, height });
83
- return lines;
84
- }
85
-
86
- export interface PanelCompositeBuildResult {
87
- readonly panelData?: PanelCompositeData;
88
- readonly panelWidth?: number;
89
- }
90
-
91
- export function buildPanelCompositeData(
92
- panelManager: PanelManager,
93
- input: InputHandler,
94
- panelWidth: number,
95
- panelHeight: number,
96
- ): PanelCompositeBuildResult {
97
- if (!panelManager.isVisible() || panelManager.getAllOpen().length === 0 || panelWidth <= 0) {
98
- return {};
99
- }
100
-
101
- const topPane = panelManager.getTopPane();
102
- const bottomPane = panelManager.getBottomPane();
103
- const focusedPane = panelManager.getFocusedPane();
104
- const workspaceTabs = panelManager.getWorkspaceTabs();
105
- const verticalSplitRatio = panelManager.getVerticalSplitRatio();
106
- const hasBottom = panelManager.isBottomPaneVisible() && bottomPane.panels.length > 0;
107
- const workspaceBar = renderPanelWorkspaceBar(workspaceTabs, panelWidth, input.panelFocused);
108
-
109
- let topContent: Line[];
110
- let bottomContent: Line[] | undefined;
111
-
112
- const topActivePanel = topPane.panels[topPane.activeIndex] ?? null;
113
-
114
- if (hasBottom) {
115
- // Single consolidated workspace bar (1 row) + h-separator (1 row); the rest
116
- // splits between the two panes' content.
117
- const paneLayout = createSplitPaneLayout(Math.max(0, panelHeight - 1), verticalSplitRatio, {
118
- topTabRows: 0,
119
- bottomTabRows: 0,
120
- });
121
- const topH = paneLayout.topContentRows;
122
- const bottomH = paneLayout.bottomContentRows;
123
- topContent = topActivePanel ? renderPanel(topActivePanel, panelWidth, topH) : [];
124
-
125
- const bottomActivePanel = bottomPane.panels[bottomPane.activeIndex] ?? null;
126
- bottomContent = bottomActivePanel ? renderPanel(bottomActivePanel, panelWidth, bottomH) : [];
127
- } else {
128
- const topH = Math.max(0, panelHeight - 1);
129
- topContent = topActivePanel ? renderPanel(topActivePanel, panelWidth, topH) : [];
130
- }
131
-
132
- return {
133
- panelData: {
134
- workspaceBar,
135
- topContent,
136
- topFocused: input.panelFocused && focusedPane === 'top',
137
- hasBottomPane: hasBottom,
138
- bottomContent,
139
- bottomFocused: input.panelFocused && focusedPane === 'bottom',
140
- separator: true,
141
- verticalSplitRatio,
142
- },
143
- panelWidth,
144
- };
145
- }
@@ -1,54 +0,0 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import type { WorkspaceTab } from '../panels/panel-manager.ts';
3
- import { renderTabStrip, type TabHitRegion } from './tab-strip.ts';
4
- import { UI_TONES } from './ui-primitives.ts';
5
-
6
- // Theme tokens (no raw hex), keeps the bar in sync with the shared palette.
7
- const ACTIVE_FG = UI_TONES.fg.primary; // selected tab text
8
- const ACTIVE_BG = UI_TONES.bg.selected; // selected tab fill (stronger contrast)
9
- const INACTIVE_FG = UI_TONES.fg.muted; // unselected tabs
10
- const SEPARATOR_FG = UI_TONES.fg.dim; // │ separators
11
- const LABEL_FG = UI_TONES.fg.secondary; // PANELS prefix label
12
- const LABEL_BG = UI_TONES.bg.title; // prefix label background when focused
13
- const FOCUS_ACCENT_FG = UI_TONES.state.active; // the one focused tab
14
-
15
- export function renderPanelWorkspaceBar(
16
- tabs: readonly WorkspaceTab[],
17
- width: number,
18
- focused: boolean,
19
- onLayout?: (regions: readonly TabHitRegion[]) => void,
20
- ): Line {
21
- return renderTabStrip({
22
- width,
23
- onLayout,
24
- tabs: tabs.map((tab, i) => ({
25
- // tab.active = selected in its own pane (drives highlighted background).
26
- // tab.focused = has keyboard focus (drives brighter text / focus indicator).
27
- // A tab can be active-but-not-focused (selected in the unfocused pane) or
28
- // active-and-focused (selected in the focused pane).
29
- //
30
- // The pane marker uses ▲/▼ (top/bottom pane). The old '^'/'v'
31
- // prefixes read as a Ctrl caret (^) implying a non-existent Ctrl+<tab>
32
- // hotkey. The REAL per-tab jump key is Alt+N (panel-tab-1..9), rendered
33
- // here as ⌥N for the first nine tabs so the shown affordance matches the
34
- // binding (tabs past nine have no Alt+N chord, so no index is shown).
35
- label: `${tab.pane === 'bottom' ? '▼' : '▲'}${i < 9 ? ` ⌥${i + 1}` : ''} ${tab.icon} ${tab.name}${tab.focused ? ' ▸' : ''}`,
36
- active: tab.active,
37
- })),
38
- prefixLabel: ' PANELS ',
39
- suffixLabel: ` ${tabs.length} `,
40
- style: {
41
- activeFg: ACTIVE_FG,
42
- activeBg: ACTIVE_BG,
43
- activeBold: focused,
44
- inactiveFg: INACTIVE_FG,
45
- separatorFg: SEPARATOR_FG,
46
- // Accent the PANELS chip when focused so it ties to the focus border.
47
- labelFg: focused ? FOCUS_ACCENT_FG : LABEL_FG,
48
- labelBg: focused ? LABEL_BG : '',
49
- labelBold: focused,
50
- overflowFg: SEPARATOR_FG,
51
- trailingFg: INACTIVE_FG,
52
- },
53
- });
54
- }
@@ -1,163 +0,0 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { UIFactory } from './ui-factory.ts';
3
- import { truncateDisplay } from '../utils/terminal-width.ts';
4
- import { GLYPHS, UI_TONES } from './ui-primitives.ts';
5
- import { activeUiTones } from './theme.ts';
6
- import { formatHints } from './hint-grammar.ts';
7
- import { voiceCaptureRowVisible, type VoiceCaptureIndicatorState } from '../core/voice-capture-status.ts';
8
-
9
- /**
10
- * renderProcessIndicator, shows a one-line summary of active background
11
- * processes below the input area.
12
- *
13
- * Dimmed when no processes are active, highlighted (cyan) when agents or
14
- * background exec processes are running. Includes an `[Enter] View` hint
15
- * (hint-grammar bracket form) when active.
16
- */
17
- /**
18
- * Agent-count label. The footer counts only ACTIVE agents, while the fleet
19
- * lists every node (running, terminal, chains, watchers). Label the count
20
- * "active" so it is never misread as a grand total, the [Enter] View hint
21
- * opens the fleet for the full picture. (item 5d.)
22
- */
23
- function agentCountLabel(agentCount: number): string {
24
- return `${agentCount} agent${agentCount !== 1 ? 's' : ''} active`;
25
- }
26
-
27
- export function renderProcessIndicator(
28
- width: number,
29
- agentCount: number,
30
- toolCount: number,
31
- focused: boolean = false,
32
- agentProgress?: string,
33
- ): Line[] {
34
- const total = agentCount + toolCount;
35
- const renderPlainStatus = (text: string, style: { fg: string; bold?: boolean; dim?: boolean }): Line[] => (
36
- [UIFactory.stringToLine(` ${text}`, width, style)]
37
- );
38
- const renderFocusedStatus = (text: string): Line[] => {
39
- const bg = '#31506f';
40
- const fg = '#eefaff';
41
- const markerFg = UI_TONES.accent.browser;
42
- const line = UIFactory.stringToLine(' '.repeat(width), width, { fg: '238' });
43
- const prefix = `${GLYPHS.navigation.selected} `;
44
- const body = truncateDisplay(text, Math.max(0, width - 8), '');
45
- const highlighted = ` ${prefix}${body} `;
46
- const startX = 2;
47
- for (let i = 0; i < highlighted.length && startX + i < width - 2; i++) {
48
- const ch = highlighted[i]!;
49
- const isMarker = i < prefix.length + 1;
50
- line[startX + i].char = ch;
51
- line[startX + i].fg = isMarker ? markerFg : fg;
52
- line[startX + i].bg = bg;
53
- line[startX + i].bold = true;
54
- line[startX + i].dim = false;
55
- }
56
- return [line];
57
- };
58
-
59
- // --- Focused state: always render before idle/active branches ---
60
- if (focused) {
61
- const parts: string[] = [];
62
- if (agentCount > 0) parts.push(agentCountLabel(agentCount));
63
- if (toolCount > 0) parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
64
- const label = total === 0
65
- ? `No background processes ${formatHints([{ key: 'Esc', verb: 'Back to input' }])}`
66
- : `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)} ${formatHints([{ key: 'Enter', verb: 'Open' }, { key: 'Esc', verb: 'Back to input' }])}`;
67
- return renderFocusedStatus(label);
68
- }
69
-
70
- if (total === 0) {
71
- return renderPlainStatus('No background processes', { fg: '238', dim: true });
72
- }
73
-
74
- // Build the label: "bg: 2 agents | Turn 3 | write - src/foo.ts"
75
- const parts: string[] = [];
76
- if (agentCount > 0) {
77
- parts.push(agentCountLabel(agentCount));
78
- }
79
- if (toolCount > 0) {
80
- parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
81
- }
82
- // Append the first running agent's progress (truncated to fit)
83
- /**
84
- * Number of columns reserved for the agent count label and hint text.
85
- * Breakdown: "bg: N agents" prefix (~15 chars) + " | " separator (~3)
86
- * + " [Enter] View " hint (~16) + padding (~9) ≈ 43 chars.
87
- */
88
- const PROGRESS_RESERVED_CHARS = 43;
89
- const progressMaxLen = Math.max(0, width - PROGRESS_RESERVED_CHARS); // reserve space for count + hint
90
- // Truncate by display width (not JS string length) so wide/CJK glyphs in the
91
- // agent progress text don't overflow the reserved budget.
92
- const progressSuffix = agentProgress && progressMaxLen > 10
93
- ? ` | ${truncateDisplay(agentProgress, progressMaxLen, '...')}`
94
- : '';
95
- const label = `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)}${progressSuffix}`;
96
- const hint = ` ${formatHints([{ key: 'Enter', verb: 'View' }])}`;
97
- return renderPlainStatus(`${label}${hint}`, { fg: activeUiTones().accent.brand, bold: true });
98
- }
99
-
100
- /**
101
- * Sentence each capture state renders as. Written out per state rather than
102
- * assembled from fragments because these are the words that tell a user whether
103
- * their microphone is open, "listening" and "recording" mean different things
104
- * and a row that blurred them would be worse than no row.
105
- */
106
- const VOICE_CAPTURE_LABELS: Record<VoiceCaptureIndicatorState['kind'], string> = {
107
- requesting: 'opening the microphone',
108
- recording: 'recording: press the voice-input key again to stop',
109
- transcribing: 'transcribing what you said',
110
- 'wake-listening': 'listening for the wake phrase',
111
- 'wake-capturing': 'wake heard: recording what follows',
112
- 'wake-restarting': 'capture stream ended: restarting',
113
- 'wake-latched': 'wake detection stopped',
114
- };
115
-
116
- /**
117
- * renderVoiceCaptureIndicator, the persistent row shown while a microphone is
118
- * open, below the input area beside the process indicator.
119
- *
120
- * It exists because a held-open capture device is otherwise invisible: wake
121
- * detection runs for as long as the feature is on, and nothing else on screen
122
- * would say so. `voice.wake.indicator` chooses between `statusline` (one dim
123
- * row), `banner` (a highlighted row that is hard to miss) and `off`; a
124
- * push-to-talk recording always renders, because the user pressed a key one
125
- * moment ago and is waiting on it.
126
- *
127
- * Returns no lines when nothing is captured, or when the wake rows are turned
128
- * off, the caller splices whatever comes back, so an empty array is "no row".
129
- */
130
- export function renderVoiceCaptureIndicator(
131
- width: number,
132
- state: VoiceCaptureIndicatorState | null,
133
- ): Line[] {
134
- if (!voiceCaptureRowVisible(state) || state === null) return [];
135
- const tones = activeUiTones();
136
- const marker = state.kind === 'wake-latched' ? GLYPHS.status.blocked : GLYPHS.status.active;
137
- const device = state.deviceLabel !== null ? ` ${GLYPHS.navigation.pipeSeparator} ${state.deviceLabel}` : '';
138
- const extra = state.detail !== undefined && state.detail.length > 0
139
- ? ` ${GLYPHS.navigation.pipeSeparator} ${state.detail}`
140
- : '';
141
- const body = `${marker} Voice: ${VOICE_CAPTURE_LABELS[state.kind]}${device}${extra}`;
142
- const isWakeRow = state.kind.startsWith('wake-');
143
- const fg = state.kind === 'wake-latched' || state.kind === 'wake-restarting'
144
- ? tones.chrome.warn
145
- : tones.accent.control;
146
-
147
- if (isWakeRow && state.indicator === 'banner') {
148
- // The prominent variant: the row is filled to the terminal width on the
149
- // footer background so it reads as a standing condition, not a passing note.
150
- const line = UIFactory.stringToLine(' '.repeat(width), width, { fg: tones.chrome.faint });
151
- const text = ` ${truncateDisplay(body, Math.max(0, width - 4), '…')} `;
152
- for (let i = 0; i < text.length && 1 + i < width - 1; i++) {
153
- const cell = line[1 + i]!;
154
- cell.char = text[i]!;
155
- cell.fg = fg;
156
- cell.bg = tones.bg.footer;
157
- cell.bold = true;
158
- cell.dim = false;
159
- }
160
- return [line];
161
- }
162
- return [UIFactory.stringToLine(` ${truncateDisplay(body, Math.max(0, width - 4), '…')}`, width, { fg, bold: true })];
163
- }
@@ -1,222 +0,0 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { getDisplayWidth } from '../utils/terminal-width.ts';
3
- import { buildStyledPanelLine, type StyledPanelSegment } from '../panels/polish.ts';
4
-
5
- export interface TabStripItem {
6
- readonly label: string;
7
- readonly active?: boolean;
8
- }
9
-
10
- export interface TabStripStyle {
11
- readonly activeFg: string;
12
- readonly activeBg?: string;
13
- readonly activeBold?: boolean;
14
- readonly inactiveFg: string;
15
- readonly inactiveBg?: string;
16
- readonly separatorFg: string;
17
- readonly labelFg?: string;
18
- readonly labelBg?: string;
19
- readonly labelBold?: boolean;
20
- readonly overflowFg?: string;
21
- readonly trailingFg?: string;
22
- }
23
-
24
- /** Absolute column range a rendered tab occupies, for click hit-testing. */
25
- export interface TabHitRegion {
26
- /** Index into the original `tabs` array. */
27
- readonly index: number;
28
- /** First column (inclusive) the tab occupies on the line. */
29
- readonly startCol: number;
30
- /** Last column (exclusive) the tab occupies on the line. */
31
- readonly endCol: number;
32
- }
33
-
34
- export interface TabStripConfig {
35
- readonly width: number;
36
- readonly tabs: readonly TabStripItem[];
37
- readonly style: TabStripStyle;
38
- readonly prefixLabel?: string;
39
- readonly suffixLabel?: string;
40
- /** Right-side overflow glyph (more tabs after the window). Default '›'. */
41
- readonly overflowIndicator?: string;
42
- /** Left-side overflow glyph (more tabs before the window). Default '‹'. */
43
- readonly overflowIndicatorLeft?: string;
44
- /**
45
- * Optional callback invoked with the absolute column ranges of each rendered
46
- * tab. Used by the compositor to hit-test mouse clicks onto tabs. Additive,
47
- * the function still returns a `Line` so existing callers are unaffected.
48
- */
49
- readonly onLayout?: (regions: readonly TabHitRegion[]) => void;
50
- }
51
-
52
- const SEPARATOR = '│';
53
-
54
- interface WindowResult {
55
- segments: StyledPanelSegment[];
56
- /** Per-tab column ranges relative to the start of the tab block. */
57
- regions: Array<{ index: number; startCol: number; endCol: number }>;
58
- overflowLeft: boolean;
59
- overflowRight: boolean;
60
- }
61
-
62
- /**
63
- * Build the visible tab segments, windowed so the ACTIVE tab is always shown.
64
- *
65
- * Tabs are laid out left-to-right, but when they don't all fit we keep a
66
- * contiguous window that contains the active tab and grow it outward (biased
67
- * toward following tabs) until the budget is exhausted. Left/right overflow is
68
- * reported so the caller can render `‹`/`›` indicators.
69
- */
70
- function makeSegments(
71
- tabs: readonly TabStripItem[],
72
- style: TabStripStyle,
73
- maxWidth: number,
74
- indicatorWidth: number,
75
- ): WindowResult {
76
- const empty: WindowResult = { segments: [], regions: [], overflowLeft: false, overflowRight: false };
77
- if (tabs.length === 0 || maxWidth <= 0) return empty;
78
-
79
- const sepWidth = getDisplayWidth(SEPARATOR);
80
- // Per-tab entry width: leading separator + (bracketed if active) label + trailing space.
81
- const labelText = (tab: TabStripItem) => (tab.active ? `[${tab.label}]` : tab.label);
82
- const entryWidths = tabs.map((tab) => sepWidth + getDisplayWidth(labelText(tab)) + 1);
83
-
84
- const activeIndex = Math.max(0, tabs.findIndex((t) => t.active));
85
- const n = tabs.length;
86
- const closing = sepWidth; // trailing │ after the last visible tab
87
- const totalAll = entryWidths.reduce((a, b) => a + b, 0) + closing;
88
-
89
- let start = activeIndex;
90
- let end = activeIndex; // inclusive
91
- let used = entryWidths[activeIndex] + closing;
92
-
93
- if (totalAll <= maxWidth) {
94
- // Everything fits, show all tabs, no windowing.
95
- start = 0;
96
- end = n - 1;
97
- } else {
98
- // Need windowing. Reserve room for indicators on whichever side still has
99
- // hidden tabs; the reserve is recomputed each step so reaching an edge
100
- // frees its indicator column.
101
- const budget = maxWidth;
102
- let leftDone = start === 0;
103
- let rightDone = end === n - 1;
104
- let preferRight = true;
105
-
106
- const fits = (extra: number, nextStart: number, nextEnd: number) => {
107
- const leftRes = nextStart > 0 ? indicatorWidth : 0;
108
- const rightRes = nextEnd < n - 1 ? indicatorWidth : 0;
109
- return used + extra + leftRes + rightRes <= budget;
110
- };
111
-
112
- while (!leftDone || !rightDone) {
113
- const goRight = !rightDone && (preferRight || leftDone);
114
- if (goRight) {
115
- const ne = end + 1;
116
- if (fits(entryWidths[ne], start, ne)) {
117
- end = ne;
118
- used += entryWidths[ne];
119
- if (end === n - 1) rightDone = true;
120
- preferRight = false;
121
- } else {
122
- rightDone = true;
123
- }
124
- } else if (!leftDone) {
125
- const ns = start - 1;
126
- if (fits(entryWidths[ns], ns, end)) {
127
- start = ns;
128
- used += entryWidths[ns];
129
- if (start === 0) leftDone = true;
130
- preferRight = true;
131
- } else {
132
- leftDone = true;
133
- }
134
- } else {
135
- break;
136
- }
137
- }
138
- }
139
-
140
- const segments: StyledPanelSegment[] = [];
141
- const regions: Array<{ index: number; startCol: number; endCol: number }> = [];
142
- let col = 0;
143
- for (let i = start; i <= end; i++) {
144
- const tab = tabs[i];
145
- const text = labelText(tab);
146
- const entryStart = col;
147
- segments.push({ text: SEPARATOR, fg: style.separatorFg });
148
- segments.push({
149
- text: `${text} `,
150
- fg: tab.active ? style.activeFg : style.inactiveFg,
151
- bg: tab.active ? style.activeBg : style.inactiveBg,
152
- bold: tab.active ? (style.activeBold ?? false) : false,
153
- });
154
- col += entryWidths[i];
155
- regions.push({ index: i, startCol: entryStart, endCol: col });
156
- }
157
- // Closing separator.
158
- segments.push({ text: SEPARATOR, fg: style.separatorFg });
159
-
160
- return {
161
- segments,
162
- regions,
163
- overflowLeft: start > 0,
164
- overflowRight: end < n - 1,
165
- };
166
- }
167
-
168
- export function renderTabStrip(config: TabStripConfig): Line {
169
- const {
170
- width,
171
- tabs,
172
- style,
173
- prefixLabel,
174
- suffixLabel,
175
- overflowIndicator = '›',
176
- overflowIndicatorLeft = '‹',
177
- onLayout,
178
- } = config;
179
-
180
- const prefixWidth = prefixLabel ? getDisplayWidth(prefixLabel) + 1 : 0;
181
- const reservedSuffixWidth = suffixLabel ? getDisplayWidth(suffixLabel) : 0;
182
- const indicatorWidth = Math.max(
183
- getDisplayWidth(overflowIndicator),
184
- getDisplayWidth(overflowIndicatorLeft),
185
- );
186
- const maxTabWidth = Math.max(0, width - prefixWidth - reservedSuffixWidth);
187
-
188
- const baseSegments: StyledPanelSegment[] = [];
189
- if (prefixLabel) {
190
- baseSegments.push({
191
- text: prefixLabel,
192
- fg: style.labelFg ?? style.inactiveFg,
193
- bg: style.labelBg,
194
- bold: style.labelBold ?? false,
195
- });
196
- baseSegments.push({ text: ' ', fg: style.separatorFg });
197
- }
198
-
199
- const { segments: tabSegments, regions, overflowLeft, overflowRight } =
200
- makeSegments(tabs, style, maxTabWidth, indicatorWidth);
201
-
202
- const leftSegments: StyledPanelSegment[] = [];
203
- if (overflowLeft) {
204
- leftSegments.push({ text: overflowIndicatorLeft, fg: style.overflowFg ?? style.separatorFg });
205
- }
206
-
207
- const suffixSegments: StyledPanelSegment[] = [];
208
- if (overflowRight) {
209
- suffixSegments.push({ text: overflowIndicator, fg: style.overflowFg ?? style.separatorFg });
210
- }
211
- if (suffixLabel) {
212
- suffixSegments.push({ text: suffixLabel, fg: style.trailingFg ?? style.inactiveFg });
213
- }
214
-
215
- // Report absolute hit regions: prefix + optional left indicator, then tabs.
216
- if (onLayout) {
217
- const offset = prefixWidth + (overflowLeft ? getDisplayWidth(overflowIndicatorLeft) : 0);
218
- onLayout(regions.map((r) => ({ index: r.index, startCol: offset + r.startCol, endCol: offset + r.endCol })));
219
- }
220
-
221
- return buildStyledPanelLine(width, [...baseSegments, ...leftSegments, ...tabSegments, ...suffixSegments]);
222
- }