@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,355 +1,115 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
- import type { SelectionItem, SelectionModal } from '../input/selection-modal.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
- import { fitLabelDetailColumns, wrapWithHangingIndent } from './text-layout.ts';
14
-
15
- const BORDER_FG = DEFAULT_OVERLAY_PALETTE.borderFg;
16
- const TITLE_FG = DEFAULT_OVERLAY_PALETTE.titleFg;
17
- const BODY_FG = DEFAULT_OVERLAY_PALETTE.bodyFg;
18
- const MUTED_FG = DEFAULT_OVERLAY_PALETTE.mutedFg;
19
- const CATEGORY_FG = '#4488cc';
20
- const SELECTED_BG = DEFAULT_OVERLAY_PALETTE.selectedBg;
21
-
22
- interface CellStyle {
23
- fg: string;
24
- bg?: string;
25
- bold?: boolean;
26
- dim?: boolean;
27
- }
28
-
29
- function putText(line: Line, startX: number, maxWidth: number, text: string, style: CellStyle): void {
30
- putOverlayText(line, startX, maxWidth, text, style);
31
- }
32
-
33
1
  /**
34
- * How an item's `detail` text fits on screen. Descriptive text is never
35
- * clipped: if it doesn't fit beside the label at a readable width, the full
36
- * text wraps onto its own line(s) below the label instead of being
37
- * truncated. `wrappedDetail` is null when the detail fits inline (no data
38
- * lost either way); otherwise it holds every wrapped line the FULL detail
39
- * needs (no line-count cap).
2
+ * renderSelectionModalOverlay, the generic picker (used by many commands and
3
+ * by the startup recovery questions), drawn with the modal surface kit.
4
+ *
5
+ * Title row, the always-live search row (when the picker searches), items as
6
+ * kit rows grouped under ✦ category headers, the selected item as the
7
+ * gradient row, details wrapped in full after the label, a muted scroll count
8
+ * and keycap hints. The modal sizes to its content: a short question is a
9
+ * small centered dialog that shows every answer; a long list takes the
10
+ * standard height and scrolls around the selected row.
40
11
  */
41
- interface DetailLayout {
42
- readonly labelWidth: number;
43
- readonly detailWidth: number;
44
- readonly wrappedDetail: readonly string[] | null;
45
- }
46
12
 
