@pellux/goodvibes-tui 2.0.21 → 2.1.1

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 (391) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -29
  3. package/docs/README.md +8 -5
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +28 -19
  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-reference.ts +2 -2
  52. package/src/input/command-registry.ts +77 -35
  53. package/src/input/commands/acp-runtime.ts +19 -9
  54. package/src/input/commands/channel-runtime.ts +2 -2
  55. package/src/input/commands/ci-runtime.ts +1 -1
  56. package/src/input/commands/codebase-runtime.ts +1 -1
  57. package/src/input/commands/config.ts +7 -1
  58. package/src/input/commands/context-window.ts +25 -19
  59. package/src/input/commands/control-room-runtime.ts +18 -18
  60. package/src/input/commands/conversation-runtime.ts +0 -1
  61. package/src/input/commands/cost-runtime.ts +29 -20
  62. package/src/input/commands/diff-runtime.ts +35 -181
  63. package/src/input/commands/eval.ts +1 -1
  64. package/src/input/commands/experience-runtime.ts +3 -7
  65. package/src/input/commands/fresh-conversation.ts +86 -0
  66. package/src/input/commands/git-runtime.ts +5 -22
  67. package/src/input/commands/graph-runtime.ts +1 -1
  68. package/src/input/commands/guidance-runtime.ts +1 -1
  69. package/src/input/commands/health-runtime.ts +8 -9
  70. package/src/input/commands/hooks-runtime.ts +2 -2
  71. package/src/input/commands/hosted-runtime.ts +3 -3
  72. package/src/input/commands/incident-runtime.ts +3 -3
  73. package/src/input/commands/intelligence-runtime.ts +6 -9
  74. package/src/input/commands/knowledge.ts +2 -2
  75. package/src/input/commands/legacy-panel-command.ts +57 -0
  76. package/src/input/commands/local-auth-runtime.ts +8 -9
  77. package/src/input/commands/local-provider-runtime.ts +7 -4
  78. package/src/input/commands/local-runtime.ts +12 -15
  79. package/src/input/commands/local-setup-review.ts +1 -1
  80. package/src/input/commands/local-setup.ts +13 -19
  81. package/src/input/commands/memory.ts +1 -1
  82. package/src/input/commands/notify-runtime.ts +9 -0
  83. package/src/input/commands/operator-runtime.ts +10 -12
  84. package/src/input/commands/permissions-provenance.ts +3 -3
  85. package/src/input/commands/planning-runtime.ts +7 -7
  86. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  87. package/src/input/commands/platform-services-runtime.ts +1 -1
  88. package/src/input/commands/policy-dispatch.ts +2 -2
  89. package/src/input/commands/policy.ts +3 -3
  90. package/src/input/commands/provider.ts +1 -1
  91. package/src/input/commands/remote-runtime.ts +1 -1
  92. package/src/input/commands/review-runtime.ts +39 -94
  93. package/src/input/commands/rewind-runtime.ts +81 -108
  94. package/src/input/commands/runtime-services.ts +15 -17
  95. package/src/input/commands/services-runtime.ts +2 -2
  96. package/src/input/commands/session-content.ts +21 -5
  97. package/src/input/commands/session-workflow.ts +13 -38
  98. package/src/input/commands/settings-sync-runtime.ts +2 -2
  99. package/src/input/commands/shell-core.ts +19 -4
  100. package/src/input/commands/skills-runtime.ts +2 -2
  101. package/src/input/commands/status-runtime.ts +13 -0
  102. package/src/input/commands/subscription-runtime.ts +1 -1
  103. package/src/input/commands/tasks-runtime.ts +2 -8
  104. package/src/input/commands/work-plan-runtime.ts +4 -4
  105. package/src/input/commands/workstream-attempts.ts +24 -47
  106. package/src/input/commands/workstream-runtime.ts +2 -2
  107. package/src/input/commands/worktree-runtime.ts +3 -3
  108. package/src/input/commands.ts +3 -1
  109. package/src/input/composer-editor.ts +44 -0
  110. package/src/input/concealed-input.ts +1 -1
  111. package/src/input/config-modal-types.ts +20 -8
  112. package/src/input/config-modal.ts +106 -105
  113. package/src/input/confirm-dialog-types.ts +28 -0
  114. package/src/input/confirm-dialog.ts +70 -0
  115. package/src/input/delete-key-policy.ts +3 -3
  116. package/src/input/feed-context-factory.ts +15 -16
  117. package/src/input/handler-command-route.ts +7 -42
  118. package/src/input/handler-content-actions.ts +16 -15
  119. package/src/input/handler-feed-routes.ts +26 -211
  120. package/src/input/handler-feed.ts +68 -95
  121. package/src/input/handler-interactions.ts +5 -3
  122. package/src/input/handler-modal-routes.ts +215 -216
  123. package/src/input/handler-modal-stack.ts +81 -11
  124. package/src/input/handler-modal-token-routes.ts +4 -0
  125. package/src/input/handler-onboarding.ts +0 -1
  126. package/src/input/handler-picker-routes.ts +65 -107
  127. package/src/input/handler-session-view-route.ts +149 -0
  128. package/src/input/handler-shortcuts.ts +16 -154
  129. package/src/input/handler-types.ts +4 -2
  130. package/src/input/handler-ui-state.ts +45 -15
  131. package/src/input/handler-work-tree-route.ts +123 -0
  132. package/src/input/handler.ts +25 -70
  133. package/src/input/keybindings.ts +16 -71
  134. package/src/input/masked-entry-modal.ts +117 -0
  135. package/src/input/mcp-workspace.ts +48 -17
  136. package/src/input/model-picker-filter.ts +8 -0
  137. package/src/input/model-picker-open.ts +99 -0
  138. package/src/input/model-picker-types.ts +1 -1
  139. package/src/input/model-picker.ts +92 -9
  140. package/src/input/notifications-modal.ts +123 -0
  141. package/src/input/overlay-filter.ts +26 -0
  142. package/src/input/profile-picker-modal.ts +37 -8
  143. package/src/input/search.ts +3 -3
  144. package/src/input/selection-modal-queue.ts +2 -0
  145. package/src/input/selection-modal.ts +28 -16
  146. package/src/input/session-picker-modal.ts +33 -8
  147. package/src/input/settings-modal-activation.ts +7 -2
  148. package/src/input/settings-modal-data.ts +11 -6
  149. package/src/input/settings-modal-types.ts +1 -1
  150. package/src/input/settings-modal.ts +20 -20
  151. package/src/input/submission-intent.ts +1 -1
  152. package/src/input/submission-router.ts +4 -9
  153. package/src/input/surface-modal-host.ts +110 -0
  154. package/src/input/theme-settings-actions.ts +82 -0
  155. package/src/input/usage-modal.ts +183 -0
  156. package/src/input/views.ts +97 -0
  157. package/src/main.ts +175 -165
  158. package/src/permissions/hunk-selection.ts +1 -1
  159. package/src/permissions/prompt-card.ts +511 -0
  160. package/src/permissions/prompt.ts +59 -622
  161. package/src/renderer/agents-modal.ts +310 -0
  162. package/src/renderer/autocomplete-overlay.ts +42 -220
  163. package/src/renderer/back-to-bottom.ts +80 -0
  164. package/src/renderer/block-actions-overlay.ts +30 -48
  165. package/src/renderer/bookmark-modal.ts +48 -79
  166. package/src/renderer/changes-modal.ts +325 -0
  167. package/src/renderer/code-block.ts +184 -106
  168. package/src/renderer/command-palette.ts +93 -0
  169. package/src/renderer/composer-fragments.ts +4 -4
  170. package/src/renderer/composer.ts +146 -0
  171. package/src/renderer/compositor.ts +26 -160
  172. package/src/renderer/config-modal.ts +185 -81
  173. package/src/renderer/confirm-dialog.ts +50 -0
  174. package/src/renderer/context-inspector.ts +116 -154
  175. package/src/renderer/context-status-hint.ts +4 -4
  176. package/src/renderer/conversation-layout.ts +20 -0
  177. package/src/renderer/conversation-overlays.ts +94 -84
  178. package/src/renderer/conversation-surface.ts +19 -38
  179. package/src/renderer/diff-tint.ts +98 -0
  180. package/src/renderer/file-picker-overlay.ts +63 -90
  181. package/src/renderer/footer-targets.ts +38 -0
  182. package/src/renderer/git-status.ts +3 -4
  183. package/src/renderer/header-line.ts +140 -117
  184. package/src/renderer/help-overlay.ts +133 -219
  185. package/src/renderer/history-search-overlay.ts +15 -12
  186. package/src/renderer/lane-graph/bead.ts +800 -0
  187. package/src/renderer/lane-graph/glyphs.ts +134 -0
  188. package/src/renderer/lane-graph/layout.ts +248 -0
  189. package/src/renderer/lane-graph/paint.ts +537 -0
  190. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  191. package/src/renderer/layout-engine.ts +2 -57
  192. package/src/renderer/layout.ts +7 -13
  193. package/src/renderer/markdown-inline.ts +2 -1
  194. package/src/renderer/markdown-table.ts +17 -12
  195. package/src/renderer/markdown.ts +92 -34
  196. package/src/renderer/masked-entry-modal.ts +78 -0
  197. package/src/renderer/mcp-workspace.ts +166 -203
  198. package/src/renderer/model-workspace.ts +381 -436
  199. package/src/renderer/notifications-modal.ts +116 -0
  200. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  201. package/src/renderer/process-view.ts +192 -0
  202. package/src/renderer/profile-picker-modal.ts +58 -110
  203. package/src/renderer/search-overlay.ts +6 -3
  204. package/src/renderer/selection-modal-overlay.ts +87 -327
  205. package/src/renderer/semantic-diff.ts +2 -2
  206. package/src/renderer/session-chips.ts +102 -0
  207. package/src/renderer/session-picker-modal.ts +177 -297
  208. package/src/renderer/settings-modal-connections.ts +14 -23
  209. package/src/renderer/settings-modal-context.ts +367 -0
  210. package/src/renderer/settings-modal-helpers.ts +6 -11
  211. package/src/renderer/settings-modal.ts +322 -726
  212. package/src/renderer/shell-surface.ts +277 -175
  213. package/src/renderer/shortcuts-overlay.ts +217 -0
  214. package/src/renderer/status-line.ts +475 -0
  215. package/src/renderer/surface-compose.ts +174 -0
  216. package/src/renderer/surface-kit-extra.ts +303 -0
  217. package/src/renderer/surface-kit-list.ts +366 -0
  218. package/src/renderer/surface-kit-parts.ts +360 -0
  219. package/src/renderer/surface-kit.ts +568 -0
  220. package/src/renderer/syntax-highlighter.ts +280 -197
  221. package/src/renderer/syntax-theme.ts +51 -0
  222. package/src/renderer/system-message.ts +0 -1
  223. package/src/renderer/term-caps.ts +42 -1
  224. package/src/renderer/terminal-bg-probe.ts +116 -19
  225. package/src/renderer/terminal-palette-probe.ts +322 -0
  226. package/src/renderer/terminal-palette-reader.ts +183 -0
  227. package/src/renderer/terminal-palette.ts +58 -0
  228. package/src/renderer/theme-mode-config.ts +16 -1
  229. package/src/renderer/theme.ts +234 -207
  230. package/src/renderer/thinking.ts +0 -1
  231. package/src/renderer/throbber.ts +188 -0
  232. package/src/renderer/toast-center.ts +119 -0
  233. package/src/renderer/ui-factory.ts +39 -602
  234. package/src/renderer/ui-primitives.ts +8 -25
  235. package/src/renderer/usage-modal.ts +353 -0
  236. package/src/renderer/user-message.ts +58 -0
  237. package/src/renderer/voice-capture-chip.ts +53 -0
  238. package/src/runtime/bootstrap-command-context.ts +0 -5
  239. package/src/runtime/bootstrap-command-parts.ts +38 -89
  240. package/src/runtime/bootstrap-core.ts +13 -15
  241. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  242. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  243. package/src/runtime/bootstrap-shell.ts +16 -50
  244. package/src/runtime/bootstrap.ts +14 -14
  245. package/src/runtime/client/fleet-union.ts +5 -5
  246. package/src/runtime/client/hosted-exit.ts +2 -2
  247. package/src/runtime/client/hosted-sessions.ts +1 -1
  248. package/src/runtime/client/spine-adoption.ts +1 -1
  249. package/src/runtime/context-accounting-source.ts +5 -4
  250. package/src/runtime/index.ts +1 -0
  251. package/src/runtime/notification-dispatch.ts +102 -29
  252. package/src/runtime/recovery-offer-wiring.ts +4 -1
  253. package/src/runtime/recovery-prompt.ts +31 -15
  254. package/src/runtime/runtime-services-types.ts +10 -6
  255. package/src/runtime/services.ts +17 -12
  256. package/src/runtime/session-continuity-hints.ts +2 -5
  257. package/src/runtime/session-inbound-inputs.ts +1 -1
  258. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  259. package/src/runtime/startup-typeahead-gate.ts +103 -0
  260. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  261. package/src/runtime/store/domains/index.ts +0 -8
  262. package/src/runtime/store/selectors/index.ts +0 -28
  263. package/src/runtime/store/state.ts +0 -8
  264. package/src/runtime/ui-services.ts +4 -6
  265. package/src/runtime/usage-tracker.ts +348 -0
  266. package/src/runtime/wrfc-persistence.ts +4 -4
  267. package/src/shell/blocking-input.ts +92 -2
  268. package/src/shell/recovery-input-helpers.ts +1 -1
  269. package/src/shell/session-views.ts +539 -0
  270. package/src/shell/status-report.ts +86 -0
  271. package/src/shell/transcript-scroll.ts +68 -0
  272. package/src/shell/ui-openers.ts +84 -172
  273. package/src/shell/view-openers.ts +205 -0
  274. package/src/tools/wrfc-agent-guard.ts +22 -2
  275. package/src/utils/format-duration.ts +4 -8
  276. package/src/utils/splash-lines.ts +9 -1
  277. package/src/utils/terminal-width.ts +56 -0
  278. package/src/verification/live-verifier.ts +0 -10
  279. package/src/version.ts +1 -1
  280. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  281. package/src/{panels → views}/builtin-modals.ts +22 -21
  282. package/src/views/builtin-views.ts +135 -0
  283. package/src/{panels → views}/confirm-state.ts +9 -9
  284. package/src/{panels → views}/diff-review-model.ts +10 -3
  285. package/src/{panels → views}/eval-registry.ts +1 -1
  286. package/src/{panels → views}/fleet-acts.ts +49 -85
  287. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  288. package/src/{panels → views}/fleet-gateway.ts +5 -5
  289. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  290. package/src/{panels → views}/fleet-read-model.ts +9 -9
  291. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  292. package/src/{panels → views}/fleet-spawn.ts +33 -49
  293. package/src/{panels → views}/fleet-steer.ts +12 -12
  294. package/src/{panels → views}/fleet-stop.ts +4 -4
  295. package/src/{panels → views}/fleet-transcript.ts +26 -27
  296. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  297. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  298. package/src/views/modal-surface-registry.ts +41 -0
  299. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  300. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  301. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  302. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  303. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  304. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  305. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  306. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  307. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  308. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  309. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  310. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  311. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  312. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  313. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  314. package/src/{panels → views}/modals/security-modal.ts +22 -16
  315. package/src/{panels → views}/modals/services-modal.ts +3 -3
  316. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  317. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  318. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  319. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  320. package/src/views/notifications-feed.ts +314 -0
  321. package/src/{panels → views}/polish-core.ts +38 -36
  322. package/src/{panels → views}/polish-tables.ts +35 -26
  323. package/src/{panels → views}/polish.ts +101 -95
  324. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  325. package/src/{panels → views}/provider-health-routes.ts +14 -14
  326. package/src/{panels → views}/search-focus.ts +8 -8
  327. package/src/{panels → views}/session-maintenance.ts +11 -11
  328. package/src/views/skills-discovery.ts +159 -0
  329. package/src/views/view-deps.ts +80 -0
  330. package/src/views/view-panel-adapter.ts +53 -0
  331. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  332. package/src/core/conversation-fold.ts +0 -101
  333. package/src/core/conversation-turn-structure.ts +0 -464
  334. package/src/core/conversation-user-receipts.ts +0 -42
  335. package/src/input/commands/operator-panel-runtime.ts +0 -186
  336. package/src/input/panel-integration-actions.ts +0 -33
  337. package/src/input/panel-mouse-geometry.ts +0 -97
  338. package/src/input/panel-paste-flood-guard.ts +0 -86
  339. package/src/panels/base-panel.ts +0 -301
  340. package/src/panels/builtin/agent.ts +0 -27
  341. package/src/panels/builtin/development.ts +0 -92
  342. package/src/panels/builtin/knowledge.ts +0 -12
  343. package/src/panels/builtin/operations.ts +0 -177
  344. package/src/panels/builtin/session.ts +0 -81
  345. package/src/panels/builtin/shared.ts +0 -357
  346. package/src/panels/builtin-panels.ts +0 -28
  347. package/src/panels/cost-tracker-panel.ts +0 -746
  348. package/src/panels/diff-panel.ts +0 -797
  349. package/src/panels/diff-review-panel.ts +0 -395
  350. package/src/panels/expandable-list-panel.ts +0 -189
  351. package/src/panels/fleet-deep-link.ts +0 -31
  352. package/src/panels/fleet-panel.ts +0 -799
  353. package/src/panels/git-panel.ts +0 -800
  354. package/src/panels/hosted-session-panel.ts +0 -221
  355. package/src/panels/index.ts +0 -21
  356. package/src/panels/local-auth-panel.ts +0 -478
  357. package/src/panels/notifications-feed.ts +0 -127
  358. package/src/panels/notifications-panel.ts +0 -160
  359. package/src/panels/panel-confirm-overlay.ts +0 -72
  360. package/src/panels/panel-manager.ts +0 -791
  361. package/src/panels/plugins-panel.ts +0 -371
  362. package/src/panels/scrollable-list-panel.ts +0 -498
  363. package/src/panels/skills-panel.ts +0 -454
  364. package/src/panels/token-budget-panel.ts +0 -688
  365. package/src/panels/types.ts +0 -191
  366. package/src/panels/wrfc-panel-format.ts +0 -133
  367. package/src/renderer/diff-view.ts +0 -134
  368. package/src/renderer/fleet-tab-strip.ts +0 -56
  369. package/src/renderer/footer-tips.ts +0 -53
  370. package/src/renderer/fullscreen-primitives.ts +0 -146
  371. package/src/renderer/fullscreen-workspace.ts +0 -194
  372. package/src/renderer/hint-grammar.ts +0 -52
  373. package/src/renderer/modal-factory.ts +0 -480
  374. package/src/renderer/overlay-box.ts +0 -154
  375. package/src/renderer/overlay-viewport.ts +0 -104
  376. package/src/renderer/panel-composite.ts +0 -145
  377. package/src/renderer/panel-workspace-bar.ts +0 -54
  378. package/src/renderer/process-indicator.ts +0 -163
  379. package/src/renderer/tab-strip.ts +0 -222
  380. package/src/renderer/text-layout.ts +0 -31
  381. package/src/renderer/tool-call.ts +0 -276
  382. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  383. package/src/renderer/tool-result-summary.ts +0 -219
  384. package/src/runtime/diagnostics/panels/index.ts +0 -21
  385. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  386. package/src/runtime/perf/panel-contracts.ts +0 -32
  387. package/src/runtime/store/domains/panels.ts +0 -117
  388. package/src/verification/verification-ledger.ts +0 -707
  389. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  390. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  391. /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
+ }