@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
@@ -4,6 +4,7 @@ import type { SelectionResult, SelectionAction } from './selection-modal.ts';
4
4
  import type { CommandContext } from './command-registry.ts';
5
5
  import type { ConfigModal } from './config-modal.ts';
6
6
  import { openTtsProviderPicker, openTtsVoicePicker } from './tts-settings-actions.ts';
7
+ import { openThemePicker } from './theme-settings-actions.ts';
7
8
  import { isTextBackspace } from './delete-key-policy.ts';
8
9
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
9
10
 
@@ -11,14 +12,11 @@ type SelectionRouteState = {
11
12
  selectionModal: {
12
13
  active: boolean;
13
14
  query: string;
14
- searchFocused: boolean;
15
15
  allowSearch: boolean;
16
16
  customActions: Map<string, SelectionAction>;
17
17
  selectedIndex: number;
18
18
  getSelected: () => SelectionResult['item'] | null | undefined;
19
19
  setQuery: (query: string) => void;
20
- focusSearch: () => void;
21
- blurSearch: () => void;
22
20
  moveUp: () => void;
23
21
  moveDown: () => void;
24
22
  close: () => void;
@@ -77,44 +75,42 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
77
75
  return shift ? baseStep * 10 : baseStep;
78
76
  };
79
77
 
78
+ const fireClaimedKey = (key: string): boolean => {
79
+ const action = state.selectionModal.customActions.get(key);
80
+ if (!action) return false;
81
+ const selected = state.selectionModal.getSelected();
82
+ if (selected) dispatchSelectionAction(action, selected);
83
+ return true;
84
+ };
85
+
86
+ const fireSpace = (): void => {
87
+ const selected = state.selectionModal.getSelected();
88
+ const action = getSpaceAction(selected);
89
+ if (action && selected && state.selectionCallback) {
90
+ state.selectionCallback({ item: selected, action });
91
+ }
92
+ };
93
+
94
+ // The search row is always live (no search mode): printable text goes into
95
+ // the query, except a key the picker claims (a custom action, or Space for
96
+ // a toggle) which fires while the query is empty. Pickers without search
97
+ // only have their claimed keys.
80
98
  if (token.type === 'text') {
81
- if (state.selectionModal.allowSearch && !state.selectionModal.searchFocused && token.value === '/') {
82
- state.selectionModal.focusSearch();
83
- } else if (state.selectionModal.allowSearch && state.selectionModal.searchFocused) {
99
+ const queryEmpty = state.selectionModal.query.length === 0;
100
+ const single = [...token.value].length === 1;
101
+ if (queryEmpty && single && token.value === ' ' && getSpaceAction(state.selectionModal.getSelected())) {
102
+ fireSpace();
103
+ } else if (queryEmpty && single && state.selectionModal.customActions.has(token.value)) {
104
+ fireClaimedKey(token.value);
105
+ } else if (state.selectionModal.allowSearch) {
84
106
  state.selectionModal.setQuery(state.selectionModal.query + token.value);
85
- } else if (token.value === ' ') {
86
- const selected = state.selectionModal.getSelected();
87
- const action = getSpaceAction(selected);
88
- if (action && selected && state.selectionCallback) {
89
- state.selectionCallback({ item: selected, action });
90
- }
91
- } else {
92
- const action = state.selectionModal.customActions.get(token.value);
93
- if (action) {
94
- const selected = state.selectionModal.getSelected();
95
- if (selected) {
96
- dispatchSelectionAction(action, selected);
97
- }
98
- } else if (state.selectionModal.allowSearch) {
99
- // instant filter: an unclaimed keystroke arms search AND starts
100
- // the query immediately, instead of silently doing nothing until the
101
- // user discovers '/' first (the "help search needs '/' arming while
102
- // the palette filters instantly" evaluator finding, /help has no
103
- // customActions, so every letter used to be swallowed here). '/'
104
- // above still works too; this is additive, not a replacement, and
105
- // never fires for a claimed hotkey letter (checked first, above).
106
- state.selectionModal.focusSearch();
107
- state.selectionModal.setQuery(token.value);
108
- }
107
+ } else if (single && token.value === ' ') {
108
+ fireSpace();
109
109
  }
110
110
  } else if (token.type === 'key') {
111
111
  if (token.logicalName === 'escape') {
112
- // ONE Escape always closes the modal, regardless of search state.
113
- // Clearing an in-progress query is Backspace's job, not Esc's, the old
114
- // two-stage contract (1st Esc clears query, 2nd blurs search, 3rd
115
- // finally closes) was the "help modal took 3 Escapes after searching"
116
- // evaluator finding (searchFocused is not modal-specific, every
117
- // SelectionModal-based picker had the same multi-Esc pattern).
112
+ // ONE Escape always closes the modal, whatever the query holds. Clearing
113
+ // the query is Backspace's job, not Esc's.
118
114
  state.handleEscape();
119
115
  return true;
120
116
  }
@@ -124,24 +120,12 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
124
120
  dispatchSelectionAction(getPrimaryAction(selected) ?? 'select', selected);
125
121
  }
126
122
  } else if (token.logicalName === 'space') {
127
- const selected = state.selectionModal.getSelected();
128
- const action = getSpaceAction(selected);
129
- if (action && selected && state.selectionCallback) {
130
- state.selectionCallback({ item: selected, action });
131
- }
123
+ fireSpace();
132
124
  } else if (token.logicalName === 'up') {
133
- if (state.selectionModal.allowSearch && !state.selectionModal.searchFocused && state.selectionModal.selectedIndex === 0) {
134
- state.selectionModal.focusSearch();
135
- } else {
136
- state.selectionModal.moveUp();
137
- }
125
+ state.selectionModal.moveUp();
138
126
  } else if (token.logicalName === 'down') {
139
- if (state.selectionModal.allowSearch && state.selectionModal.searchFocused) {
140
- state.selectionModal.blurSearch();
141
- } else {
142
- state.selectionModal.moveDown();
143
- }
144
- } else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.selectionModal.searchFocused) {
127
+ state.selectionModal.moveDown();
128
+ } else if (token.logicalName === 'left' || token.logicalName === 'right') {
145
129
  const selected = state.selectionModal.getSelected();
146
130
  if (selected?.adjustable) {
147
131
  dispatchSelectionAction(
@@ -151,22 +135,16 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
151
135
  );
152
136
  }
153
137
  } else if (isTextBackspace(token.logicalName ?? '')) {
154
- if (state.selectionModal.allowSearch && state.selectionModal.searchFocused && state.selectionModal.query.length > 0) {
138
+ if (state.selectionModal.allowSearch && state.selectionModal.query.length > 0) {
155
139
  state.selectionModal.setQuery(state.selectionModal.query.slice(0, -1));
156
140
  }
157
141
  // 'delete' is intentionally absent here: modal search filters are
158
142
  // end-anchored with no cursor, so forward-delete is a no-op per the
159
143
  // delete-key policy (src/input/delete-key-policy.ts).
160
- } else if (state.selectionModal.allowSearch && !state.selectionModal.searchFocused && token.logicalName === '/') {
161
- state.selectionModal.focusSearch();
162
- } else if (!state.selectionModal.searchFocused && token.logicalName && token.logicalName.length === 1) {
163
- const action = state.selectionModal.customActions.get(token.logicalName);
164
- if (action) {
165
- const selected = state.selectionModal.getSelected();
166
- if (selected) {
167
- dispatchSelectionAction(action, selected);
168
- }
169
- }
144
+ } else if (token.logicalName && [...token.logicalName].length === 1) {
145
+ // A modified letter (a CSI-u chord): fires a claimed action whatever
146
+ // the query holds, it is never typed.
147
+ fireClaimedKey(token.logicalName);
170
148
  }
171
149
  }