47
- function computeDetailLayout(item: SelectionItem, remaining: number): DetailLayout {
48
- if (!item.detail) {
49
- return { labelWidth: remaining, detailWidth: 0, wrappedDetail: null };
50
- }
51
- const cols = fitLabelDetailColumns(item.label, item.detail, remaining);
52
- const fitsInline = cols.detailWidth >= 12 && getDisplayWidth(item.detail) <= cols.detailWidth;
53
- if (fitsInline) {
54
- return { labelWidth: cols.labelWidth, detailWidth: cols.detailWidth, wrappedDetail: null };
13
+ import type { SelectionItem, SelectionModal } from '../input/selection-modal.ts';
14
+ import { activeTokens } from './theme.ts';
15
+ import {
16
+ beginModal,
17
+ drawWrapped,
18
+ finishModal,
19
+ maxModalHeight,
20
+ searchRow,
21
+ scrollCountText,
22
+ type KitHint,
23
+ type SurfaceLayer,
24
+ } from './surface-kit.ts';
25
+ import { drawList, type KitRow } from './surface-kit-list.ts';
26
+ import { kitHintsFromStrings, listHeight, modalHeightFor, modalTextWidth } from './surface-kit-extra.ts';
27
+
28
+ function primaryVerb(modal: SelectionModal, item: SelectionItem | null): string {
29
+ if (modal.primaryVerbLabel) return modal.primaryVerbLabel.toLowerCase();
30
+ switch (item?.primaryAction) {
31
+ case 'toggle': return 'toggle';
32
+ case 'edit': return 'edit';
33
+ case 'delete': return 'delete';
34
+ default: return 'select';
55
35
  }
56
- // Nothing shares this row with the label once the detail moves below it,
57
- // so the label itself gets the full row width too.
58
- return {
59
- labelWidth: remaining,
60
- detailWidth: cols.detailWidth,
61
- wrappedDetail: wrapWithHangingIndent(item.detail, Math.max(8, remaining), ''),
62
- };
63
36
  }
64
37
 
65
- /** Physical rows an item occupies: 1 for the label/inline-detail row, plus
66
- * one more per wrapped detail line when the detail didn't fit inline. */
67
- function rowCostFor(item: SelectionItem, remaining: number): number {
68
- if (!item.detail) return 1;
69
- return 1 + (computeDetailLayout(item, remaining).wrappedDetail?.length ?? 0);
38
+ function hintsFor(modal: SelectionModal): KitHint[] {
39
+ const item = modal.getSelected();
40
+ const base: KitHint[] = [['↑↓', 'move'], ['⏎', primaryVerb(modal, item)]];
41
+ if (item?.primaryAction === 'toggle' && !item.actions) base.push(['space', 'toggle']);
42
+ if (item?.adjustable) base.push(['←→', 'adjust']);
43
+ // Item action strings are legacy "[d] delete" / "d delete" text; each one
44
+ // becomes a keycap. Several may share one string separated by spaces.
45
+ const actionStrings = item?.actions ? splitActionString(item.actions) : [];
46
+ return kitHintsFromStrings(actionStrings, base).hints;
70
47
  }
71
48
 
72
- const MODAL_MARGIN = 4;
73
- const MODAL_MAX_WIDTH = 72;
74
- const INDICATOR_WIDTH = 2;
75
- /** Rows kept clear between the box and the top/bottom of the terminal. */
76
- const VIEWPORT_INSET_ROWS = 2;
77
-
78
- /**
79
- * Rows the box spends on its own frame, counted as they are actually pushed
80
- * below: top and bottom border, the title, the section row (a search label,
81
- * search input and divider when the modal searches), the "Results" header and
82
- * the footer hint. `chromeRows` is the budget-math constant the surface
83
- * metrics subtract from a total; this is the physical count, and it is what
84
- * decides how tall the box may really grow.
85
- */
86
- function frameRowsFor(modal: SelectionModal): number {
87
- return modal.allowSearch ? 8 : 6;
49
+ /** "[d] delete [r] rename" → ["[d] delete", "[r] rename"]. */
50
+ function splitActionString(text: string): string[] {
51
+ const bracketed = text.match(/\[[^\]]+\][^[]*/g);
52
+ if (bracketed && bracketed.length > 0) return bracketed.map((part) => part.trim());
53
+ return text.split(/\s{2,}|\s·\s/).map((part) => part.trim()).filter(Boolean);
88
54
  }
89
55
 
90
- /**
91
- * Physical rows every row of the list needs, category headers included, the
92
- * height the box would have to give the list for nothing to be scrolled away.
93
- */
94
- function totalContentRowsFor(modal: SelectionModal, remaining: number): number {
95
- let rows = 0;
56
+ function rowsFor(items: readonly SelectionItem[], selectedIndex: number): KitRow[] {
57
+ const rows: KitRow[] = [];
96
58
  let lastCategory: string | undefined;
97
- for (const item of modal.filteredItems) {
59
+ items.forEach((item, index) => {
98
60
  if (item.category && item.category !== lastCategory) {
99
61
  lastCategory = item.category;
100
- rows += 1;
62
+ rows.push({ header: item.category });
101
63
  }
102
- rows += rowCostFor(item, remaining);
103
- }
64
+ rows.push({
65
+ label: item.label,
66
+ desc: item.detail,
67
+ selected: index === selectedIndex,
68
+ labelFg: item.fg,
69
+ });
70
+ });
104
71
  return rows;
105
72
  }
106
73
 
107
74
  /**
108
- * Render the selection modal as Line[] for overlay in the viewport.
75
+ * Render the selection modal as a SurfaceLayer in screen coordinates.
109
76
  */
110
77
  export function renderSelectionModalOverlay(
111
78
  modal: SelectionModal,
112
- width: number,
113
- viewportHeight = 24,
114
- ): Line[] {
115
- const lines: Line[] = [];
116
- const chromeRows = modal.allowSearch ? 5 : 4;
117
- // Width does not depend on the row budget, so the box layout is settled
118
- // first: the list's real height can only be measured once the wrap width is
119
- // known. `createOverlayBoxLayout` resolves the requested max width the same
120
- // way `getOverlaySurfaceMetrics` does, so both agree on the box.
121
- const layout = createOverlayBoxLayout(width, MODAL_MARGIN, MODAL_MAX_WIDTH);
122
- const remaining = layout.innerWidth - INDICATOR_WIDTH;
123
-
124
- // Size to content before falling back to the viewport's height ratio. When
125
- // every row (and its wrapped detail) fits in the space the terminal
126
- // actually has, ask for exactly that many rows, so a short list is never
127
- // split behind a "(n below)" hint with screen still empty below the box,
128
- // a two-item question in particular must always show both its answers. A
129
- // list genuinely taller than the terminal keeps the ratio-derived budget
130
- // and scrolls as before.
131
- const neededContentRows = totalContentRowsFor(modal, remaining);
132
- const fittableContentRows = viewportHeight - VIEWPORT_INSET_ROWS - frameRowsFor(modal);
133
- const growToFitTotalRows = neededContentRows > 0 && neededContentRows <= fittableContentRows
134
- ? neededContentRows + chromeRows
135
- : undefined;
136
-
137
- const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
138
- margin: MODAL_MARGIN,
139
- maxWidth: MODAL_MAX_WIDTH,
140
- chromeRows,
141
- minContentRows: 6,
142
- // Modals size to content: items whose detail wraps onto extra lines can
143
- // need more physical rows than a plain one-row-per-item modal, so this
144
- // ceiling grows with the viewport rather than staying pinned at a small
145
- // constant (still bounded, never larger than the terminal can show).
146
- maxContentRows: Math.max(10, viewportHeight - chromeRows - 4),
147
- // A floor only. The shared budget keeps its own target and ceiling, so a
148
- // modal whose content is shorter than that target is unaffected: this can
149
- // raise the budget to fit the list, never lower it.
150
- minTotalRows: growToFitTotalRows,
151
- });
152
-
153
- lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.topLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.topRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.titleBg));
154
-
155
- const titleLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.titleBg);
156
- putText(
157
- titleLine,
158
- layout.margin + 2,
159
- layout.innerWidth,
160
- fitDisplay(truncateDisplay(modal.title, layout.innerWidth), layout.innerWidth),
161
- { fg: TITLE_FG, bold: true },
162
- );
163
- lines.push(titleLine);
164
-
79
+ screenWidth: number,
80
+ screenHeight = 24,
81
+ ): SurfaceLayer {
82
+ const t = activeTokens();
83
+ const hints = hintsFor(modal);
84
+ const rows = rowsFor(modal.filteredItems, modal.selectedIndex);
85
+ const textWidth = modalTextWidth(screenWidth, screenHeight);
86
+ // Size to every item (not the filtered set) so typing never resizes the
87
+ // modal; measure against the same text span drawList uses.
88
+ const allRows = rowsFor(modal.items, -1);
89
+ const needed = allRows.length === 0 ? 1 : listHeight(allRows, 0, textWidth - 1);
90
+ const searchRows = modal.allowSearch ? 2 : 0;
91
+ const height = modalHeightFor(screenWidth, screenHeight, { hints }, searchRows + needed);
92
+ // A short picker (it fits well inside the screen) is a small centered dialog.
93
+ const center = height <= Math.round(maxModalHeight(screenHeight) * 0.6);
94
+ const f = beginModal(screenWidth, screenHeight, { title: modal.title, hints, height, center });
95
+
96
+ let top = f.top;
165
97
  if (modal.allowSearch) {
166
- const labelLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
167
- putText(labelLine, layout.margin + 2, layout.innerWidth, fitDisplay(' Search', layout.innerWidth), {
168
- fg: CATEGORY_FG,
169
- dim: true,
170
- });
171
- lines.push(labelLine);
172
- const searchLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.inputBg);
173
- const prefix = '/ ';
174
- const queryAreaWidth = layout.innerWidth - getDisplayWidth(prefix);
175
- const queryValue = modal.query + (modal.searchFocused ? OVERLAY_GLYPHS.cursor : '');
176
- const queryText = fitDisplay(
177
- truncateDisplay(queryValue, queryAreaWidth),
178
- queryAreaWidth,
179
- );
180
- putText(searchLine, layout.margin + 2, getDisplayWidth(prefix), prefix, { fg: modal.searchFocused ? BODY_FG : MUTED_FG });
181
- putText(searchLine, layout.margin + 2 + getDisplayWidth(prefix), queryAreaWidth, queryText, {
182
- fg: modal.query.length > 0 || modal.searchFocused ? BODY_FG : MUTED_FG,
183
- });
184
- lines.push(searchLine);
185
- lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg));
186
- } else {
187
- lines.push(createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg));
98
+ const total = modal.items.length;
99
+ const count = modal.query.length > 0
100
+ ? `${modal.filteredItems.length} of ${total}`
101
+ : `${total} ${total === 1 ? 'item' : 'items'}`;
102
+ searchRow(f, top, modal.query, 'Type to filter', count);
103
+ top += 2;
188
104
  }
