@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
@@ -225,6 +225,14 @@ export function buildFilteredModels(
225
225
  result = [...recent, ...rest];
226
226
  }
227
227
 
228
+ // Pinned models lead (before recent), so the picker's Pinned group sits at
229
+ // the top and up/down walks the list in the order it is drawn. Pins may be
230
+ // recorded by model id or by registry key.
231
+ if (pinnedIds.size > 0) {
232
+ const isPinned = (m: ModelDefinition): boolean => pinnedIds.has(m.id) || pinnedIds.has(m.registryKey);
233
+ result = [...result.filter(isPinned), ...result.filter((m) => !isPinned(m))];
234
+ }
235
+
228
236
  const newCache: FilteredModelsCache = {
229
237
  modelsRef: models,
230
238
  configuredProvidersKey,
@@ -0,0 +1,99 @@
1
+ /**
2
+ * model-picker-open.ts, opening the model picker without waiting on slow reads.
3
+ *
4
+ * The picker's model and provider lists come from the provider registry's
5
+ * cached catalog, which is synchronous. What used to hold the open back for
6
+ * seconds were the reads awaited before the modal appeared: one credential
7
+ * store lookup per configured provider (which credential source configured
8
+ * it) and the embedding registry's status probe (the Embeddings tab). Those
9
+ * only decorate rows or feed other tabs, so the picker now opens at once on
10
+ * the cached catalog with a muted "loading catalog…" row, and those reads,
11
+ * plus the live re-check of each provider's served models, fill in when they
12
+ * land. A close or a newer open drops any load still in flight.
13
+ */
14
+
15
+ import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers';
16
+ import type {
17
+ EmbeddingProviderPickerEntry,
18
+ ModelPickerCatalogFill,
19
+ ModelPickerModal,
20
+ ModelPickerTargetInfo,
21
+ } from './model-picker.ts';
22
+
23
+ type ConfiguredVia = Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'>;
24
+
25
+ export interface ModelPickerOpenDeps {
26
+ readonly picker: ModelPickerModal;
27
+ /** Register the picker as the open modal (input focus, modal stack). */
28
+ readonly modalOpened: () => void;
29
+ readonly render: () => void;
30
+ /** The selectable models from the cached catalog (synchronous). */
31
+ readonly listModels: () => ModelDefinition[];
32
+ /** The selectable provider ids from the cached catalog (synchronous). */
33
+ readonly listProviders: () => string[];
34
+ readonly currentModelId: () => string;
35
+ readonly currentProviderId: () => string;
36
+ readonly configuredProviderIds: () => ReadonlySet<string>;
37
+ /** Which credential source configured each provider, given the providers the credential store holds keys for. */
38
+ readonly buildConfiguredVia: (providerIds: readonly string[], configured: ReadonlySet<string>, secretProviderIds: ReadonlySet<string>) => ConfiguredVia;
39
+ readonly buildTargets: (embeddingProviders: EmbeddingProviderPickerEntry[]) => ModelPickerTargetInfo[];
40
+ /** Slow: one credential store read per configured provider. */
41
+ readonly resolveSecretProviderIds: () => Promise<ReadonlySet<string>>;
42
+ /** Slow: the embedding registry's status probe. */
43
+ readonly resolveEmbeddingProviders: () => Promise<EmbeddingProviderPickerEntry[]>;
44
+ /** Optional live re-check of each provider's served models; resolves true when a list changed. */
45
+ readonly refreshLiveModels?: () => Promise<boolean>;
46
+ /** Best-effort prefetches (pinned and recent models); failures stay silent. */
47
+ readonly prefetch?: () => Promise<void>;
48
+ readonly onError: (error: unknown) => void;
49
+ }
50
+
51
+ /**
52
+ * Open the picker on the cached catalog right now (it is visible when this
53
+ * returns), then fill in the slow parts. `mode` picks the model list (/model)
54
+ * or the provider list (/provider). The returned promise settles when every
55
+ * fill has landed or been dropped; callers do not need to await it.
56
+ */
57
+ export function openModelPickerNow(deps: ModelPickerOpenDeps, mode: 'models' | 'providers' = 'models'): Promise<void> {
58
+ const { picker } = deps;
59
+ const configured = new Set(deps.configuredProviderIds());
60
+ const models = deps.listModels();
61
+ const providers = mode === 'providers' ? deps.listProviders() : [...new Set(models.map((m) => m.provider))];
62
+ picker.configuredProviders = configured;
63
+ picker.configuredViaMap = deps.buildConfiguredVia(providers, configured, new Set());
64
+ deps.modalOpened();
65
+ picker.setTargetInfos(deps.buildTargets(picker.embeddingProviders));
66
+ if (mode === 'providers') picker.openProviders(providers, deps.currentProviderId());
67
+ else picker.openAllModels(models, deps.currentModelId());
68
+ const ticket = picker.beginCatalogLoad();
69
+ deps.render();
70
+
71
+ const prefetch = (deps.prefetch?.() ?? Promise.resolve()).catch(() => {}).then(() => deps.render());
72
+
73
+ const decorations = (async () => {
74
+ const [secretIds, embedding] = await Promise.all([deps.resolveSecretProviderIds(), deps.resolveEmbeddingProviders()]);
75
+ const fill: ModelPickerCatalogFill = {
76
+ configuredViaMap: deps.buildConfiguredVia(providers, configured, secretIds),
77
+ embeddingProviders: embedding,
78
+ targetInfos: deps.buildTargets(embedding),
79
+ };
80
+ if (picker.fillCatalog(ticket, fill, false)) deps.render();
81
+ })();
82
+
83
+ const live = (async () => {
84
+ if (!deps.refreshLiveModels) return;
85
+ const changed = await deps.refreshLiveModels().catch(() => false);
86
+ if (!changed) return;
87
+ const fresh = mode === 'providers' ? { providers: deps.listProviders() } : { models: deps.listModels() };
88
+ if (picker.fillCatalog(ticket, fresh, false)) deps.render();
89
+ })();
90
+
91
+ return Promise.all([decorations, live])
92
+ .catch((error: unknown) => deps.onError(error))
93
+ .finally(() => {
94
+ // Loaded or failed, the loading row goes: the picker keeps what it has.
95
+ if (picker.fillCatalog(ticket, {})) deps.render();
96
+ return prefetch;
97
+ })
98
+ .then(() => {});
99
+ }
@@ -13,7 +13,7 @@ export type PickerMode = 'model' | 'provider' | 'effort' | 'contextCap' | 'embed
13
13
  */
14
14
  export type ModelPickerTarget = 'main' | 'helper' | 'tool' | 'tts' | 'embeddings';
15
15
 
16
- export type ModelPickerFocusPane = 'targets' | 'items';
16
+ export type ModelPickerFocusRegion = 'targets' | 'items';
17
17
 
18
18
  export interface ModelPickerTargetInfo {
19
19
  readonly target: ModelPickerTarget;
@@ -6,6 +6,7 @@ import {
6
6
  } from '../providers/reasoning-effort-surface.ts';
7
7
  import type { FavoritesStore } from '@pellux/goodvibes-sdk/platform/providers';
8
8
  import type { BenchmarkStore } from '@pellux/goodvibes-sdk/platform/providers';
9
+ import { compositeScore } from '@pellux/goodvibes-sdk/platform/providers';
9
10
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
10
11
  import { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
11
12
  import type {
@@ -17,7 +18,7 @@ import type {
17
18
  FilteredProvidersCache,
18
19
  GroupByMode,
19
20
  ModelItemsCache,
20
- ModelPickerFocusPane,
21
+ ModelPickerFocusRegion,
21
22
  ModelPickerTarget,
22
23
  ModelPickerTargetInfo,
23
24
  PickerItem,
@@ -42,6 +43,15 @@ import {
42
43
  } from './model-picker-items.ts';
43
44
 
44
45
  export { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
46
+
47
+ /** Catalog data that arrives after the picker opened (see ModelPickerModal.fillCatalog). */
48
+ export interface ModelPickerCatalogFill {
49
+ readonly models?: ModelDefinition[];
50
+ readonly providers?: string[];
51
+ readonly configuredViaMap?: Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'>;
52
+ readonly embeddingProviders?: EmbeddingProviderPickerEntry[];
53
+ readonly targetInfos?: ModelPickerTargetInfo[];
54
+ }
45
55
  export type {
46
56
  BenchmarkSort,
47
57
  CapabilityFilter,
@@ -49,7 +59,7 @@ export type {
49
59
  EmbeddingProviderPickerEntry,
50
60
  GroupByMode,
51
61
  ModelFamily,
52
- ModelPickerFocusPane,
62
+ ModelPickerFocusRegion,
53
63
  ModelPickerTarget,
54
64
  ModelPickerTargetInfo,
55
65
  PickerItem,
@@ -81,7 +91,7 @@ export class ModelPickerModal {
81
91
  public mode: PickerMode = 'model';
82
92
  /** Which config target this picker session will write to on commit. */
83
93
  public target: ModelPickerTarget = 'main';
84
- public focusPane: ModelPickerFocusPane = 'items';
94
+ public focusRegion: ModelPickerFocusRegion = 'items';
85
95
  public targetInfos: ModelPickerTargetInfo[] = [];
86
96
  public targetIndex = 0;
87
97
  public searchFocused = false;
@@ -135,6 +145,15 @@ export class ModelPickerModal {
135
145
  /** Current group-by mode. */
136
146
  public groupBy: GroupByMode = 'provider';
137
147
 
148
+ /**
149
+ * True from the moment the picker opens on its cached catalog until the
150
+ * slower reads (credential sources, embedding providers, the live model
151
+ * lists) land through fillCatalog. The list shows a muted "loading catalog…"
152
+ * row meanwhile; the picker is usable the whole time.
153
+ */
154
+ public catalogLoading = false;
155
+ private catalogTicket = 0;
156
+
138
157
  private filteredModelsCache: FilteredModelsCache | null = null;
139
158
  private filteredProvidersCache: FilteredProvidersCache | null = null;
140
159
  private modelItemsCache: ModelItemsCache | null = null;
@@ -151,12 +170,12 @@ export class ModelPickerModal {
151
170
  }
152
171
 
153
172
  focusTargets(): void {
154
- this.focusPane = 'targets';
173
+ this.focusRegion = 'targets';
155
174
  this.searchFocused = false;
156
175
  }
157
176
 
158
177
  focusItems(): void {
159
- this.focusPane = 'items';
178
+ this.focusRegion = 'items';
160
179
  }
161
180
 
162
181
  moveTarget(delta: number): void {
@@ -294,7 +313,7 @@ export class ModelPickerModal {
294
313
  this.mode = 'model';
295
314
  this.active = true;
296
315
  this.pendingModel = null;
297
- this.focusPane = 'items';
316
+ this.focusRegion = 'items';
298
317
  this.searchFocused = false;
299
318
  this.focusSearch();
300
319
  this.query = '';
@@ -314,7 +333,7 @@ export class ModelPickerModal {
314
333
  this.mode = 'provider';
315
334
  this.active = true;
316
335
  this.pendingModel = null;
317
- this.focusPane = 'items';
336
+ this.focusRegion = 'items';
318
337
  this.searchFocused = false;
319
338
  this.focusSearch();
320
339
  this.query = '';
@@ -339,7 +358,7 @@ export class ModelPickerModal {
339
358
  this.mode = 'embeddingProvider';
340
359
  this.active = true;
341
360
  this.pendingModel = null;
342
- this.focusPane = 'items';
361
+ this.focusRegion = 'items';
343
362
  this.searchFocused = false;
344
363
  this.query = '';
345
364
  const idx = providers.findIndex((provider) => provider.id === currentId);
@@ -379,12 +398,53 @@ export class ModelPickerModal {
379
398
  this.scrollOffset = 0;
380
399
  }
381
400
 
401
+ /** Mark the catalog as loading; the returned ticket lets fillCatalog drop a load that a close or a newer open overtook. */
402
+ beginCatalogLoad(): number {
403
+ this.catalogLoading = true;
404
+ return ++this.catalogTicket;
405
+ }
406
+
407
+ /**
408
+ * Land catalog data that finished loading after the picker opened. The
409
+ * query, filters and the selected row (matched by id) stay as they are.
410
+ * Returns false, changing nothing, when the picker closed or a newer load
411
+ * started since `ticket` was issued.
412
+ */
413
+ fillCatalog(ticket: number, fill: ModelPickerCatalogFill, done = true): boolean {
414
+ if (!this.active || ticket !== this.catalogTicket) return false;
415
+ const selectedId = this.selectedItemId();
416
+ if (fill.models) this.models = fill.models;
417
+ if (fill.providers) this.providers = fill.providers;
418
+ if (fill.configuredViaMap) this.configuredViaMap = fill.configuredViaMap;
419
+ if (fill.embeddingProviders) this.embeddingProviders = fill.embeddingProviders;
420
+ if (fill.targetInfos) this.targetInfos = fill.targetInfos;
421
+ this.clearCaches();
422
+ if (selectedId !== null && this.mode !== 'effort') {
423
+ const ids = this.mode === 'model' ? this.getFilteredModels().map((m) => m.id)
424
+ : this.mode === 'provider' ? this.getFilteredProviders()
425
+ : this.embeddingProviders.map((provider) => provider.id);
426
+ const idx = ids.indexOf(selectedId);
427
+ this.selectedIndex = idx >= 0 ? idx : Math.min(this.selectedIndex, Math.max(0, ids.length - 1));
428
+ }
429
+ if (done) this.catalogLoading = false;
430
+ return true;
431
+ }
432
+
433
+ private selectedItemId(): string | null {
434
+ if (this.mode === 'model') return this.getFilteredModels()[this.selectedIndex]?.id ?? null;
435
+ if (this.mode === 'provider') return this.getFilteredProviders()[this.selectedIndex] ?? null;
436
+ if (this.mode === 'embeddingProvider') return this.embeddingProviders[this.selectedIndex]?.id ?? null;
437
+ return null;
438
+ }
439
+
382
440
  /** Close the picker entirely. */
383
441
  close(): void {
442
+ this.catalogLoading = false;
443
+ this.catalogTicket++;
384
444
  this.active = false;
385
445
  this.mode = 'model';
386
446
  this.target = 'main';
387
- this.focusPane = 'items';
447
+ this.focusRegion = 'items';
388
448
  this.targetInfos = [];
389
449
  this.targetIndex = 0;
390
450
  this.models = [];
@@ -407,6 +467,29 @@ export class ModelPickerModal {
407
467
 
408
468
  // ── Search helpers ─────────────────────────────────────────────────────────────────────
409
469
 
470
+ /** Composite benchmark score (0..1) for a model, or null when there is no benchmark data. */
471
+ benchmarkScore(model: ModelDefinition): number | null {
472
+ const record = this.benchmarkStore.getBenchmarks(model.id) ?? this.benchmarkStore.getBenchmarks(model.displayName);
473
+ return record ? compositeScore(record.benchmarks) : null;
474
+ }
475
+
476
+ /** Whether a model is pinned (pins may be recorded by id or registry key). */
477
+ isPinned(model: ModelDefinition): boolean {
478
+ return this.pinnedIds.has(model.id) || this.pinnedIds.has(model.registryKey);
479
+ }
480
+
481
+ /** Flip a model's pin locally (the caller persists it); returns the new state. */
482
+ togglePinnedLocal(model: ModelDefinition): boolean {
483
+ const pinned = this.isPinned(model);
484
+ const next = new Set(this.pinnedIds);
485
+ next.delete(model.id);
486
+ next.delete(model.registryKey);
487
+ if (!pinned) next.add(model.registryKey);
488
+ this.pinnedIds = next;
489
+ this.clearCaches();
490
+ return !pinned;
491
+ }
492
+
410
493
  /** Append a character to the search query and clamp selectedIndex. */
411
494
  appendChar(ch: string): void {
412
495
  this.query += ch;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * notifications-modal.ts, the notification history (/notifications, ctrl+p
3
+ * "notifications").
4
+ *
5
+ * ↑↓ move, Enter goes to what the notification is about (when it is about
6
+ * something that can be opened), x dismisses the selected one, ctrl+x clears
7
+ * them all. Opening the modal marks everything as seen.
8
+ */
9
+
10
+ import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
11
+ import type { SurfaceModal, SurfaceModalHost } from './surface-modal-host.ts';
12
+ import type { SurfaceLayer } from '../renderer/surface-kit.ts';
13
+ import { renderNotificationsModal, type NotificationsModalView } from '../renderer/notifications-modal.ts';
14
+ import type { NotificationFeedEntry, NotificationFeed } from '../views/notifications-feed.ts';
15
+
16
+ export interface NotificationsModalOptions {
17
+ readonly feed: NotificationFeed;
18
+ /**
19
+ * The opener for the view a notification is about (views.ts resolves the
20
+ * name), or null when nothing holds it.
21
+ */
22
+ readonly resolveSubject: (subject: string) => (() => void) | null;
23
+ /** Clock (tests pass a fixed one). */
24
+ readonly now?: () => number;
25
+ }
26
+
27
+ export class NotificationsModal implements SurfaceModal, NotificationsModalView {
28
+ readonly name = 'notifications';
29
+ selectedIndex = 0;
30
+ status: string | null = null;
31
+ /** Unread when the modal opened (opening marks everything seen). */
32
+ readonly unread: number;
33
+ private readonly unreadKeys: ReadonlySet<string>;
34
+ private readonly unsubscribe: () => void;
35
+
36
+ constructor(private readonly options: NotificationsModalOptions) {
37
+ const feed = options.feed;
38
+ this.unread = feed.unreadCount();
39
+ this.unreadKeys = new Set(feed.list().filter((entry) => feed.isUnread(entry)).map((entry) => entry.key));
40
+ feed.markAllSeen();
41
+ this.unsubscribe = feed.subscribe(() => {
42
+ this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.entries.length - 1));
43
+ });
44
+ }
45
+
46
+ get entries(): readonly NotificationFeedEntry[] {
47
+ return this.options.feed.list();
48
+ }
49
+
50
+ get now(): number {
51
+ return (this.options.now ?? Date.now)();
52
+ }
53
+
54
+ isUnread = (entry: NotificationFeedEntry): boolean => this.unreadKeys.has(entry.key);
55
+
56
+ onClose(): void {
57
+ this.unsubscribe();
58
+ this.options.feed.markAllSeen();
59
+ }
60
+
61
+ private selected(): NotificationFeedEntry | undefined {
62
+ return this.entries[this.selectedIndex];
63
+ }
64
+
65
+ private move(delta: number): void {
66
+ const count = this.entries.length;
67
+ if (count === 0) return;
68
+ this.selectedIndex = Math.max(0, Math.min(count - 1, this.selectedIndex + delta));
69
+ }
70
+
71
+ private dismissSelected(): void {
72
+ const entry = this.selected();
73
+ if (!entry) return;
74
+ this.options.feed.dismiss(entry.key);
75
+ this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.entries.length - 1));
76
+ this.status = `Dismissed "${entry.title}".`;
77
+ }
78
+
79
+ private goTo(host: SurfaceModalHost): void {
80
+ const entry = this.selected();
81
+ if (!entry) return;
82
+ if (!entry.subject) {
83
+ this.status = 'This notification is not about anything that can be opened.';
84
+ return;
85
+ }
86
+ const open = this.options.resolveSubject(entry.subject);
87
+ if (!open) {
88
+ this.status = `Nothing to open for "${entry.subject}".`;
89
+ return;
90
+ }
91
+ host.close(this, 'done');
92
+ open();
93
+ }
94
+
95
+ handleToken(token: InputToken, host: SurfaceModalHost): void {
96
+ if (token.type === 'text') {
97
+ if (token.value === 'x') this.dismissSelected();
98
+ else if (token.value === 'j') this.move(1);
99
+ else if (token.value === 'k') this.move(-1);
100
+ return;
101
+ }
102
+ if (token.type !== 'key') return;
103
+ const key = token.logicalName ?? '';
104
+ if (token.ctrl && key === 'x') {
105
+ const count = this.entries.length;
106
+ this.options.feed.clear();
107
+ this.selectedIndex = 0;
108
+ this.status = count > 0 ? `Cleared ${count} notification${count === 1 ? '' : 's'}.` : null;
109
+ return;
110
+ }
111
+ if (key === 'up') this.move(-1);
112
+ else if (key === 'down') this.move(1);
113
+ else if (key === 'pageup') this.move(-10);
114
+ else if (key === 'pagedown') this.move(10);
115
+ else if (key === 'home') this.selectedIndex = 0;
116
+ else if (key === 'end') this.selectedIndex = Math.max(0, this.entries.length - 1);
117
+ else if (key === 'enter') this.goTo(host);
118
+ }
119
+
120
+ render(screenWidth: number, screenHeight: number): SurfaceLayer {
121
+ return renderNotificationsModal(this, screenWidth, screenHeight);
122
+ }
123
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * overlay-filter.ts, the always-live search row state for the help and
3
+ * keyboard-shortcuts overlays. Their visibility and scroll offsets are plain
4
+ * handler fields copied through the feed context; the query lives in these
5
+ * long-lived objects instead, shared by reference between the input route
6
+ * (which types into it) and the renderer (which filters with it and records
7
+ * how far the list can scroll, so ↓ never runs past the end).
8
+ */
9
+
10
+ export class OverlayFilter {
11
+ /** Typed filter text. */
12
+ public query = '';
13
+ /** Largest useful scroll offset for the current query, recorded by the renderer. */
14
+ public maxScroll = 0;
15
+
16
+ clear(): void {
17
+ this.query = '';
18
+ this.maxScroll = 0;
19
+ }
20
+ }
21
+
22
+ /** One filter per overlay. */
23
+ export class OverlayFilters {
24
+ public readonly help = new OverlayFilter();
25
+ public readonly shortcuts = new OverlayFilter();
26
+ }
@@ -57,6 +57,8 @@ export class ProfilePickerModal {
57
57
 
58
58
  /** Last status message (success/error feedback). */
59
59
  public statusMessage = '';
60
+ /** The always-live search row's query; selectedIndex indexes visibleProfiles. */
61
+ public query = '';
60
62
 
61
63
  public constructor(private readonly profileManager: ProfileManager) {}
62
64
 
@@ -69,6 +71,7 @@ export class ProfilePickerModal {
69
71
  this.scrollOffset = 0;
70
72
  this.statusMessage = '';
71
73
  this.deleteConfirmationTarget = null;
74
+ this.query = '';
72
75
  this.active = true;
73
76
  }
74
77
 
@@ -76,18 +79,44 @@ export class ProfilePickerModal {
76
79
  this.active = false;
77
80
  this.statusMessage = '';
78
81
  this.deleteConfirmationTarget = null;
82
+ this.query = '';
83
+ }
84
+
85
+ /** The profiles the search row lets through, in order. */
86
+ get visibleProfiles(): ProfileInfo[] {
87
+ const q = this.query.trim().toLowerCase();
88
+ if (!q) return this.profiles;
89
+ return this.profiles.filter((profile) => profile.name.toLowerCase().includes(q));
90
+ }
91
+
92
+ /** Replace the query; the selection moves to the first match and any armed delete is dropped. */
93
+ setQuery(query: string): void {
94
+ this.query = query;
95
+ this.selectedIndex = 0;
96
+ this.scrollOffset = 0;
97
+ this.deleteConfirmationTarget = null;
98
+ }
99
+
100
+ /** Esc's first level: drop an armed delete. Returns true when one was armed. */
101
+ cancelDeleteConfirmation(): boolean {
102
+ if (this.deleteConfirmationTarget === null) return false;
103
+ this.deleteConfirmationTarget = null;
104
+ this.statusMessage = '';
105
+ return true;
79
106
  }
80
107
 
81
108
  moveUp(): void {
82
- if (this.profiles.length === 0) return;
83
- this.selectedIndex = (this.selectedIndex - 1 + this.profiles.length) % this.profiles.length;
109
+ const n = this.visibleProfiles.length;
110
+ if (n === 0) return;
111
+ this.selectedIndex = (this.selectedIndex - 1 + n) % n;
84
112
  this._clampScroll();
85
113
  this.deleteConfirmationTarget = null;
86
114
  }
87
115
 
88
116
  moveDown(): void {
89
- if (this.profiles.length === 0) return;
90
- this.selectedIndex = (this.selectedIndex + 1) % this.profiles.length;
117
+ const n = this.visibleProfiles.length;
118
+ if (n === 0) return;
119
+ this.selectedIndex = (this.selectedIndex + 1) % n;
91
120
  this._clampScroll();
92
121
  this.deleteConfirmationTarget = null;
93
122
  }
@@ -98,7 +127,7 @@ export class ProfilePickerModal {
98
127
  }
99
128
 
100
129
  getSelected(): ProfileInfo | null {
101
- return this.profiles[this.selectedIndex] ?? null;
130
+ return this.visibleProfiles[this.selectedIndex] ?? null;
102
131
  }
103
132
 
104
133
  /**
@@ -162,8 +191,8 @@ export class ProfilePickerModal {
162
191
  return false;
163
192
  }
164
193
  this.profiles = this.profileManager.list();
165
- if (this.selectedIndex >= this.profiles.length) {
166
- this.selectedIndex = Math.max(0, this.profiles.length - 1);
194
+ if (this.selectedIndex >= this.visibleProfiles.length) {
195
+ this.selectedIndex = Math.max(0, this.visibleProfiles.length - 1);
167
196
  }
168
197
  this._clampScroll();
169
198
  this.deleteConfirmationTarget = null;
@@ -216,7 +245,7 @@ export class ProfilePickerModal {
216
245
  } else if (this.selectedIndex >= this.scrollOffset + visRows) {
217
246
  this.scrollOffset = this.selectedIndex - visRows + 1;
218
247
  }
219
- const maxOffset = Math.max(0, this.profiles.length - visRows);
248
+ const maxOffset = Math.max(0, this.visibleProfiles.length - visRows);
220
249
  this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxOffset));
221
250
  }
222
251
  }
@@ -146,7 +146,7 @@ export class SearchManager {
146
146
  // attempt to reveal a header-corpus hit, since there is no single
147
147
  // member to isolate it to.
148
148
  const headerHits = countOccurrences(block.rawContent.toLowerCase(), lowerQuery);
149
- const allKeys = [block.collapseKey, ...block.groupMemberIndexes.map((idx) => `msg_${idx}`)];
149
+ const allKeys = [block.collapseKey, ...block.groupMemberIndexes.map((idx, k) => block.groupMemberKeys?.[k] ?? `msg_${idx}`)];
150
150
  for (let i = 0; i < headerHits; i++) {
151
151
  hiddenMatches.push({
152
152
  line: block.startLine, col: 0, length: query.length,
@@ -159,10 +159,10 @@ export class SearchManager {
159
159
  // whole message array and search() runs on every keystroke, so a
160
160
  // per-member read would stall typing outright.
161
161
  const snapshot = conversationManager.getMessageSnapshot();
162
- for (const memberIdx of block.groupMemberIndexes) {
162
+ for (const [k, memberIdx] of block.groupMemberIndexes.entries()) {
163
163
  const content = snapshot[memberIdx]?.content;
164
164
  if (typeof content !== 'string') continue;
165
- const memberKey = `msg_${memberIdx}`;
165
+ const memberKey = block.groupMemberKeys?.[k] ?? `msg_${memberIdx}`;
166
166
  const memberHits = countOccurrences(content.toLowerCase(), lowerQuery);
167
167
  for (let i = 0; i < memberHits; i++) {
168
168
  hiddenMatches.push({
@@ -40,6 +40,8 @@ export interface SelectionOpenOpts {
40
40
  allowSearch?: boolean;
41
41
  customActions?: Map<string, SelectionAction>;
42
42
  primaryVerbLabel?: string;
43
+ /** Called with the highlighted row whenever the cursor lands on a different row (live preview). */
44
+ onHighlight?: (item: SelectionItem | null) => void;
43
45
  }
44
46
 
45
47
  export interface PendingSelectionRequest {