172
150
 
@@ -178,6 +156,8 @@ type BookmarkRouteState = {
178
156
  bookmarkModal: {
179
157
  active: boolean;
180
158
  entries: Array<unknown>;
159
+ query: string;
160
+ setQuery: (query: string) => void;
181
161
  moveUp: () => void;
182
162
  moveDown: () => void;
183
163
  getSelected: () => { key: string } | null;
@@ -190,33 +170,49 @@ type BookmarkRouteState = {
190
170
  handleEscape: () => void;
191
171
  };
192
172
 
173
+ /**
174
+ * Bookmarks: the search row is always live. `d` (remove) and `o` (open the
175
+ * saved file) fire while the query is empty; otherwise every printable key is
176
+ * typed into the query. Backspace edits it; Esc closes.
177
+ */
193
178
  export function handleBookmarkModalToken(state: BookmarkRouteState, token: InputToken): boolean {
194
179
  if (!state.bookmarkModal.active) return false;
180
+ const modal = state.bookmarkModal;
181
+
182
+ const act = (key: string): boolean => {
183
+ if (key === 'd') {
184
+ modal.removeSelected();
185
+ if (modal.entries.length === 0) modal.close();
186
+ return true;
187
+ }
188
+ if (key === 'o') {
189
+ modal.openSelectedFile();
190
+ return true;
191
+ }
192
+ return false;
193
+ };
195
194
 
196
195
  if (token.type === 'key') {
197
196
  if (token.logicalName === 'escape') {
198
197
  state.handleEscape();
199
198
  return true;
200
199
  }
201
- if (token.logicalName === 'up') state.bookmarkModal.moveUp();
202
- else if (token.logicalName === 'down') state.bookmarkModal.moveDown();
200
+ if (token.logicalName === 'up') modal.moveUp();
201
+ else if (token.logicalName === 'down') modal.moveDown();
203
202
  else if (token.logicalName === 'enter') {
204
- const entry = state.bookmarkModal.getSelected();
203
+ const entry = modal.getSelected();
205
204
  if (entry) state.commandContext?.jumpToBookmark?.(entry.key);
206
- state.bookmarkModal.close();
207
- } else if (token.logicalName === 'd') {
208
- state.bookmarkModal.removeSelected();
209
- if (state.bookmarkModal.entries.length === 0) state.bookmarkModal.close();
210
- } else if (token.logicalName === 'o') {
211
- state.bookmarkModal.openSelectedFile();
205
+ modal.close();
206
+ } else if (isTextBackspace(token.logicalName ?? '')) {
207
+ if (modal.query.length > 0) modal.setQuery(modal.query.slice(0, -1));
208
+ } else if (token.logicalName) {
209
+ // Modified letters (CSI-u chords) fire their action whatever the query holds.
210
+ act(token.logicalName);
212
211
  }
213
212
  } else if (token.type === 'text') {
214
- if (token.value === 'd') {
215
- state.bookmarkModal.removeSelected();
216
- if (state.bookmarkModal.entries.length === 0) state.bookmarkModal.close();
217
- } else if (token.value === 'o') {
218
- state.bookmarkModal.openSelectedFile();
219
- }
213
+ const claimed = modal.query.length === 0 && (token.value === 'd' || token.value === 'o');
214
+ if (claimed) act(token.value);
215
+ else modal.setQuery(modal.query + token.value);
220
216
  }
221
217
 
222
218
  state.requestRender();
@@ -228,14 +224,14 @@ type SettingsRouteState = {
228
224
  active: boolean;
229
225
  editingMode: boolean;
230
226
  currentCategory: string;
231
- focusPane?: 'categories' | 'settings';
227
+ focusRegion?: 'categories' | 'settings';
232
228
  /** True when the user is actively typing into the search input bar. */
233
229
  searchFocused: boolean;
234
230
  /** Current cross-category search query. */
235
231
  searchQuery: string;
236
232
  commitEdit: () => void;
237
233
  toggleSelectedFlag: () => void;
238
- /** Scroll the documentation pane (PgUp/PgDn); the renderer clamps and shows honest markers. */
234
+ /** Scroll the documentation region (PgUp/PgDn); the renderer clamps and shows honest markers. */
239
235
  scrollContext?: (delta: number) => void;
240
236
  activateSelected: () => void;
241
237
  handleSubscriptionLogoutKey?: (key: string) => 'confirmed' | 'cancelled' | 'absorbed' | 'inactive';
@@ -246,7 +242,7 @@ type SettingsRouteState = {
246
242
  moveDown?: () => void;
247
243
  focusCategories?: () => void;
248
244
  focusSettings?: () => void;
249
- toggleFocusPane?: () => void;
245
+ toggleFocusRegion?: () => void;
250
246
  nextCategory: () => void;
251
247
  prevCategory?: () => void;
252
248
  editBackspace: () => void;
@@ -263,7 +259,7 @@ type SettingsRouteState = {
263
259
  cancelEdit: () => void;
264
260
  pendingModelPickerTarget: import('./model-picker.ts').ModelPickerTarget | null;
265
261
  pendingProviderModelPickerTarget?: import('./model-picker.ts').ModelPickerTarget | null;
266
- pendingSettingsPickerAction?: 'tts-provider' | 'tts-voice' | null;
262
+ pendingSettingsPickerAction?: 'tts-provider' | 'tts-voice' | 'theme' | null;
267
263
  resetSelected?: () => { key: string; value: unknown } | null;
268
264
  initiateResetCategory?: () => void;
269
265
  initiateResetAll?: () => void;
@@ -309,6 +305,10 @@ function consumeSettingsPickerRequest(state: SettingsRouteState): void {
309
305
  openTtsProviderPicker(state.commandContext);
310
306
  return;
311
307
  }
308
+ if (settingsAction === 'theme') {
309
+ openThemePicker(state.commandContext);
310
+ return;
311
+ }
312
312
  void openTtsVoicePicker(state.commandContext).catch((error: unknown) => {
313
313
  state.commandContext?.print(`Unable to list TTS voices: ${summarizeError(error)}`);
314
314
  state.requestRender();
@@ -369,114 +369,104 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
369
369
  }
370
370
  }
371
371
 
372
+ // The search row is always live: printable text goes to the query. While
373
+ // the query is non-empty the list shows search results across every
374
+ // category (searchFocused); clearing it returns to the category view. Space
375
+ // toggles the selected setting only while the query is empty; resets are
376
+ // key chords (ctrl+r selected, shift+R category, ctrl+shift+R all) so every
377
+ // letter can be searched for.
378
+ const modal = state.settingsModal;
379
+ const searching = modal.searchQuery.length > 0;
380
+ const setQuery = (query: string): void => {
381
+ if (query.length === 0) modal.clearSearch();
382
+ else modal.setSearchQuery(query);
383
+ };
384
+ const resetSelected = (): void => {
385
+ const reset = modal.resetSelected?.();
386
+ if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
387
+ };
388
+
372
389
  if (token.type === 'key') {
373
- const focusPane = state.settingsModal.focusPane ?? 'settings';
390
+ const focusRegion = modal.focusRegion ?? 'settings';
374
391
  if (token.logicalName === 'escape') {
375
- // Cancel inline edit first, mirrors the global contract in handler-modal-stack.ts.
376
- // Must check editingMode before searchFocused: the reachable path
377
- // search→Enter(string/number)→Esc must cancel the edit, NOT just clear search.
378
- if (state.settingsModal.editingMode) {
379
- state.settingsModal.cancelEdit();
380
- state.requestRender();
381
- return true;
382
- }
383
- // Two-stage escape: if in search mode, first Esc exits search (clearSearch),
384
- // second Esc closes the modal.
385
- if (state.settingsModal.searchFocused) {
386
- state.settingsModal.clearSearch();
392
+ // One level per press: an inline edit is a sub-dialog and is cancelled
393
+ // first (mirrors handler-modal-stack.ts); otherwise the modal closes.
394
+ if (modal.editingMode) {
395
+ modal.cancelEdit();
387
396
  state.requestRender();
388
397
  return true;
389
398
  }
390
399
  state.handleEscape();
391
400
  return true;
392
401
  }
393
- if (token.logicalName === 'enter' || (token.logicalName === 'space' && !state.settingsModal.editingMode && !state.settingsModal.searchFocused)) {
394
- if (state.settingsModal.editingMode) state.settingsModal.commitEdit();
395
- else if (state.settingsModal.searchFocused) {
396
- // Enter in search mode: activate the selected search result
397
- state.settingsModal.activateSelected();
398
- consumeSettingsPickerRequest(state);
399
- } else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
402
+ if (token.logicalName === 'enter' || (token.logicalName === 'space' && !modal.editingMode && !searching)) {
403
+ if (modal.editingMode) modal.commitEdit();
404
+ else if (focusRegion === 'categories' && !searching) modal.focusSettings?.();
400
405
  else {
401
- // Feature-unit toggle headers are boolean settings rows now, so
402
- // activateSelected toggles them (via the flag-toggle chokepoint) the same
403
- // way it edits/toggles any other setting, no flags-category special case.
404
- state.settingsModal.activateSelected();
406
+ // Feature-unit toggle headers are boolean settings rows, so
407
+ // activateSelected toggles them the same way it edits/toggles any
408
+ // other setting (search results included).
409
+ modal.activateSelected();
405
410
  consumeSettingsPickerRequest(state);
406
411
  }
407
- } else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
408
- if (token.logicalName === 'left') state.settingsModal.focusCategories?.();
409
- else state.settingsModal.focusSettings?.();
412
+ } else if (token.logicalName === 'space' && searching && !modal.editingMode) {
413
+ setQuery(`${modal.searchQuery} `);
414
+ } else if ((token.logicalName === 'left' || token.logicalName === 'right') && !modal.editingMode && !searching) {
415
+ if (token.logicalName === 'left') modal.focusCategories?.();
416
+ else modal.focusSettings?.();
410
417
  } else if (token.logicalName === 'up') {
411
- if (state.settingsModal.searchFocused) {
412
- state.settingsModal.moveUp?.();
413
- } else if (state.settingsModal.moveFocusedUp) state.settingsModal.moveFocusedUp();
414
- else state.settingsModal.moveUp?.();
418
+ if (searching) modal.moveUp?.();
419
+ else if (modal.moveFocusedUp) modal.moveFocusedUp();
420
+ else modal.moveUp?.();
415
421
  } else if (token.logicalName === 'down') {
416
- if (state.settingsModal.searchFocused) {
417
- state.settingsModal.moveDown?.();
418
- } else if (state.settingsModal.moveFocusedDown) state.settingsModal.moveFocusedDown();
419
- else state.settingsModal.moveDown?.();
422
+ if (searching) modal.moveDown?.();
423
+ else if (modal.moveFocusedDown) modal.moveFocusedDown();
424
+ else modal.moveDown?.();
420
425
  }
421
- else if (token.logicalName === 'r' && token.shift && token.ctrl && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
422
- state.settingsModal.initiateResetAll?.();
426
+ else if (token.logicalName === 'r' && token.shift && token.ctrl && !modal.editingMode && !searching) {
427
+ modal.initiateResetAll?.();
423
428
  }
424
- else if (token.logicalName === 'r' && token.shift && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
425
- state.settingsModal.initiateResetCategory?.();
429
+ else if (token.logicalName === 'r' && token.ctrl && !modal.editingMode) {
430
+ resetSelected();
426
431
  }
427
- else if (token.logicalName === 'r' && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
428
- const reset = state.settingsModal.resetSelected?.();
429
- if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
432
+ else if (token.logicalName === 'r' && token.shift && !modal.editingMode && !searching) {
433
+ modal.initiateResetCategory?.();
430
434
  }
431
- else if (token.logicalName === 'pageup' && !state.settingsModal.editingMode) {
432
- state.settingsModal.scrollContext?.(-3);
435
+ else if (token.logicalName === 'pageup' && !modal.editingMode) {
436
+ modal.scrollContext?.(-3);
433
437
  }
434
- else if (token.logicalName === 'pagedown' && !state.settingsModal.editingMode) {
435
- state.settingsModal.scrollContext?.(3);
438
+ else if (token.logicalName === 'pagedown' && !modal.editingMode) {
439
+ modal.scrollContext?.(3);
436
440
  }
437
- else if (token.logicalName === 'tab' && !state.settingsModal.searchFocused) {
438
- if (state.settingsModal.toggleFocusPane) state.settingsModal.toggleFocusPane();
439
- else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
440
- else state.settingsModal.focusCategories?.();
441
+ else if (token.logicalName === 'tab' && !searching) {
442
+ if (modal.toggleFocusRegion) modal.toggleFocusRegion();
443
+ else if (focusRegion === 'categories') modal.focusSettings?.();
444
+ else modal.focusCategories?.();
441
445
  }
442
446
  else if (isTextBackspace(token.logicalName ?? '')) {
443
- if (state.settingsModal.editingMode) {
444
- state.settingsModal.editBackspace();
445
- } else if (state.settingsModal.searchFocused) {
446
- // Backspace in search mode: trim query
447
- const trimmed = state.settingsModal.searchQuery.slice(0, -1);
448
- state.settingsModal.setSearchQuery(trimmed);
449
- }
447
+ if (modal.editingMode) modal.editBackspace();
448
+ else if (searching) setQuery(modal.searchQuery.slice(0, -1));
450
449
  }
451
450
  // token.logicalName === 'delete' is intentionally absent: search filters
452
451
  // are end-anchored with no cursor, so forward-delete is a no-op per
453
452
  // delete-key policy (src/input/delete-key-policy.ts).
454
- else if (!state.settingsModal.editingMode && !state.settingsModal.searchFocused && token.logicalName === '/') {
455
- state.settingsModal.focusSearch();
456
- }
457
453
  } else if (token.type === 'text') {
458
- if (state.settingsModal.editingMode) {
459
- // editingMode takes priority over search, Enter on a string/number search
460
- // result enters inline edit; subsequent chars must go to editChar, not the query.
461
- state.settingsModal.editChar(token.value);
462
- } else if (state.settingsModal.searchFocused) {
463
- // Any printable char in search mode appends to the query
464
- state.settingsModal.setSearchQuery(state.settingsModal.searchQuery + token.value);
465
- } else if (token.value === '/' && !state.settingsModal.editingMode) {
466
- // / enters search mode
467
- state.settingsModal.focusSearch();
468
- } else if (token.value === ' ' && !state.settingsModal.editingMode) {
469
- const focusPane = state.settingsModal.focusPane ?? 'settings';
470
- if (focusPane === 'categories') state.settingsModal.focusSettings?.();
454
+ if (modal.editingMode) {
455
+ // Inline edit takes priority over search: characters go to the edit buffer.
456
+ modal.editChar(token.value);
457
+ } else if (searching) {
458
+ setQuery(modal.searchQuery + token.value);
459
+ } else if (token.value === ' ') {
460
+ const focusRegion = modal.focusRegion ?? 'settings';
461
+ if (focusRegion === 'categories') modal.focusSettings?.();
471
462
  else {
472
- // See the Enter/Space handler above: feature-unit headers toggle through
473
- // activateSelected now, so no flags-category special case is needed.
474
- state.settingsModal.activateSelected();
463
+ modal.activateSelected();
475
464
  consumeSettingsPickerRequest(state);
476
465
  }
477
- } else if (token.value === 'r') {
478
- const reset = state.settingsModal.resetSelected?.();
479
- if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
466
+ } else if (token.value !== '/') {
467
+ // '/' used to arm search; search is always live now, so it is ignored
468
+ // as a first character rather than searched for.
469
+ setQuery(token.value);
480
470
  }
481
471
  }
482
472
 
@@ -487,6 +477,8 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
487
477
  type SessionPickerRouteState = {
488
478
  sessionPickerModal: {
489
479
  active: boolean;
480
+ query: string;
481
+ setQuery: (query: string) => void;
490
482
  loadSelected: (conversationManager: CommandContext['session']['conversationManager']) => void;
491
483
  moveUp: () => void;
492
484
  moveDown: () => void;
@@ -497,8 +489,14 @@ type SessionPickerRouteState = {
497
489
  handleEscape: () => void;
498
490
  };
499
491
 
492
+ /**
493
+ * The session picker's search row is always live: printable keys go to the
494
+ * query. `d` arms (then confirms) delete only while the query is empty, so a
495
+ * search can still contain the letter d.
496
+ */
500
497
  export function handleSessionPickerToken(state: SessionPickerRouteState, token: InputToken): boolean {
501
498
  if (!state.sessionPickerModal.active) return false;
499
+ const modal = state.sessionPickerModal;
502
500
 
503
501
  if (token.type === 'key') {
504
502
  if (token.logicalName === 'escape') {
@@ -508,13 +506,15 @@ export function handleSessionPickerToken(state: SessionPickerRouteState, token:
508
506
  if (token.logicalName === 'enter') {
509
507
  const conversationManager = state.commandContext?.session.conversationManager;
510
508
  if (conversationManager) {
511
- state.sessionPickerModal.loadSelected(conversationManager);
509
+ modal.loadSelected(conversationManager);
512
510
  }
513
- } else if (token.logicalName === 'up') state.sessionPickerModal.moveUp();
514
- else if (token.logicalName === 'down') state.sessionPickerModal.moveDown();
515
- else if (token.logicalName === 'd') state.sessionPickerModal.deleteSelected();
516
- } else if (token.type === 'text' && token.value === 'd') {
517
- state.sessionPickerModal.deleteSelected();
511
+ } else if (token.logicalName === 'up') modal.moveUp();
512
+ else if (token.logicalName === 'down') modal.moveDown();
513
+ else if (isTextBackspace(token.logicalName ?? '')) modal.setQuery(modal.query.slice(0, -1));
514
+ else if (token.logicalName === 'd' && !token.ctrl && modal.query.length === 0) modal.deleteSelected();
515
+ } else if (token.type === 'text') {
516
+ if (token.value === 'd' && modal.query.length === 0) modal.deleteSelected();
517
+ else modal.setQuery(modal.query + token.value);
518
518
  }
519
519
 
520
520
  state.requestRender();
@@ -524,6 +524,9 @@ export function handleSessionPickerToken(state: SessionPickerRouteState, token:
524
524
  type ProfilePickerRouteState = {
525
525
  profilePickerModal: {
526
526
  active: boolean;
527
+ query: string;
528
+ setQuery: (query: string) => void;
529
+ cancelDeleteConfirmation: () => boolean;
527
530
  loadSelected: (configManager: CommandContext['platform']['configManager']) => void;
528
531
  moveUp: () => void;
529
532
  moveDown: () => void;
@@ -535,32 +538,48 @@ type ProfilePickerRouteState = {
535
538
  handleEscape: () => void;
536
539
  };
537
540
 
541
+ /**
542
+ * Profiles: the search row is always live. `d` (arm, then delete) and `s`
543
+ * (save the current settings) fire while the query is empty; otherwise every
544
+ * printable key is typed into the query. Esc drops an armed delete first,
545
+ * otherwise it closes.
546
+ */
538
547
  export function handleProfilePickerToken(state: ProfilePickerRouteState, token: InputToken): boolean {
539
548
  if (!state.profilePickerModal.active) return false;
549
+ const modal = state.profilePickerModal;
540
550
 
541
551
  const saveCurrent = (): void => {
542
552
  if (state.commandContext?.platform.configManager) {
543
553
  const name = `profile-${Date.now()}`;
544
- state.profilePickerModal.saveCurrentAs(name, state.commandContext.platform.configManager);
554
+ modal.saveCurrentAs(name, state.commandContext.platform.configManager);
545
555
  }
546
556
  };
557
+ const act = (key: string): void => {
558
+ if (key === 'd') modal.deleteSelected();
559
+ else if (key === 's') saveCurrent();
560
+ };
547
561
 
548
562
  if (token.type === 'key') {
549
563
  if (token.logicalName === 'escape') {
564
+ if (modal.cancelDeleteConfirmation()) {
565
+ state.requestRender();
566
+ return true;
567
+ }
550
568
  state.handleEscape();
551
569
  return true;
552
570
  }
553
571
  if (token.logicalName === 'enter') {
554
572
  if (state.commandContext?.platform.configManager) {
555
- state.profilePickerModal.loadSelected(state.commandContext.platform.configManager);
573
+ modal.loadSelected(state.commandContext.platform.configManager);
556
574
  }
557
- } else if (token.logicalName === 'up') state.profilePickerModal.moveUp();
558
- else if (token.logicalName === 'down') state.profilePickerModal.moveDown();
559
- else if (token.logicalName === 'd') state.profilePickerModal.deleteSelected();
560
- else if (token.logicalName === 's') saveCurrent();
575
+ } else if (token.logicalName === 'up') modal.moveUp();
576
+ else if (token.logicalName === 'down') modal.moveDown();
577
+ else if (isTextBackspace(token.logicalName ?? '')) {
578
+ if (modal.query.length > 0) modal.setQuery(modal.query.slice(0, -1));
579
+ } else if (token.logicalName) act(token.logicalName);
561
580
  } else if (token.type === 'text') {
562
- if (token.value === 'd') state.profilePickerModal.deleteSelected();
563
- else if (token.value === 's') saveCurrent();
581
+ if (modal.query.length === 0 && (token.value === 'd' || token.value === 's')) act(token.value);
582
+ else modal.setQuery(modal.query + token.value);
564
583
  }
565
584
 
566
585
  state.requestRender();
@@ -576,20 +595,18 @@ type ConfigModalRouteState = {
576
595
 
577
596
  /**
578
597
  * Route a key to the generic config-modal host (MIGRATE-TO-MODAL
579
- * surfaces). The host owns the reserved navigation keys (Esc, up/down/j/k tab
580
- * left/right, and now '/', item 1's type-to-filter, armed the same
581
- * way as scrollable-list-panel.ts's opt-in filter and SettingsModal's own
582
- * '/'-armed search); every other key is offered to the active surface's
583
- * declarative action table (fireAction handles the two-press confirm for
584
- * destructive actions). Any unrecognised key is absorbed so the modal stays
585
- * modal, this is the same "active modal captures all keys" shape as the
586
- * other modal routers.
598
+ * surfaces). The host owns the reserved navigation keys (Esc, up/down, tab,
599
+ * left/right) and the always-live search row: a printable key the surface
600
+ * claims as an action fires the action while the query is empty; every other
601
+ * printable key (and any multi-char paste) is typed into the query, and
602
+ * Backspace edits it. Other named keys (Enter, ctrl chords) go to the
603
+ * surface's declarative action table (fireAction handles the two-press
604
+ * confirm for destructive actions). Any unrecognised key is absorbed so the
605
+ * modal stays modal.
587
606
  *
588
- * While the filter is armed (`configModal.isFilterActive()`), printable text
589
- * tokens go to the query instead of nav/action dispatch, this is WHY 'j'/'k'
590
- * lose their vim-nav meaning mid-filter (you're typing, not navigating) but
591
- * Escape, Backspace, and the arrow/tab nav keys keep their ordinary meaning
592
- * (arrows navigate the FILTERED list; Enter still acts on the selected row).
607
+ * Esc pops exactly one level: an armed destructive confirm is cancelled
608
+ * first; otherwise the modal closes through handleEscape. Esc never clears
609
+ * the query as a separate step.
593
610
  */
594
611
  export function handleConfigModalToken(state: ConfigModalRouteState, token: InputToken): boolean {
595
612
  if (!state.configModal.active) return false;
@@ -600,32 +617,20 @@ export function handleConfigModalToken(state: ConfigModalRouteState, token: Inpu
600
617
  // without a keypress (batch refutation finding 3).
601
618
  state.configModal.noteInteraction();
602
619
 
603
- const filtering = state.configModal.isFilterActive();
604
-
605
620
  if (token.type === 'key') {
606
621
  if (token.logicalName === 'escape') {
607
- // Two-stage Esc, but ONLY while there is a query to clear (the one
608
- // documented exception to single-Esc-close): clearFilterOrFallthrough
609
- // returns false on an empty query, so an armed-but-empty filter (or no
610
- // filter at all) closes the modal in one press, same as every other
611
- // modal here.
612
- if (state.configModal.clearFilterOrFallthrough()) {
622
+ if (state.configModal.cancelPendingConfirm()) {
613
623
  state.requestRender();
614
624
  return true;
615
625
  }
616
626
  state.handleEscape();
617
627
  return true;
618
628
  }
619
- if (filtering && isTextBackspace(token.logicalName ?? '')) {
629
+ if (isTextBackspace(token.logicalName ?? '')) {
620
630
  state.configModal.backspaceFilter();
621
631
  state.requestRender();
622
632
  return true;
623
633
  }
624
- if (!filtering && token.logicalName === '/') {
625
- state.configModal.activateFilter();
626
- state.requestRender();
627
- return true;
628
- }
629
634
  switch (token.logicalName) {
630
635
  case 'up':
631
636
  state.configModal.moveUp();
@@ -645,25 +650,19 @@ export function handleConfigModalToken(state: ConfigModalRouteState, token: Inpu
645
650
  state.requestRender();
646
651
  return true;
647
652
  }
648
- // Any other 'key' token (e.g. 'enter') falls through to the action table
649
- // below even while filtering, only printable TEXT tokens are captured
650
- // by the filter (below), so Enter still fires on the current selection.
653
+ // Any other 'key' token (Enter, a ctrl chord) falls through to the
654
+ // action table below, whatever the query holds.
651
655
  } else if (token.type === 'text') {
652
- if (!filtering && token.value === '/') {
653
- state.configModal.activateFilter();
654
- state.requestRender();
655
- return true;
656
- }
657
- if (filtering) {
658
- // The WHOLE token value lands in the filter in one call, including a
659
- // multi-char paste token, never split into per-char nav/action
660
- // dispatch (the text-capture invariant this item's brief calls out).
656
+ const claimed = state.configModal.getFilterQuery().length === 0
657
+ && [...token.value].length === 1
658
+ && state.configModal.resolveAction(token.value) !== null;
659
+ if (!claimed) {
660
+ // The WHOLE token value lands in the query in one call, including a
661
+ // multi-char paste token, never split into per-char dispatch.
661
662
  state.configModal.appendFilterText(token.value);
662
663
  state.requestRender();
663
664
  return true;
664
665
  }
665
- if (token.value === 'j') { state.configModal.moveDown(); state.requestRender(); return true; }
666
- if (token.value === 'k') { state.configModal.moveUp(); state.requestRender(); return true; }
667
666
  }
668
667
 
669
668
  const actionKey = token.type === 'key' ? (token.logicalName ?? '') : token.type === 'text' ? token.value : '';