189
105
 
190
- const listTitle = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
191
- putText(listTitle, layout.margin + 2, layout.innerWidth, fitDisplay(' Results', layout.innerWidth), {
192
- fg: CATEGORY_FG,
193
- dim: true,
194
- });
195
- lines.push(listTitle);
196
-
197
- const items = modal.filteredItems;
198
- if (items.length === 0) {
199
- const line = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.bodyBg);
106
+ if (rows.length === 0) {
200
107
  const message = modal.query ? 'No matching items' : 'No items';
201
- putText(line, layout.margin + 2, layout.innerWidth, fitDisplay(message, layout.innerWidth), { fg: MUTED_FG, dim: true });
202
- lines.push(line);
203
- } else {
204
- const rowBudget = Math.max(1, metrics.contentRows);
205
-
206
- // Row-budget-aware windowing: rather than assuming one physical row per
207
- // item, account for items whose detail wraps onto extra lines, so the
208
- // visible window never silently shows fewer full items than the budget
209
- // allows just because a fixed item-count cap ran out. Always shows the
210
- // selected item (even alone, if its wrapped detail is taller than the
211
- // whole budget, descriptive text is never clipped to make it fit),
212
- // then grows outward, roughly centered, until the budget is spent.
213
- const selected = Math.max(0, Math.min(modal.selectedIndex, items.length - 1));
214
- let startIdx = selected;
215
- let endIdx = selected + 1;
216
- let usedRows = rowCostFor(items[selected], remaining);
217
- let growBefore = true;
218
- while (startIdx > 0 || endIdx < items.length) {
219
- const canGrowBefore = growBefore && startIdx > 0;
220
- const canGrowAfter = !growBefore && endIdx < items.length;
221
- if (canGrowBefore) {
222
- const cost = rowCostFor(items[startIdx - 1], remaining);
223
- if (usedRows + cost > rowBudget) break;
224
- startIdx -= 1;
225
- usedRows += cost;
226
- } else if (canGrowAfter) {
227
- const cost = rowCostFor(items[endIdx], remaining);
228
- if (usedRows + cost > rowBudget) break;
229
- endIdx += 1;
230
- usedRows += cost;
231
- } else if (startIdx > 0) {
232
- const cost = rowCostFor(items[startIdx - 1], remaining);
233
- if (usedRows + cost > rowBudget) break;
234
- startIdx -= 1;
235
- usedRows += cost;
236
- } else if (endIdx < items.length) {
237
- const cost = rowCostFor(items[endIdx], remaining);
238
- if (usedRows + cost > rowBudget) break;
239
- endIdx += 1;
240
- usedRows += cost;
241
- } else {
242
- break;
243
- }
244
- growBefore = !growBefore;
245
- }
246
-
247
- let lastCategory: string | undefined;
248
-
249
- for (let i = startIdx; i < endIdx; i++) {
250
- const item = items[i];
251
- const isSelected = i === modal.selectedIndex;
252
-
253
- if (item.category && item.category !== lastCategory) {
254
- lastCategory = item.category;
255
- const categoryLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
256
- putText(categoryLine, layout.margin + 2, layout.innerWidth, fitDisplay(` ${item.category}`, layout.innerWidth), {
257
- fg: CATEGORY_FG,
258
- dim: true,
259
- });
260
- lines.push(categoryLine);
261
- }
262
-
263
- const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
264
- const labelColor = isSelected ? TITLE_FG : (item.fg ?? BODY_FG);
265
- const detailColor = isSelected ? BODY_FG : MUTED_FG;
266
- const detailLayout = computeDetailLayout(item, remaining);
267
- const labelWidth = detailLayout.labelWidth;
268
- const labelLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
269
- putText(labelLine, layout.margin + 2, INDICATOR_WIDTH, indicator, {
270
- fg: isSelected ? TITLE_FG : MUTED_FG,
271
- bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
272
- bold: isSelected,
273
- });
274
- putText(labelLine, layout.margin + 2 + INDICATOR_WIDTH, labelWidth, fitDisplay(truncateDisplay(item.label, labelWidth), labelWidth), {
275
- fg: labelColor,
276
- bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
277
- bold: isSelected,
278
- });
279
- if (item.detail) {
280
- if (detailLayout.wrappedDetail === null) {
281
- // Fits beside the label at a readable width, no truncation needed.
282
- putText(labelLine, layout.margin + 2 + INDICATOR_WIDTH + labelWidth, 2, ' ', {
283
- fg: BODY_FG,
284
- bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
285
- });
286
- putText(labelLine, layout.margin + 2 + INDICATOR_WIDTH + labelWidth + 2, detailLayout.detailWidth, fitDisplay(item.detail, detailLayout.detailWidth), {
287
- fg: detailColor,
288
- bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
289
- });
290
- lines.push(labelLine);
291
- } else {
292
- // Doesn't fit beside the label, wrap the FULL detail text onto as
293
- // many lines as it needs below the label, rather than clipping it.
294
- lines.push(labelLine);
295
- for (const detailLineText of detailLayout.wrappedDetail) {
296
- const detailLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
297
- putText(detailLine, layout.margin + 2 + INDICATOR_WIDTH, remaining, fitDisplay(detailLineText, remaining), {
298
- fg: detailColor,
299
- bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
300
- dim: !isSelected,
301
- });
302
- lines.push(detailLine);
303
- }
304
- }
305
- } else {
306
- lines.push(labelLine);
307
- }
308
- }
309
-
310
- if (startIdx > 0 || endIdx < items.length) {
311
- const above = startIdx;
312
- const below = items.length - endIdx;
313
- const scrollHint = above > 0 && below > 0
314
- ? `(${above} above, ${below} below)`
315
- : below > 0
316
- ? `(${below} below)`
317
- : `(${above} above)`;
318
- const hintLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
319
- putText(hintLine, layout.margin + 2, layout.innerWidth, fitDisplay(scrollHint, layout.innerWidth), { fg: MUTED_FG, dim: true });
320
- lines.push(hintLine);
321
- }
108
+ drawWrapped(f.canvas, f.l, top, f.r - f.l + 1, message, { fg: t.textMuted }, f.bottom);
109
+ return finishModal(f);
322
110
  }
