@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
@@ -31,18 +31,18 @@
31
31
  * 2. The window is one the provider vouched for, not the fallback constant.
32
32
  * A model whose real window happens to equal the fallback is
33
33
  * indistinguishable from "not resolved yet", and in that ambiguity
34
- * staying quiet is the honest choice, the Tokens panel and /context
34
+ * staying quiet is the honest choice, the Usage modal and /context
35
35
  * still show the real numbers either way.
36
36
  * Neither guard touches 'compacting', which reports work that is provably
37
37
  * running rather than a prediction about headroom.
38
38
  */
39
39
 
40
40
  import { DEFAULT_CONTEXT_WINDOW } from '@pellux/goodvibes-sdk/platform/providers';
41
- import type { PanelSessionMaintenanceLevel } from '../panels/session-maintenance.ts';
41
+ import type { ViewSessionMaintenanceLevel } from '../views/session-maintenance.ts';
42
42
 
43
43
  interface ContextStatusHintOptions {
44
44
  /** Maintenance level from evaluateSessionMaintenance. */
45
- readonly level: PanelSessionMaintenanceLevel;
45
+ readonly level: ViewSessionMaintenanceLevel;
46
46
  /** Whether auto-compaction is active (threshold > 0 in config). */
47
47
  readonly autoCompactEnabled: boolean;
48
48
  /** Current usage percent 0–100. */
@@ -104,7 +104,7 @@ function buildContextStatusHint(options: ContextStatusHintOptions): string | nul
104
104
  */
105
105
  export function resolveContextStatusHint(input: {
106
106
  readonly evaluate: (args: { readonly currentTokens: number; readonly contextWindow: number }) => {
107
- readonly level: PanelSessionMaintenanceLevel;
107
+ readonly level: ViewSessionMaintenanceLevel;
108
108
  readonly autoCompactEnabled: boolean;
109
109
  readonly usagePct: number;
110
110
  };
@@ -81,3 +81,23 @@ export function replaceViewportWithOverlay(
81
81
  next.push(...overlay);
82
82
  return next;
83
83
  }
84
+
85
+ /**
86
+ * Center the non-blank rows of a viewport vertically (the home splash). A
87
+ * body that already fills the height is returned unchanged.
88
+ */
89
+ export function centerViewportContent(viewport: readonly Line[], height: number, width: number): Line[] {
90
+ const isBlank = (line: Line): boolean => line.every((cell) => cell.char === ' ' || cell.char === '');
91
+ const first = viewport.findIndex((line) => !isBlank(line));
92
+ if (first < 0) return [...viewport];
93
+ let last = viewport.length - 1;
94
+ while (last > first && isBlank(viewport[last]!)) last--;
95
+ const body = viewport.slice(first, last + 1);
96
+ if (body.length >= height) return [...viewport];
97
+ const top = Math.floor((height - body.length) / 2);
98
+ const out: Line[] = [];
99
+ for (let i = 0; i < top; i++) out.push(createEmptyLine(width));
100
+ out.push(...body);
101
+ while (out.length < height) out.push(createEmptyLine(width));
102
+ return out;
103
+ }
@@ -20,6 +20,10 @@ import { renderBlockActionsMenu } from './block-actions-overlay.ts';
20
20
  import { renderAutocompleteOverlay } from './autocomplete-overlay.ts';
21
21
  import { renderOnboardingWizard } from './onboarding/onboarding-wizard.ts';
22
22
  import { overlayViewportBottom, replaceViewportWithOverlay } from './conversation-layout.ts';
23
+ import type { SurfaceLayer } from './surface-kit.ts';
24
+ import { layerToLines } from './surface-kit-extra.ts';
25
+ import { renderToasts } from './surface-kit-parts.ts';
26
+ import { getSharedToastCenter } from './toast-center.ts';
23
27
 
24
28
  export interface ConversationOverlayContext {
25
29
  readonly input: InputHandler;
@@ -31,44 +35,39 @@ export interface ConversationOverlayContext {
31
35
  readonly contextWindow?: number;
32
36
  }
33
37
 
38
+ /** True while a modal covers the screen, so the composer-anchored popups stay hidden. */
39
+ function modalOpen(input: InputHandler): boolean {
40
+ return input.modelPicker.active || input.settingsModal.active || input.configModal.active || input.mcpWorkspace.active
41
+ || input.sessionPickerModal.active || input.profilePickerModal.active || input.bookmarkModal.active
42
+ || input.blockActionsMenu.active || input.selectionModal.active || input.contextInspectorModal.active
43
+ || input.helpOverlayActive || input.shortcutsOverlayActive || input.surfaceModals.active;
44
+ }
45
+
46
+ /**
47
+ * The parts of the conversation area that are rows rather than modals:
48
+ * onboarding (which owns the whole viewport), the @ and slash popups anchored
49
+ * to the composer (never dimmed: you are still typing), and the search bars.
50
+ * Every modal is a layer instead, see buildConversationLayers.
51
+ */
34
52
  export function applyConversationOverlays(
35
53
  viewport: Line[],
36
54
  context: ConversationOverlayContext,
37
55
  ): Line[] {
38
- const { input, conversation, commandRegistry, keybindingsManager, conversationWidth, viewportHeight, contextWindow } = context;
56
+ const { input, conversationWidth, viewportHeight } = context;
39
57
  let next = viewport;
40
58
  const bottomDockInset = 1 + (input.searchManager.active || input.historySearch.active ? 1 : 0);
41
-
42
- // Overlay posture rule: workspaces (onboarding, model, settings, mcp, help,
43
- // shortcuts) claim the full viewport and are mutually exclusive with docked
44
- // pickers/modals. Once a fullscreen workspace claims the screen, no docked
45
- // overlay is drawn on top of it, enforcing a single visible overlay even if
46
- // stray state survives the handler's clearModalStack. The input layer keeps
47
- // the modal stack to one entry; this is the renderer-side backstop.
48
- let fullscreenClaimed = false;
59
+ const covered = modalOpen(input);
49
60
 
50
61
  if (input.onboardingWizard.active) {
51
- const lines = renderOnboardingWizard(input.onboardingWizard, conversationWidth, viewportHeight);
52
- next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
53
- fullscreenClaimed = true;
62
+ const layer = renderOnboardingWizard(input.onboardingWizard, conversationWidth, viewportHeight);
63
+ next = replaceViewportWithOverlay(layerToLines(layer, conversationWidth, viewportHeight), conversationWidth, viewportHeight);
54
64
  }
55
65
 
56
- if (!fullscreenClaimed && input.filePicker.active) {
66
+ if (!input.onboardingWizard.active && !covered && input.filePicker.active) {
57
67
  const lines = renderFilePickerOverlay(input.filePicker, conversationWidth, viewportHeight);
58
68
  next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
59
69
  }
60
70
 
61
- if (input.modelPicker.active) {
62
- const lines = renderModelWorkspace(input.modelPicker, conversationWidth, viewportHeight);
63
- next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
64
- fullscreenClaimed = true;
65
- }
66
-
67
- if (!fullscreenClaimed && input.selectionModal.active) {
68
- const lines = renderSelectionModalOverlay(input.selectionModal, conversationWidth, viewportHeight);
69
- next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
70
- }
71
-
72
71
  if (input.searchManager.active) {
73
72
  next.push(...renderSearchOverlay(input.searchManager, conversationWidth));
74
73
  }
@@ -77,66 +76,7 @@ export function applyConversationOverlays(
77
76
  next.push(...renderHistorySearchOverlay(input.historySearch, conversationWidth));
78
77
  }
79
78
 
80
- // retirement: the process, agent-detail, and live-tail modal overlays
81
- // were removed with those modals, F2 now opens the Fleet panel, which
82
- // subsumes the live process tree.
83
-
84
- if (!fullscreenClaimed && input.contextInspectorModal.active) {
85
- const lines = renderContextInspector(conversation, conversationWidth, viewportHeight, contextWindow);
86
- next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
87
- }
88
-
89
- if (input.settingsModal.active) {
90
- const lines = renderSettingsModal(input.settingsModal, conversationWidth, viewportHeight);
91
- next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
92
- fullscreenClaimed = true;
93
- }
94
-
95
- if (!fullscreenClaimed && input.configModal.active) {
96
- const lines = renderConfigModal(input.configModal, conversationWidth, viewportHeight);
97
- next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
98
- fullscreenClaimed = true;
99
- }
100
-
101
- if (input.mcpWorkspace.active) {
102
- const lines = renderMcpWorkspace(input.mcpWorkspace, conversationWidth, viewportHeight);
103
- next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
104
- fullscreenClaimed = true;
105
- }
106
-
107
- if (!fullscreenClaimed && input.sessionPickerModal.active) {
108
- const lines = renderSessionPickerModal(input.sessionPickerModal, conversationWidth, viewportHeight);
109
- next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
110
- }
111
-
112
- if (!fullscreenClaimed && input.profilePickerModal.active) {
113
- const lines = renderProfilePickerModal(input.profilePickerModal, conversationWidth, viewportHeight);
114
- next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
115
- }
116
-
117
- if (!fullscreenClaimed && input.bookmarkModal.active) {
118
- const lines = renderBookmarkModal(input.bookmarkModal, conversationWidth, viewportHeight);
119
- next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
120
- }
121
-
122
- if (!fullscreenClaimed && input.blockActionsMenu.active) {
123
- const lines = renderBlockActionsMenu(input.blockActionsMenu, conversationWidth, viewportHeight);
124
- next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
125
- }
126
-
127
- if (input.helpOverlayActive) {
128
- const lines = renderHelpOverlay(conversationWidth, keybindingsManager, commandRegistry.getAll(), input.helpScrollOffset, viewportHeight);
129
- next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
130
- fullscreenClaimed = true;
131
- }
132
-
133
- if (input.shortcutsOverlayActive) {
134
- const lines = renderShortcutsOverlay(conversationWidth, keybindingsManager, input.shortcutsScrollOffset, viewportHeight);
135
- next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
136
- fullscreenClaimed = true;
137
- }
138
-
139
- if (!fullscreenClaimed && input.commandMode && input.autocomplete?.isActive) {
79
+ if (!input.onboardingWizard.active && !covered && input.commandMode && input.autocomplete?.isActive) {
140
80
  const lines = renderAutocompleteOverlay(input.autocomplete, conversationWidth, viewportHeight);
141
81
  if (lines.length > 0) {
142
82
  next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
@@ -145,3 +85,73 @@ export function applyConversationOverlays(
145
85
 
146
86
  return next;
147
87
  }
88
+
89
+ export interface ConversationLayerContext {
90
+ readonly input: InputHandler;
91
+ readonly conversation: ConversationManager;
92
+ readonly commandRegistry: CommandRegistry;
93
+ readonly keybindingsManager: KeybindingsManager;
94
+ /** Full terminal size: modals are sized against the whole screen. */
95
+ readonly screenWidth: number;
96
+ readonly screenHeight: number;
97
+ readonly contextWindow?: number;
98
+ /** The permission dialog, when a request is waiting (drawn above the other modals). */
99
+ readonly permission?: SurfaceLayer | null;
100
+ /** Rows at the top of the screen held by the header (and the session chips row); toasts start below them. */
101
+ readonly headerRows?: number;
102
+ /** Rows at the bottom of the screen held by the footer (the throbber and its empty row while main works, the composer, the status line); toasts stay above them. */
103
+ readonly footerRows?: number;
104
+ }
105
+
106
+ type ModalEntry = readonly [name: string, active: boolean, render: () => SurfaceLayer | null];
107
+
108
+ /**
109
+ * Modal surfaces drawn with the surface kit, as layers the compositor stamps
110
+ * over the dimmed screen (surface-compose.ts). Normally one modal is open;
111
+ * when one opens another (settings opening the model picker or a theme
112
+ * picker), both are drawn in the order they were opened (the input layer's
113
+ * modal stack), the newer on top, each dimming what is under it. Kit-host
114
+ * modals (command palette, confirm dialog), the permission dialog and toasts
115
+ * sit above all of them.
116
+ */
117
+ export function buildConversationLayers(context: ConversationLayerContext): SurfaceLayer[] {
118
+ const { input, screenWidth: w, screenHeight: h } = context;
119
+ // Onboarding owns the screen; only the pickers it launches draw over it.
120
+ const onboarding = input.onboardingWizard.active;
121
+ const entries: ModalEntry[] = [
122
+ ['modelPicker', input.modelPicker.active, () => renderModelWorkspace(input.modelPicker, w, h)],
123
+ ['settings', input.settingsModal.active, () => renderSettingsModal(input.settingsModal, w, h)],
124
+ ['mcpWorkspace', input.mcpWorkspace.active, () => renderMcpWorkspace(input.mcpWorkspace, w, h)],
125
+ ['config', !onboarding && input.configModal.active, () => renderConfigModal(input.configModal, w, h)],
126
+ ['sessionPicker', !onboarding && input.sessionPickerModal.active, () => renderSessionPickerModal(input.sessionPickerModal, w, h)],
127
+ ['profilePicker', !onboarding && input.profilePickerModal.active, () => renderProfilePickerModal(input.profilePickerModal, w, h)],
128
+ ['bookmark', !onboarding && input.bookmarkModal.active, () => renderBookmarkModal(input.bookmarkModal, w, h)],
129
+ ['blockActions', !onboarding && input.blockActionsMenu.active, () => renderBlockActionsMenu(input.blockActionsMenu, w, h)],
130
+ ['contextInspector', !onboarding && input.contextInspectorModal.active,
131
+ () => renderContextInspector(context.conversation, w, h, context.contextWindow, input.contextInspectorModal)],
132
+ ['selection', input.selectionModal.active, () => renderSelectionModalOverlay(input.selectionModal, w, h)],
133
+ ['help', input.helpOverlayActive,
134
+ () => renderHelpOverlay(w, h, context.keybindingsManager, context.commandRegistry.getAll(), input.helpScrollOffset, input.overlayFilters.help)],
135
+ ['shortcuts', input.shortcutsOverlayActive,
136
+ () => renderShortcutsOverlay(w, h, context.keybindingsManager, input.shortcutsScrollOffset, input.overlayFilters.shortcuts)],
137
+ ];
138
+ const active = entries.filter(([, isActive]) => isActive);
139
+ // Opened order first (the modal stack), then any active modal the stack does not name.
140
+ const rank = (name: string): number => {
141
+ const index = input.modalStack.indexOf(name);
142
+ return index >= 0 ? index : input.modalStack.length + entries.findIndex(([n]) => n === name);
143
+ };
144
+ active.sort((a, b) => rank(a[0]) - rank(b[0]));
145
+ const layers: SurfaceLayer[] = [];
146
+ for (const [, , render] of active) {
147
+ const layer = render();
148
+ if (layer) layers.push(layer);
149
+ }
150
+ // Kit modals (command palette, confirm dialog, ...) stack on top of everything.
151
+ layers.push(...input.surfaceModals.render(w, h));
152
+ if (context.permission) layers.push(context.permission);
153
+ // Toasts sit above everything and are never dimmed, between the header and the footer.
154
+ const toasts = renderToasts(w, h, getSharedToastCenter().visible(), { top: context.headerRows ?? 1, bottom: h - (context.footerRows ?? 2) });
155
+ if (toasts) layers.push(toasts);
156
+ return layers;
157
+ }
@@ -2,7 +2,13 @@ import { type Line, createEmptyLine, createStyledCell } from '@pellux/goodvibes-
2
2
  import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
3
3
  import { LAYOUT } from './layout.ts';
4
4
  import { GLYPHS } from './ui-primitives.ts';
5
- import { foldPreviewText, treeBranchCol, treeContentCol } from '@pellux/goodvibes-terminal-shell';
5
+ import { foldPreviewText } from '@pellux/goodvibes-terminal-shell';
6
+
7
+ /** Column of a transcript event row's marker (the ● of a flush row). */
8
+ const MARKER_COL = LAYOUT.LEFT_MARGIN - 1;
9
+ /** Column where the row's segment run begins (segments carry their own leading blank). */
10
+ const CONTENT_COL = MARKER_COL + 2;
11
+ import { activeTokens } from './theme.ts';
6
12
 
7
13
  export interface ConversationSurfacePalette {
8
14
  readonly accent: string;
@@ -103,18 +109,11 @@ export function renderConversationFragment(
103
109
  content: string,
104
110
  width: number,
105
111
  palette: ConversationFragmentPalette,
106
- indentCols = 0,
107
112
  ): Line[] {
108
- // A tree fragment is a continuation of the row above it, so it starts at that
109
- // row's content column, which puts its ` ▸ ` prefix glyph in exactly the same
110
- // column as the parent row's own ` ▸ N lines ` badge. A flush fragment (a user
111
- // message ghost box) keeps the box margin it has always used.
112
- const indent = Math.max(0, indentCols);
113
- const margin = indent > 0 ? treeContentCol(indent) : LAYOUT.USER_BOX_MARGIN;
113
+ // A fragment (a user message ghost box) keeps the box margin it has always used.
114
+ const margin = LAYOUT.USER_BOX_MARGIN;
114
115
  const prefixWidth = getDisplayWidth(palette.prefix);
115
- // The indent is charged to the content budget rather than allowed to push the
116
- // fragment past the right edge, a narrow terminal shrinks the preview text
117
- // instead of silently truncating its tail.
116
+ // A narrow terminal shrinks the preview text instead of silently truncating its tail.
118
117
  const maxContentWidth = Math.max(1, width - margin - LAYOUT.USER_BOX_MARGIN - prefixWidth - 2);
119
118
  const wrapped = wrapText(content, maxContentWidth);
120
119
  const contentWidth = wrapped.length > 0 ? Math.max(...wrapped.map((line) => getDisplayWidth(line))) : 0;
@@ -193,19 +192,18 @@ export function renderConversationFoldedRow(
193
192
  tone: ConversationEventTone,
194
193
  details: readonly ConversationStatusSegment[],
195
194
  preview: string,
196
- indentCols = 0,
197
195
  ): Line {
198
196
  // What is left of the row after the marker, the label and the badges.
199
197
  const usedCols = (tone.label ? getDisplayWidth(` ${tone.label} `) : 0)
200
198
  + details.reduce((sum, segment) => sum + getDisplayWidth(segment.text), 0);
201
- const availCols = width - LAYOUT.RIGHT_MARGIN - treeContentCol(Math.max(0, indentCols)) - usedCols - 1;
199
+ const availCols = width - LAYOUT.RIGHT_MARGIN - CONTENT_COL - usedCols - 1;
202
200
  // Whether a preview renders at all, and its flattening, are the policy's call
203
201
  // (foldPreviewText returns null for "no preview"). Only the TRUNCATION is
204
202
  // ours: display width is a product-local rule the policy deliberately leaves
205
203
  // to the caller, and it must truncate, never wrap.
206
204
  const trimmed = foldPreviewText(preview, availCols);
207
205
  if (trimmed === null) {
208
- return renderConversationEventLine(width, tone, details, indentCols);
206
+ return renderConversationEventLine(width, tone, details);
209
207
  }
210
208
  const text = getDisplayWidth(trimmed) > availCols
211
209
  ? `${trimmed.slice(0, Math.max(1, availCols - 1))}…`
@@ -213,12 +211,11 @@ export function renderConversationFoldedRow(
213
211
  return renderConversationEventLine(
214
212
  width,
215
213
  tone,
216
- [...details, { text: `${text} `, fg: tone.detailFg ?? '244', dim: true }],
217
- indentCols,
214
+ [...details, { text: `${text} `, fg: activeTokens().textFaint }],
218
215
  );
219
216
  }
220
217
 
221
- export function renderConversationStatusLine(
218
+ function renderConversationStatusLine(
222
219
  width: number,
223
220
  segments: readonly ConversationStatusSegment[],
224
221
  options: {
@@ -226,29 +223,17 @@ export function renderConversationStatusLine(
226
223
  readonly markerFg?: string;
227
224
  readonly markerBg?: string;
228
225
  readonly bodyBg?: string;
229
- /**
230
- * Tree-branch indent in columns (see conversation-tree.ts). Shifts the
231
- * marker and content columns together, so a branch row keeps the same
232
- * marker→content relationship a flush row has. Callers pass an indent
233
- * already clamped by treeIndentCols(), so this never eats the content
234
- * budget below the guaranteed minimum.
235
- */
236
- readonly indentCols?: number;
237
226
  } = {},
238
227
  ): Line {
239
228
  const line = createEmptyLine(width);
240
- const indent = Math.max(0, options.indentCols ?? 0);
241
- // Both columns come from conversation-tree.ts so a flush row and a branch row
242
- // sit on one grid: the marker where its depth's glyph belongs, the segment run
243
- // two columns further right.
244
- const markerCol = treeBranchCol(indent);
245
- const startCol = treeContentCol(indent);
229
+ const markerCol = MARKER_COL;
230
+ const startCol = CONTENT_COL;
246
231
  const endCol = Math.max(startCol, width - LAYOUT.RIGHT_MARGIN);
247
232
  const marker = options.marker ?? '▌';
248
233
  const markerWidth = getDisplayWidth(marker);
249
234
  if (markerCol >= 0 && markerCol < width && markerWidth > 0) {
250
235
  const markerStyle = {
251
- fg: options.markerFg ?? '#64748b',
236
+ fg: options.markerFg ?? activeTokens().textMuted,
252
237
  bg: options.markerBg ?? options.bodyBg ?? '',
253
238
  bold: true,
254
239
  };
@@ -277,12 +262,9 @@ export function renderConversationEventLine(
277
262
  width: number,
278
263
  tone: ConversationEventTone,
279
264
  details: readonly ConversationStatusSegment[] = [],
280
- indentCols = 0,
281
265
  ): Line {
282
- // An empty label is legitimate on a branch row: the tree already says what
283
- // the row is (a result hanging under its call), so repeating "tool result"
284
- // on every one of them is exactly the boilerplate this layout removes. The
285
- // row then leads with its own first informative detail segment.
266
+ // An empty label is legitimate: the row then leads with its own first
267
+ // informative detail segment.
286
268
  const labelSegments = tone.label
287
269
  ? [{ text: ` ${tone.label} `, fg: tone.labelFg, bold: true }]
288
270
  : [];
@@ -298,7 +280,6 @@ export function renderConversationEventLine(
298
280
  {
299
281
  marker: tone.marker,
300
282
  markerFg: tone.markerFg,
301
- indentCols,
302
283
  },
303
284
  );
304
285
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * diff-tint.ts, row tints for a diff that stay visible on the fill they sit on.
3
+ *
4
+ * The theme's diff tokens (diffAddedBg, diffRemovedBg and their line-number
5
+ * variants) are tuned against the terminal background. A diff drawn inside a
6
+ * lighter element inset can land within a few color units of them: gruvbox
7
+ * light's added row is 7 units from its element fill, rosepine light's removed
8
+ * line-number tint 5. Those rows read as untinted.
9
+ *
10
+ * visibleTint keeps the theme's token whenever it is far enough from the fill.
11
+ * When it is not, it pushes the token further from the fill along the same
12
+ * direction (the same hue shift, just stronger), and when the token carries
13
+ * no usable direction it mixes the fill toward the diff's sign color instead.
14
+ */
15
+
16
+ import { mixHex } from '@pellux/goodvibes-sdk/platform/presentation';
17
+ import { activeDiffTones, activeTokens } from './theme.ts';
18
+
19
+ /**
20
+ * Smallest Euclidean sRGB distance (0..441) between a diff row's tint and the
21
+ * fill under it. A flat fill change becomes noticeable side by side at about 8
22
+ * units; a full-width row has to read as tinted at a glance, from across the
23
+ * modal, so the floor is three times that. For reference, GitHub dark's own
24
+ * added-row tint sits about 20 units from its canvas and reads as faint.
25
+ */
26
+ const MIN_DIFF_TINT_DISTANCE = 24;
27
+
28
+ type Rgb = readonly [number, number, number];
29
+
30
+ function parse(hex: string): Rgb | null {
31
+ const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
32
+ if (!m) return null;
33
+ const n = parseInt(m[1]!, 16);
34
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
35
+ }
36
+
37
+ function toHex(c: Rgb): string {
38
+ return `#${c.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('')}`;
39
+ }
40
+
41
+ /** Euclidean distance between two #rrggbb colors (0 when either does not parse). */
42
+ function colorDistance(a: string, b: string): number {
43
+ const ca = parse(a);
44
+ const cb = parse(b);
45
+ if (!ca || !cb) return 0;
46
+ return Math.hypot(ca[0] - cb[0], ca[1] - cb[1], ca[2] - cb[2]);
47
+ }
48
+
49
+ /**
50
+ * `token` when it is at least `floor` from `fill`; otherwise the closest color
51
+ * that is, found first along the token's own direction from the fill, then by
52
+ * mixing the fill toward `tone` (the diff sign color). Colors that do not parse
53
+ * come back unchanged.
54
+ */
55
+ function visibleTint(token: string, fill: string, tone: string, floor = MIN_DIFF_TINT_DISTANCE): string {
56
+ const t = parse(token);
57
+ const f = parse(fill);
58
+ if (!t || !f) return token;
59
+ const len = Math.hypot(t[0] - f[0], t[1] - f[1], t[2] - f[2]);
60
+ if (len >= floor) return token;
61
+ // Aim one unit past the floor so rounding to whole channels never lands short.
62
+ const target = floor + 1;
63
+ if (len >= 4) {
64
+ const k = target / len;
65
+ const pushed = toHex([f[0] + (t[0] - f[0]) * k, f[1] + (t[1] - f[1]) * k, f[2] + (t[2] - f[2]) * k]);
66
+ if (colorDistance(pushed, fill) >= floor) return pushed;
67
+ }
68
+ const full = colorDistance(tone, fill);
69
+ if (full <= 0) return token;
70
+ return mixHex(fill, tone, Math.min(1, target / full));
71
+ }
72
+
73
+ export interface DiffRowTints {
74
+ readonly addedRow: string;
75
+ readonly removedRow: string;
76
+ readonly addedGutter: string;
77
+ readonly removedGutter: string;
78
+ }
79
+
80
+ const tintCache = new Map<string, DiffRowTints>();
81
+
82
+ /** The active theme's diff tints, each kept visible on `fill` (the inset the diff sits on). */
83
+ export function diffRowTints(fill: string): DiffRowTints {
84
+ const t = activeTokens();
85
+ const tones = activeDiffTones();
86
+ const key = [fill, t.diffAddedBg, t.diffRemovedBg, t.diffAddedLineNumberBg, t.diffRemovedLineNumberBg, tones.add, tones.del].join('|');
87
+ const cached = tintCache.get(key);
88
+ if (cached) return cached;
89
+ const tints: DiffRowTints = {
90
+ addedRow: visibleTint(t.diffAddedBg, fill, tones.add),
91
+ removedRow: visibleTint(t.diffRemovedBg, fill, tones.del),
92
+ addedGutter: visibleTint(t.diffAddedLineNumberBg, fill, tones.add),
93
+ removedGutter: visibleTint(t.diffRemovedLineNumberBg, fill, tones.del),
94
+ };
95
+ if (tintCache.size > 64) tintCache.clear();
96
+ tintCache.set(key, tints);
97
+ return tints;
98
+ }
@@ -1,101 +1,74 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
- import type { FilePickerModal } from '../input/file-picker.ts';
4
- import {
5
- createOverlayBoxLayout,
6
- createOverlayContentLine,
7
- createOverlayFilledBorderLine,
8
- DEFAULT_OVERLAY_PALETTE,
9
- OVERLAY_GLYPHS,
10
- putOverlayText,
11
- } from './overlay-box.ts';
12
- import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
13
-
14
1
  /**
15
- * Render the file picker modal as Line[] for overlay in the viewport.
16
- * Shows a bordered box with fuzzy-matched file results.
2
+ * renderFilePickerOverlay, the @ file popup.
3
+ *
4
+ * Anchored to the composer with no dimming, the same shape as the slash
5
+ * popup (┃ bar, surface fill, no frame, no hint rows). Its first row is the
6
+ * typed query ("@ query▏" and the match count), since the query lives in the
7
+ * picker rather than the composer. Fuzzy-matched letters of the file name are
8
+ * drawn in the brand color; the directory part is dimmer than the name, so
9
+ * the path dims toward the root.
17
10
  */
11
+
12
+ import type { Line } from '@pellux/goodvibes-sdk/platform/types';
13
+ import type { FilePickerModal } from '../input/file-picker.ts';
14
+ import { activeTokens } from './theme.ts';
15
+ import { renderPopup } from './surface-kit-parts.ts';
16
+ import type { KitRow, KitSpan } from './surface-kit-list.ts';
17
+
18
+ function popupRows(viewportHeight: number): number {
19
+ return Math.max(4, Math.min(12, Math.floor(viewportHeight * 0.5)));
20
+ }
21
+
22
+ /** Split a path into directory and name spans, marking the query's letters (in order) in the name. */
23
+ function fileSpans(file: string, query: string, selected: boolean): KitSpan[] {
24
+ const t = activeTokens();
25
+ const trimmed = file.endsWith('/') ? file.slice(0, -1) : file;
26
+ const cut = trimmed.lastIndexOf('/') + 1;
27
+ const dir = file.slice(0, cut);
28
+ const name = file.slice(cut);
29
+ const spans: KitSpan[] = [];
30
+ if (dir) spans.push({ text: dir, fg: t.textFaint });
31
+ const wanted = [...query.toLowerCase()].filter((ch) => ch !== ' ');
32
+ let q = 0;
33
+ let run = '';
34
+ let runHit = false;
35
+ const flush = (): void => {
36
+ if (run) spans.push({ text: run, fg: runHit && !selected ? t.brand : t.text, bold: selected || runHit });
37
+ run = '';
38
+ };
39
+ for (const ch of name) {
40
+ const hit = q < wanted.length && ch.toLowerCase() === wanted[q];
41
+ if (hit) q++;
42
+ if (run && hit !== runHit) flush();
43
+ runHit = hit;
44
+ run += ch;
45
+ }
46
+ flush();
47
+ return spans;
48
+ }
49
+
18
50
  export function renderFilePickerOverlay(
19
51
  picker: FilePickerModal,
20
52
  width: number,
21
53
  viewportHeight = 24,
22
54
  ): Line[] {
23
- const lines: Line[] = [];
24
- const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
25
- chromeRows: 4,
26
- maxWidth: 70,
27
- minContentRows: 6,
28
- maxContentRows: 10,
29
- });
30
- const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
31
- const contentW = layout.innerWidth;
32
- const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
33
- const titleFg = DEFAULT_OVERLAY_PALETTE.titleFg;
34
- const bodyFg = DEFAULT_OVERLAY_PALETTE.bodyFg;
35
- const mutedFg = DEFAULT_OVERLAY_PALETTE.mutedFg;
36
- const selectedBg = DEFAULT_OVERLAY_PALETTE.selectedBg;
37
-
38
- // Title bar
39
- const titleLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.topLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.topRight, borderFg, DEFAULT_OVERLAY_PALETTE.titleBg);
40
- putOverlayText(titleLine, layout.margin + 2, layout.width - 4, 'Select File', { fg: titleFg, bold: true });
41
- lines.push(titleLine);
42
-
43
- // Search input
44
- const queryDisplay = picker.query || '';
45
- const searchLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.inputBg);
46
- const searchPrefix = '@ ';
47
- const queryText = fitDisplay(`${queryDisplay}${picker.searchFocused ? OVERLAY_GLYPHS.cursor : ''}`, Math.max(0, contentW - getDisplayWidth(searchPrefix)));
48
- putOverlayText(searchLine, layout.margin + 2, getDisplayWidth(searchPrefix), searchPrefix, { fg: picker.searchFocused ? bodyFg : mutedFg });
49
- putOverlayText(searchLine, layout.margin + 2 + getDisplayWidth(searchPrefix), contentW - getDisplayWidth(searchPrefix), queryText, { fg: picker.query.length > 0 || picker.searchFocused ? bodyFg : mutedFg });
50
- lines.push(searchLine);
51
-
52
- // Separator
53
- lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
54
-
55
- // Results
55
+ const t = activeTokens();
56
+ const count = picker.results.length === 0 ? '' : `${picker.results.length} file${picker.results.length === 1 ? '' : 's'}`;
57
+ const header: KitRow = {
58
+ spans: [
59
+ { text: `@${picker.query}`, fg: t.text, bold: true },
60
+ { text: '▏', fg: t.brand },
61
+ ],
62
+ right: count || undefined,
63
+ };
64
+ const rows: KitRow[] = [];
56
65
  if (picker.results.length === 0) {
57
- const noResults = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
58
- putOverlayText(noResults, layout.margin + 2, contentW, fitDisplay('No matching files', contentW), { fg: '244', dim: true });
59
- lines.push(noResults);
66
+ rows.push({ label: picker.query ? 'No matching files' : 'Loading files…', muted: true });
60
67
  } else {
61
- const maxVisible = metrics.contentRows;
62
- let startIdx = 0;
63
- if (picker.results.length > maxVisible) {
64
- startIdx = Math.max(0, Math.min(
65
- picker.selectedIndex - Math.floor(maxVisible / 2),
66
- picker.results.length - maxVisible,
67
- ));
68
- }
69
- const endIdx = Math.min(startIdx + maxVisible, picker.results.length);
70
-
71
- for (let i = startIdx; i < endIdx; i++) {
72
- const file = picker.results[i];
73
- const isSelected = i === picker.selectedIndex;
74
- const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
75
- const displayFile = getDisplayWidth(file) > contentW - 2
76
- ? truncateDisplay(file, contentW - 2)
77
- : file;
78
- const line = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
79
- putOverlayText(
80
- line,
81
- layout.margin + 2,
82
- contentW,
83
- fitDisplay(indicator + fitDisplay(displayFile, contentW - 2), contentW),
84
- {
85
- fg: isSelected ? titleFg : file.endsWith('/') ? titleFg : bodyFg,
86
- bg: isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg,
87
- bold: isSelected,
88
- },
89
- );
90
- lines.push(line);
91
- }
68
+ picker.results.forEach((file, index) => {
69
+ const selected = index === picker.selectedIndex;
70
+ rows.push({ spans: fileSpans(file, picker.query, selected), selected });
71
+ });
92
72
  }
93
-
94
- // Bottom border with hints
95
- const hints = '[Up/Down] Navigate [/] Search [Enter] Select [Esc] Cancel';
96
- const bottomLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
97
- putOverlayText(bottomLine, layout.margin + 2, layout.width - 4, truncateDisplay(hints, layout.width - 4), { fg: mutedFg, dim: true });
98
- lines.push(bottomLine);
99
-
100
- return lines;
73
+ return renderPopup({ width, rows, header, maxRows: popupRows(viewportHeight), scrollOwner: picker }).lines;
101
74
  }