323
111
 
324
- const footerLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
325
- const selectedItem = modal.getSelected();
326
- // vocab unification: a caller-supplied primaryVerbLabel (e.g. /help's
327
- // "Run", matching the slash-command palette) wins over the generic
328
- // per-primaryAction default.
329
- const primaryVerb = modal.primaryVerbLabel
330
- ? `[Enter] ${modal.primaryVerbLabel}`
331
- : selectedItem?.primaryAction === 'toggle'
332
- ? '[Enter] Toggle'
333
- : selectedItem?.primaryAction === 'edit'
334
- ? '[Enter] Edit'
335
- : selectedItem?.primaryAction === 'delete'
336
- ? '[Enter] Delete'
337
- : '[Enter] Select';
338
- let hints = `[Up/Down] Navigate ${primaryVerb}`;
339
- if (modal.allowSearch) hints += ' [/] Search';
340
- if (selectedItem?.primaryAction === 'toggle' && !selectedItem.actions) hints += ' [Space] Toggle';
341
- if (selectedItem?.actions) hints += ` ${selectedItem.actions}`;
342
- // hint-grammar: Esc is the conventional "way out" and always sorts last.
343
- hints += ' [Esc] Close';
344
- putText(
345
- footerLine,
346
- layout.margin + 2,
347
- layout.innerWidth,
348
- fitDisplay(truncateDisplay(hints, layout.innerWidth), layout.innerWidth),
349
- { fg: MUTED_FG, dim: true },
350
- );
351
- lines.push(footerLine);
352
- lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg));
353
-
354
- return lines;
112
+ const res = drawList(f.canvas, { rows, top, bottom: f.bottom, x0: f.l, x1: f.r });
113
+ f.hintRight = scrollCountText(res.above, res.below);
114
+ return finishModal(f);
355
115
  }
@@ -318,11 +318,11 @@ const CHANGE_GLYPH: Record<ChangeKind, string> = {
318
318
 
319
319
  /**
320
320
  * Render a SemanticDiff as an array of compact summary strings, one change
321
- * per line. Suitable for display in the diff panel status area or a tooltip.
321
+ * per line. Suitable for display in the Changes modal's status area or a tooltip.
322
322
  *
323
323
  * Format examples:
324
324
  * + fn renderDiffView
325
- * ~ method DiffPanel.showDiff (signature changed)
325
+ * ~ method DiffModal.showDiff (signature changed)
326
326
  * - class OldWidget
327
327
  * + import ./utils { A, B }
328
328
  * ~ import ./types (+NewType, -OldType)
@@ -0,0 +1,102 @@
1
+ /**
2
+ * session-chips.ts, the one row under the header that lists every session
3
+ * there is to switch to: main, each running agent, each background process.
4
+ *
5
+ * col 3 the first chip; chips sit 1 column apart
6
+ * chip 2 columns of padding, the status mark, 1 space, the name,
7
+ * 2 columns of padding
8
+ * current filled in the session's own color, dark bold text
9
+ * others on the surface fill, muted name, the mark in its status color
10
+ * right `tab next shift+tab prev` keycaps ending at width-4
11
+ *
12
+ * Shown only when there is more than one session (the caller decides).
13
+ * Chips that do not fit are left out whole and counted in a faint "+N".
14
+ */
15
+
16
+ import { type Line, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
17
+ import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
18
+ import { activeTokens } from './theme.ts';
19
+ import { keycapHintsWidth, paintKeycapHints } from './surface-kit-parts.ts';
20
+ import type { KitHint } from './surface-kit.ts';
21
+
22
+ export interface SessionChip {
23
+ /** The status mark: ◐ running, ● idle, ✓ finished, ✕ failed, ○ cancelled, ▶ a running process. */
24
+ readonly mark: string;
25
+ /** The mark's color on an unselected chip. */
26
+ readonly markFg: string;
27
+ readonly name: string;
28
+ /** The session's own color (main: brand; an agent: its lane color; a process: success). */
29
+ readonly color: string;
30
+ readonly current: boolean;
31
+ }
32
+
33
+ const LEFT_X = 3;
34
+ const CHIP_PAD = 2;
35
+ const CHIP_GAP = 1;
36
+ /** Longest name a chip shows before it is cut with an ellipsis. */
37
+ const NAME_MAX = 24;
38
+ const HINTS: readonly KitHint[] = [['tab', 'next'], ['shift+tab', 'prev']];
39
+
40
+ function put(line: Line, x: number, endX: number, text: string, style: { fg: string; bg: string; bold?: boolean }): number {
41
+ let cx = x;
42
+ for (const ch of text) {
43
+ const w = getDisplayWidth(ch);
44
+ if (w <= 0) continue;
45
+ if (cx + w > endX || cx >= line.length) break;
46
+ line[cx] = { char: ch, fg: style.fg, bg: style.bg, bold: style.bold ?? false, dim: false, underline: false, italic: false, strikethrough: false };
47
+ if (w === 2 && cx + 1 < line.length) line[cx + 1] = { ...line[cx]!, char: '' };
48
+ cx += w;
49
+ }
50
+ return cx;
51
+ }
52
+
53
+ function chipWidth(chip: SessionChip): number {
54
+ return CHIP_PAD * 2 + getDisplayWidth(chip.mark) + 1 + getDisplayWidth(truncateDisplay(chip.name, NAME_MAX));
55
+ }
56
+
57
+ export function renderSessionChips(width: number, chips: readonly SessionChip[]): Line {
58
+ const t = activeTokens();
59
+ const line = createEmptyLine(width);
60
+ for (const cell of line) cell.bg = '';
61
+ const rightEdge = width - 3; // exclusive: right-aligned text ends at width-4
62
+ const hintsW = keycapHintsWidth(HINTS);
63
+ const showHints = LEFT_X + hintsW + 3 + (chips[0] ? chipWidth(chips[0]) : 0) <= rightEdge;
64
+ const chipsEnd = showHints ? rightEdge - hintsW - 3 : rightEdge;
65
+
66
+ // The current chip is always drawn: when the row is short, the ones before it make way first.
67
+ const order = chips.map((chip, i) => ({ chip, i }));
68
+ const currentIdx = chips.findIndex((c) => c.current);
69
+ const shown = new Set<number>();
70
+ let used = 0;
71
+ const moreW = (n: number): number => (n > 0 ? CHIP_GAP + getDisplayWidth(`+${n}`) : 0);
72
+ const tryAdd = (i: number): boolean => {
73
+ const w = chipWidth(chips[i]!) + (shown.size > 0 ? CHIP_GAP : 0);
74
+ const remaining = chips.length - shown.size - 1;
75
+ if (LEFT_X + used + w + moreW(remaining) > chipsEnd) return false;
76
+ shown.add(i);
77
+ used += w;
78
+ return true;
79
+ };
80
+ if (currentIdx >= 0) tryAdd(currentIdx);
81
+ for (const { i } of order) if (i !== currentIdx && !tryAdd(i)) break;
82
+
83
+ let x = LEFT_X;
84
+ let first = true;
85
+ for (const { chip, i } of order) {
86
+ if (!shown.has(i)) continue;
87
+ if (!first) x += CHIP_GAP;
88
+ first = false;
89
+ const name = truncateDisplay(chip.name, NAME_MAX);
90
+ const bg = chip.current ? chip.color : t.backgroundPanel;
91
+ const end = x + chipWidth(chip);
92
+ put(line, x, end, ' '.repeat(end - x), { fg: t.text, bg });
93
+ let cx = x + CHIP_PAD;
94
+ cx = put(line, cx, end, chip.mark, chip.current ? { fg: t.selectedListItemText, bg, bold: true } : { fg: chip.markFg, bg, bold: true });
95
+ put(line, cx + 1, end, name, chip.current ? { fg: t.selectedListItemText, bg, bold: true } : { fg: t.textMuted, bg });
96
+ x = end;
97
+ }
98
+ const hidden = chips.length - shown.size;
99
+ if (hidden > 0) x = put(line, x + CHIP_GAP, chipsEnd, `+${hidden}`, { fg: t.textFaint, bg: '' });
100
+ if (showHints) paintKeycapHints(line, rightEdge - hintsW, rightEdge, HINTS, { fg: t.textFaint });
101
+ return line;
102
+ }