@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
@@ -8,8 +8,7 @@ import type { SelectionResult } from './selection-modal.ts';
8
8
 
9
9
  export type ModalStackState = ActiveModalState & {
10
10
  modalStack: string[];
11
- modalReturnFocus?: 'prompt' | 'panel' | 'indicator';
12
- panelFocused: boolean;
11
+ modalReturnFocus?: 'prompt' | 'indicator';
13
12
  indicatorFocused: boolean;
14
13
  };
15
14
 
@@ -20,7 +19,7 @@ export function modalOpened(state: ModalStackState, name: string): void {
20
19
  if (state.modalStack[state.modalStack.length - 1] === name) return;
21
20
  if (state.modalStack.includes(name)) return;
22
21
  if (state.modalStack.length === 0) {
23
- state.modalReturnFocus = state.indicatorFocused ? 'indicator' : state.panelFocused ? 'panel' : 'prompt';
22
+ state.modalReturnFocus = state.indicatorFocused ? 'indicator' : 'prompt';
24
23
  }
25
24
  state.modalStack.push(name);
26
25
  }
@@ -49,10 +48,34 @@ export type EscapeState = ModalStackState & {
49
48
  selectionModal: ModalStackState['selectionModal'];
50
49
  autocompleteReset: () => void;
51
50
  autocompleteUpdate?: (query: string) => void;
51
+ /**
52
+ * An agent or process view (core/session-focus.ts). With no modal open and
53
+ * the composer empty, Esc there goes back up one level (closing the
54
+ * process view's search first) and never reaches cancelGeneration: leaving
55
+ * a view never stops the agent, the process or main.
56
+ */
57
+ sessionView?: { readonly active: boolean; escape(): void };
58
+ /**
59
+ * The main transcript's scroll. While it is scrolled away from the live
60
+ * bottom (and no modal, composer text or view took the Esc first), Esc
61
+ * returns to the bottom and re-locks there; it never reaches
62
+ * cancelGeneration then. At the bottom the next Esc interrupts as before.
63
+ */
64
+ transcriptScroll?: TranscriptScrollControls;
65
+ /** Kit modals: the top one (or its own sub-level) is the first thing Esc pops. */
66
+ surfaceModals?: { readonly active: boolean; escape(): boolean };
52
67
  clearOnboardingModelPickerCancelState?: () => void;
53
68
  restoreOnboardingModelPickerCancelState?: () => void;
54
69
  };
55
70
 
71
+ /** What handleEscape needs of the main transcript's scroll. */
72
+ export interface TranscriptScrollControls {
73
+ /** The main transcript is scrolled away from its live bottom. */
74
+ scrolledBack(): boolean;
75
+ /** Return to the live bottom and follow it again. */
76
+ toBottom(): void;
77
+ }
78
+
56
79
  export function handleEscape(state: EscapeState): {
57
80
  prompt: string;
58
81
  cursorPos: number;
@@ -62,7 +85,6 @@ export function handleEscape(state: EscapeState): {
62
85
  shortcutsOverlayActive: boolean;
63
86
  shortcutsScrollOffset: number;
64
87
  selectionCallback: ((result: SelectionResult | null) => void) | null;
65
- panelFocused: boolean;
66
88
  indicatorFocused: boolean;
67
89
  modalReturnFocus: NonNullable<ModalStackState['modalReturnFocus']>;
68
90
  } {
@@ -74,7 +96,6 @@ export function handleEscape(state: EscapeState): {
74
96
  let shortcutsOverlayActive = state.shortcutsOverlayActive;
75
97
  let shortcutsScrollOffset = state.shortcutsScrollOffset;
76
98
  let selectionCallback = state.selectionCallback;
77
- let panelFocused = state.panelFocused;
78
99
  let indicatorFocused = state.indicatorFocused;
79
100
  let modalReturnFocus: NonNullable<ModalStackState['modalReturnFocus']> = state.modalReturnFocus ?? 'prompt';
80
101
 
@@ -85,12 +106,29 @@ export function handleEscape(state: EscapeState): {
85
106
  shortcutsOverlayActive,
86
107
  commandMode,
87
108
  }) !== null) return;
88
- panelFocused = modalReturnFocus === 'panel';
89
109
  indicatorFocused = modalReturnFocus === 'indicator';
90
110
  modalReturnFocus = 'prompt';
91
111
  state.modalReturnFocus = 'prompt';
92
112
  };
93
113
 
114
+ // Kit modals are always on top: one Esc pops one of their levels and stops.
115
+ if (state.surfaceModals?.active) {
116
+ state.surfaceModals.escape();
117
+ state.requestRender();
118
+ return {
119
+ prompt,
120
+ cursorPos,
121
+ commandMode,
122
+ helpOverlayActive,
123
+ helpScrollOffset,
124
+ shortcutsOverlayActive,
125
+ shortcutsScrollOffset,
126
+ selectionCallback,
127
+ indicatorFocused,
128
+ modalReturnFocus,
129
+ };
130
+ }
131
+
94
132
  if (state.settingsModal.active && state.settingsModal.editingMode) {
95
133
  state.settingsModal.cancelEdit();
96
134
  state.requestRender();
@@ -103,7 +141,6 @@ export function handleEscape(state: EscapeState): {
103
141
  shortcutsOverlayActive,
104
142
  shortcutsScrollOffset,
105
143
  selectionCallback,
106
- panelFocused,
107
144
  indicatorFocused,
108
145
  modalReturnFocus,
109
146
  };
@@ -200,7 +237,6 @@ export function handleEscape(state: EscapeState): {
200
237
  shortcutsOverlayActive,
201
238
  shortcutsScrollOffset,
202
239
  selectionCallback,
203
- panelFocused,
204
240
  indicatorFocused,
205
241
  modalReturnFocus,
206
242
  };
@@ -225,7 +261,6 @@ export function handleEscape(state: EscapeState): {
225
261
  shortcutsOverlayActive,
226
262
  shortcutsScrollOffset,
227
263
  selectionCallback,
228
- panelFocused,
229
264
  indicatorFocused,
230
265
  modalReturnFocus,
231
266
  };
@@ -244,7 +279,43 @@ export function handleEscape(state: EscapeState): {
244
279
  shortcutsOverlayActive,
245
280
  shortcutsScrollOffset,
246
281
  selectionCallback,
247
- panelFocused,
282
+ indicatorFocused,
283
+ modalReturnFocus,
284
+ };
285
+ }
286
+
287
+ // Inside an agent or process view Esc goes up one level and stops here:
288
+ // interrupting belongs to main alone.
289
+ if (state.sessionView?.active) {
290
+ state.sessionView.escape();
291
+ state.requestRender();
292
+ return {
293
+ prompt,
294
+ cursorPos,
295
+ commandMode,
296
+ helpOverlayActive,
297
+ helpScrollOffset,
298
+ shortcutsOverlayActive,
299
+ shortcutsScrollOffset,
300
+ selectionCallback,
301
+ indicatorFocused,
302
+ modalReturnFocus,
303
+ };
304
+ }
305
+
306
+ // Scrolled back: Esc returns to the live output and never interrupts the turn.
307
+ if (state.transcriptScroll?.scrolledBack()) {
308
+ state.transcriptScroll.toBottom();
309
+ state.requestRender();
310
+ return {
311
+ prompt,
312
+ cursorPos,
313
+ commandMode,
314
+ helpOverlayActive,
315
+ helpScrollOffset,
316
+ shortcutsOverlayActive,
317
+ shortcutsScrollOffset,
318
+ selectionCallback,
248
319
  indicatorFocused,
249
320
  modalReturnFocus,
250
321
  };
@@ -260,7 +331,6 @@ export function handleEscape(state: EscapeState): {
260
331
  shortcutsOverlayActive,
261
332
  shortcutsScrollOffset,
262
333
  selectionCallback,
263
- panelFocused,
264
334
  indicatorFocused,
265
335
  modalReturnFocus,
266
336
  };
@@ -12,6 +12,7 @@ import type { ModelPickerModal } from './model-picker.ts';
12
12
  import { handleMcpWorkspaceToken, type McpWorkspace } from './mcp-workspace.ts';
13
13
  import type { CommandContext } from './command-registry.ts';
14
14
  import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
15
+ import type { OverlayFilters } from './overlay-filter.ts';
15
16
  import type { FilePickerModal } from './file-picker.ts';
16
17
  import type { BlockActionsMenu, BlockActionId } from '../renderer/block-actions.ts';
17
18
  import type { SearchManager } from './search.ts';
@@ -64,6 +65,7 @@ export type ModalTokenRouteState = {
64
65
  requestRender: () => void;
65
66
  handleEscape: () => void;
66
67
  contextInspectorModal: ContextInspectorModal;
68
+ overlayFilters?: OverlayFilters;
67
69
  modalOpened: (name: string) => void;
68
70
  filePicker: FilePickerModal;
69
71
  imageRegistry: Map<string, { data: string; mediaType: string }>;
@@ -191,6 +193,7 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
191
193
  helpScrollOffset: state.helpScrollOffset,
192
194
  shortcutsOverlayActive: state.shortcutsOverlayActive,
193
195
  shortcutsScrollOffset: state.shortcutsScrollOffset,
196
+ overlayFilters: state.overlayFilters,
194
197
  requestRender: state.requestRender,
195
198
  handleEscape: state.handleEscape,
196
199
  };
@@ -241,6 +244,7 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
241
244
  active: state.contextInspectorModal.active,
242
245
  requestRender: state.requestRender,
243
246
  handleEscape: state.handleEscape,
247
+ scroll: (delta) => state.contextInspectorModal.scrollBy(delta),
244
248
  }, token)) {
245
249
  return withState(state, true);
246
250
  }
@@ -348,7 +348,6 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
348
348
  }
349
349
  if (handler.modalStack.length === 0) {
350
350
  const returnFocus = handler.modalReturnFocus;
351
- handler.panelFocused = returnFocus === 'panel';
352
351
  handler.indicatorFocused = returnFocus === 'indicator';
353
352
  handler.modalReturnFocus = 'prompt';
354
353
  }
@@ -4,6 +4,7 @@ import type { CommandContext } from './command-registry.ts';
4
4
  import type { CapabilityFilter, CategoryFilter, ModelPickerModal } from './model-picker.ts';
5
5
  import { MODEL_PICKER_CHROME_LINES } from '../renderer/model-picker-overlay.ts';
6
6
  import { ensureProviderKeyThenSelect } from './provider-key-intake.ts';
7
+ import { isTextBackspace } from './delete-key-policy.ts';
7
8
  import { resolveAndValidatePath } from '@pellux/goodvibes-sdk/platform/utils';
8
9
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
9
10
  import type { BlockActionId } from '../renderer/block-actions.ts';
@@ -49,17 +50,10 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
49
50
 
50
51
  if (token.type === 'key') {
51
52
  if (token.logicalName === 'escape') {
52
- // Search now starts focused by default (see
53
- // ModelPickerModal.openAllModels() doc comment). Escape with an empty
54
- // query used to only blurSearch() here, leaving the picker open and
55
- // requiring a SECOND Escape to actually close it, confusing on a
56
- // freshly-opened, untouched picker where there is nothing to "clear".
57
- // Clearing a non-empty query is still a distinct first Escape (whether
58
- // or not search is focused); an empty query falls through to the same
59
- // effort/contextCap/previousMode/close cascade as before.
60
- if (state.modelPicker.mode !== 'contextCap' && state.modelPicker.query.length > 0) {
61
- state.modelPicker.clearQuery();
62
- } else if (state.modelPicker.mode === 'effort') {
53
+ // Esc pops exactly one level: the effort and context-cap steps and a
54
+ // provider-first model list are sub-levels of the picker; otherwise the
55
+ // picker closes. A typed query is not a level, it goes with the picker.
56
+ if (state.modelPicker.mode === 'effort') {
63
57
  state.modelPicker.mode = 'model';
64
58
  state.modelPicker.selectedIndex = 0;
65
59
  } else if (state.modelPicker.mode === 'contextCap') {
@@ -73,11 +67,11 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
73
67
  state.handleEscape();
74
68
  return true;
75
69
  }
76
- } else if (token.logicalName === 'backspace') {
70
+ } else if (isTextBackspace(token.logicalName ?? '')) {
77
71
  if (state.modelPicker.mode === 'contextCap') state.modelPicker.deleteContextCapChar();
78
- else if (state.modelPicker.searchFocused && (state.modelPicker.mode === 'model' || state.modelPicker.mode === 'provider')) state.modelPicker.deleteChar();
72
+ else if (state.modelPicker.canFocusSearch()) state.modelPicker.deleteChar();
79
73
  } else if (token.logicalName === 'enter') {
80
- if (state.modelPicker.focusPane === 'targets') {
74
+ if (state.modelPicker.focusRegion === 'targets') {
81
75
  state.modelPicker.focusItems();
82
76
  state.requestRender();
83
77
  return true;
@@ -174,84 +168,45 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
174
168
  if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
175
169
  }
176
170
  }
177
- } else if (token.logicalName === 'up') {
178
- if (state.modelPicker.focusPane === 'targets') {
179
- state.modelPicker.moveTarget(-1);
180
- state.requestRender();
181
- return true;
182
- }
183
- if (state.modelPicker.canFocusSearch() && !state.modelPicker.searchFocused && state.modelPicker.selectedIndex === 0) {
184
- state.modelPicker.focusSearch();
185
- } else if (state.modelPicker.searchFocused) {
186
- // Symmetric with 'down' (below): search now starts focused by default
187
- // so 'up' must have a way out of it too, or it would be a
188
- // silent no-op the very first time a user reaches for the list.
189
- state.modelPicker.blurSearch();
190
- } else {
191
- const maxVis = Math.max(5, state.getViewportHeight() - MODEL_PICKER_CHROME_LINES - 4);
192
- state.modelPicker.moveUp(maxVis);
193
- }
194
- } else if (token.logicalName === 'down') {
195
- if (state.modelPicker.focusPane === 'targets') {
196
- state.modelPicker.moveTarget(1);
197
- state.requestRender();
198
- return true;
199
- }
200
- if (state.modelPicker.searchFocused) {
201
- state.modelPicker.blurSearch();
202
- } else {
203
- const maxVis = Math.max(5, state.getViewportHeight() - MODEL_PICKER_CHROME_LINES - 4);
204
- state.modelPicker.moveDown(maxVis);
205
- }
206
- } else if (token.logicalName === 'left' && !state.modelPicker.searchFocused && state.modelPicker.mode !== 'contextCap') {
207
- state.modelPicker.focusTargets();
208
- } else if (token.logicalName === 'right' && !state.modelPicker.searchFocused && state.modelPicker.mode !== 'contextCap') {
171
+ } else if (token.logicalName === 'up' || token.logicalName === 'down') {
172
+ // The search row is always live, so arrows always walk the list.
173
+ const maxVis = Math.max(5, state.getViewportHeight() - MODEL_PICKER_CHROME_LINES - 4);
209
174
  state.modelPicker.focusItems();
210
- } else if (token.logicalName === 'tab' && state.modelPicker.mode === 'model') {
211
- if (state.modelPicker.focusPane === 'targets') {
212
- state.modelPicker.focusItems();
213
- } else {
175
+ if (token.logicalName === 'up') state.modelPicker.moveUp(maxVis);
176
+ else state.modelPicker.moveDown(maxVis);
177
+ } else if ((token.logicalName === 'tab' || token.logicalName === 'right' || token.logicalName === 'left') && state.modelPicker.mode !== 'contextCap' && state.modelPicker.mode !== 'effort') {
178
+ // The five model targets are tabs: tab / → next, shift+tab / ← previous.
179
+ const back = token.logicalName === 'left' || (token.logicalName === 'tab' && token.shift);
180
+ state.modelPicker.moveTarget(back ? -1 : 1);
181
+ } else if (token.ctrl && state.modelPicker.mode === 'model') {
182
+ const picker = state.modelPicker;
183
+ if (token.logicalName === 't') {
214
184
  const cycle: CategoryFilter[] = ['all', 'free', 'paid', 'subscription'];
215
- const cur = cycle.indexOf(state.modelPicker.categoryFilter);
216
- state.modelPicker.setCategoryFilter(cycle[(cur + 1) % cycle.length]!);
185
+ picker.setCategoryFilter(cycle[(cycle.indexOf(picker.categoryFilter) + 1) % cycle.length]!);
186
+ } else if (token.logicalName === 'k') cycleCapabilityFilter(picker);
187
+ else if (token.logicalName === 'a') picker.toggleAvailableOnly();
188
+ else if (token.logicalName === 'b') picker.cycleBenchmarkSort();
189
+ else if (token.logicalName === 'g') picker.cycleGroupBy();
190
+ else if (token.logicalName === 'f') togglePin(state);
191
+ else if (token.logicalName === 'r') {
192
+ void state.commandContext?.executeCommand?.('refresh-models', []);
217
193
  }
218
- } else if (!state.modelPicker.searchFocused && token.logicalName === 'g' && state.modelPicker.mode === 'model') {
219
- state.modelPicker.cycleGroupBy();
220
- } else if (!state.modelPicker.searchFocused && token.logicalName === 'c' && state.modelPicker.mode === 'model') {
221
- cycleCapabilityFilter(state.modelPicker);
222
- } else if (!state.modelPicker.searchFocused && token.logicalName === 'a' && state.modelPicker.mode === 'model') {
223
- state.modelPicker.toggleAvailableOnly();
224
- } else if (!state.modelPicker.searchFocused && token.logicalName === 'b' && state.modelPicker.mode === 'model') {
225
- state.modelPicker.cycleBenchmarkSort();
226
- } else if (!state.modelPicker.searchFocused && token.logicalName === '/' && state.modelPicker.canFocusSearch()) {
227
- state.modelPicker.focusItems();
228
- state.modelPicker.focusSearch();
229
194
  }
230
195
  } else if (token.type === 'text') {
231
- if (state.modelPicker.mode === 'contextCap') {
232
- if (token.value.length === 1) state.modelPicker.appendContextCapChar(token.value);
233
- } else if ((state.modelPicker.mode === 'model' || state.modelPicker.mode === 'provider') && state.modelPicker.searchFocused) {
234
- // When search is focused every printable char, including space, goes to the query.
235
- // The space=context-cap shortcut remains active only in the non-search branch below.
236
- const ch = token.value;
237
- if (ch.length === 1 && ch >= ' ') state.modelPicker.appendChar(ch);
238
- } else if (token.value === ' ' && state.modelPicker.mode === 'model') {
239
- const selected = state.modelPicker.getSelected();
240
- if (selected && state.modelPicker.isLocalModel(selected)) state.modelPicker.enterContextCapMode(selected);
196
+ const picker = state.modelPicker;
197
+ if (picker.mode === 'contextCap') {
198
+ if (token.value.length === 1) picker.appendContextCapChar(token.value);
199
+ } else if (token.value === ' ' && picker.mode === 'model' && picker.query.length === 0) {
200
+ // Space on an untouched search sets a context cap for a local model.
201
+ const selected = picker.getSelected();
202
+ if (selected && picker.isLocalModel(selected)) picker.enterContextCapMode(selected);
241
203
  } else if (token.value === '\t') {
242
- if (state.modelPicker.focusPane === 'targets') state.modelPicker.focusItems();
243
- else state.modelPicker.focusTargets();
244
- } else if (token.value === 'g' && state.modelPicker.mode === 'model') {
245
- state.modelPicker.cycleGroupBy();
246
- } else if (token.value === 'c' && state.modelPicker.mode === 'model') {
247
- cycleCapabilityFilter(state.modelPicker);
248
- } else if (token.value === 'a' && state.modelPicker.mode === 'model') {
249
- state.modelPicker.toggleAvailableOnly();
250
- } else if (token.value === 'b' && state.modelPicker.mode === 'model') {
251
- state.modelPicker.cycleBenchmarkSort();
252
- } else if (token.value === '/' && state.modelPicker.canFocusSearch()) {
253
- state.modelPicker.focusItems();
254
- state.modelPicker.focusSearch();
204
+ picker.moveTarget(1);
205
+ } else if (picker.canFocusSearch() && !(token.value === '/' && picker.query.length === 0)) {
206
+ // Every printable character goes to the always-live search.
207
+ picker.focusItems();
208
+ picker.focusSearch();
209
+ for (const ch of token.value) if (ch >= ' ') picker.appendChar(ch);
255
210
  }
256
211
  }
257
212
 
@@ -259,6 +214,14 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
259
214
  return true;
260
215
  }
261
216
 
217
+ /** Pin or unpin the selected model: flipped locally at once, persisted through /pin or /unpin. */
218
+ function togglePin(state: ModelPickerRouteState): void {
219
+ const model = state.modelPicker.getSelected();
220
+ if (!model) return;
221
+ const pinned = state.modelPicker.togglePinnedLocal(model);
222
+ void state.commandContext?.executeCommand?.(pinned ? 'pin' : 'unpin', [model.registryKey]);
223
+ }
224
+
262
225
  function cycleCapabilityFilter(modelPicker: ModelPickerModal): void {
263
226
  const cycle: CapabilityFilter[] = ['none', 'reasoning', 'toolUse', 'multimodal'];
264
227
  const cur = cycle.indexOf(modelPicker.capabilityFilter);
@@ -274,6 +237,8 @@ type EscapeOnlyModalRouteState = {
274
237
  active: boolean;
275
238
  requestRender: () => void;
276
239
  handleEscape: () => void;
240
+ /** Optional scrolling for read-only modals: ↑ passes +1 (back), ↓ passes -1. */
241
+ scroll?: (delta: number) => void;
277
242
  };
278
243
 
279
244
  export function handleEscapeOnlyModalToken(state: EscapeOnlyModalRouteState, token: InputToken): boolean {
@@ -282,6 +247,12 @@ export function handleEscapeOnlyModalToken(state: EscapeOnlyModalRouteState, tok
282
247
  state.handleEscape();
283
248
  return true;
284
249
  }
250
+ if (token.type === 'key' && state.scroll) {
251
+ if (token.logicalName === 'up') state.scroll(1);
252
+ else if (token.logicalName === 'down') state.scroll(-1);
253
+ else if (token.logicalName === 'pageup') state.scroll(10);
254
+ else if (token.logicalName === 'pagedown') state.scroll(-10);
255
+ }
285
256
  state.requestRender();
286
257
  return true;
287
258
  }
@@ -319,21 +290,16 @@ type FilePickerRouteState = {
319
290
  export function handleFilePickerToken(state: FilePickerRouteState, token: InputToken): boolean {
320
291
  if (!state.filePicker.active) return false;
321
292
 
293
+ // The query is always live: typing filters, arrows move, Esc closes.
322
294
  if (token.type === 'text') {
323
- if (!state.filePicker.searchFocused && token.value === '/') {
324
- state.filePicker.focusSearch();
325
- } else if (state.filePicker.searchFocused && token.value === ' ' && state.filePicker.query === '') {
295
+ if (token.value === ' ' && state.filePicker.query === '') {
326
296
  state.filePicker.close();
327
- } else if (state.filePicker.searchFocused) {
297
+ } else {
298
+ state.filePicker.focusSearch();
328
299
  state.filePicker.setQuery(state.filePicker.query + token.value);
329
300
  }
330
301
  } else if (token.type === 'key') {
331
302
  if (token.logicalName === 'escape') {
332
- if (state.filePicker.searchFocused && state.filePicker.query.length > 0) {
333
- state.filePicker.setQuery('');
334
- state.requestRender();
335
- return true;
336
- }
337
303
  state.handleEscape();
338
304
  return true;
339
305
  } else if (token.logicalName === 'enter') {
@@ -379,21 +345,13 @@ export function handleFilePickerToken(state: FilePickerRouteState, token: InputT
379
345
  }
380
346
  state.filePicker.close();
381
347
  } else if (token.logicalName === 'up') {
382
- if (!state.filePicker.searchFocused && state.filePicker.selectedIndex === 0) {
383
- state.filePicker.focusSearch();
384
- } else if (!state.filePicker.searchFocused) {
385
- state.filePicker.moveUp();
386
- }
348
+ state.filePicker.moveUp();
387
349
  } else if (token.logicalName === 'down') {
388
- if (state.filePicker.searchFocused) {
389
- state.filePicker.blurSearch();
390
- } else {
391
- state.filePicker.moveDown();
392
- }
350
+ state.filePicker.moveDown();
393
351
  } else if (token.logicalName === 'backspace') {
394
- if (state.filePicker.searchFocused && state.filePicker.query.length > 0) {
352
+ if (state.filePicker.query.length > 0) {
395
353
  state.filePicker.setQuery(state.filePicker.query.slice(0, -1));
396
- } else if (state.filePicker.searchFocused) {
354
+ } else {
397
355
  const removeCount = state.filePicker.injectMode ? 2 : 1;
398
356
  if (state.cursorPos >= removeCount) {
399
357
  state.prompt = state.prompt.slice(0, state.cursorPos - removeCount) + state.prompt.slice(state.cursorPos);
@@ -0,0 +1,149 @@
1
+ /**
2
+ * handler-session-view-route.ts, the keyboard while an agent or process is
3
+ * open full screen, and the session chips' Tab cycling everywhere.
4
+ *
5
+ * Runs after the modals (a modal open takes every key first) and before the
6
+ * work tree and the composer.
7
+ *
8
+ * Tab / Shift+Tab next / previous session chip, only while the composer
9
+ * is empty and not completing a command (otherwise Tab
10
+ * keeps its completion meaning and Shift+Tab its mode
11
+ * cycling), and only when the chips row shows
12
+ * ctrl+x stop the agent or process: the first press asks, the
13
+ * second (within 3 s) stops
14
+ * PageUp/PageDown scroll the view
15
+ *
16
+ * Agent view: Enter sends the composer's text to the agent as a steer (main
17
+ * keeps running); everything else types into the composer as usual.
18
+ *
19
+ * Process view: the composer takes no text (the process's stdin is not
20
+ * reachable), so letters are the view's keys: / search, n next match, y copy
21
+ * the output, r restart (when supported); ↑ ↓ scroll a row, End follows the
22
+ * output again.
23
+ *
24
+ * Esc is not handled here: it goes through the shared chain
25
+ * (handler-modal-stack.ts), whose view step goes back up one level and never
26
+ * cancels anything.
27
+ */
28
+
29
+ import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
30
+ import { isTextBackspace } from './delete-key-policy.ts';
31
+
32
+ /** What the route can do to the session views (shell/session-views.ts). */
33
+ export interface SessionViewControls {
34
+ /** 'main' when no view is open. */
35
+ readonly kind: 'main' | 'agent' | 'process';
36
+ readonly active: boolean;
37
+ /** The chips row is showing (more than one session to switch to). */
38
+ chipsVisible(): boolean;
39
+ cycle(delta: 1 | -1): void;
40
+ /** ctrl+x: ask on the first press, stop on the second. */
41
+ pressStop(): void;
42
+ scroll(rows: number): void;
43
+ /** Rows a page scroll moves. */
44
+ pageRows(): number;
45
+ /** Follow the newest output again (End). */
46
+ follow(): void;
47
+ /** Agent view: send text to the agent as a steer. True when it was accepted. */
48
+ steer(text: string): boolean;
49
+ /** Process view keys. */
50
+ readonly searchEditing: boolean;
51
+ startSearch(): void;
52
+ searchType(text: string): void;
53
+ searchBackspace(): void;
54
+ searchNext(): void;
55
+ copyOutput(): void;
56
+ restart(): void;
57
+ /** A key the process view does not take (the composer is off there). */
58
+ refuseText(): void;
59
+ /** Esc in a view: close the search, else go back up one level. */
60
+ escape(): void;
61
+ }
62
+
63
+ export interface SessionViewRouteState {
64
+ readonly controls: SessionViewControls | null;
65
+ prompt: string;
66
+ cursorPos: number;
67
+ readonly commandMode: boolean;
68
+ readonly saveUndoState: () => void;
69
+ readonly requestRender: () => void;
70
+ }
71
+
72
+ function isBackTab(token: InputToken): boolean {
73
+ return token.type === 'key' && (token.logicalName === '\x1b[Z' || (token.logicalName === 'tab' && token.shift === true));
74
+ }
75
+
76
+ /** Returns true when the token was handled here. */
77
+ export function handleSessionViewToken(state: SessionViewRouteState, token: InputToken): boolean {
78
+ const c = state.controls;
79
+ if (!c) return false;
80
+ const composerIdle = state.prompt.length === 0 && !state.commandMode;
81
+
82
+ if (token.type === 'key' && composerIdle && !token.ctrl && !token.meta && (token.logicalName === 'tab' || isBackTab(token)) && c.chipsVisible()) {
83
+ c.cycle(isBackTab(token) ? -1 : 1);
84
+ state.requestRender();
85
+ return true;
86
+ }
87
+ if (!c.active) return false;
88
+
89
+ if (token.type === 'key') {
90
+ const name = token.logicalName ?? '';
91
+ if (token.ctrl && !token.meta && name === 'x') {
92
+ c.pressStop();
93
+ state.requestRender();
94
+ return true;
95
+ }
96
+ if (!token.ctrl && !token.meta && (name === 'pageup' || name === 'pagedown')) {
97
+ c.scroll(name === 'pageup' ? c.pageRows() : -c.pageRows());
98
+ state.requestRender();
99
+ return true;
100
+ }
101
+ if (c.kind === 'agent' && name === 'enter' && !token.shift && !token.ctrl && !token.meta && !state.commandMode) {
102
+ const text = state.prompt.trim();
103
+ if (!text) return true; // an empty Enter in an agent view sends nothing (and never opens main's block actions)
104
+ if (c.steer(text)) {
105
+ state.saveUndoState();
106
+ state.prompt = '';
107
+ state.cursorPos = 0;
108
+ }
109
+ state.requestRender();
110
+ return true;
111
+ }
112
+ if (c.kind === 'process') return handleProcessKey(state, c, token, name);
113
+ return false;
114
+ }
115
+
116
+ if (token.type === 'text' && c.kind === 'process') {
117
+ if (c.searchEditing) {
118
+ c.searchType(token.value.replace(/[\r\n]+/g, ' ').replace(/[\x00-\x1f\x7f]/g, ''));
119
+ } else if (token.value === '/') c.startSearch();
120
+ else if (token.value === 'n') c.searchNext();
121
+ else if (token.value === 'y') c.copyOutput();
122
+ else if (token.value === 'r') c.restart();
123
+ else if (token.value === 'G') c.follow();
124
+ else c.refuseText();
125
+ state.requestRender();
126
+ return true;
127
+ }
128
+ return false;
129
+ }
130
+
131
+ function handleProcessKey(state: SessionViewRouteState, c: SessionViewControls, token: InputToken & { type: 'key' }, name: string): boolean {
132
+ if (token.ctrl || token.meta) return false;
133
+ if (c.searchEditing) {
134
+ if (name === 'enter') c.searchNext();
135
+ else if (isTextBackspace(name)) c.searchBackspace();
136
+ else return false;
137
+ state.requestRender();
138
+ return true;
139
+ }
140
+ switch (name) {
141
+ case 'up': c.scroll(1); break;
142
+ case 'down': c.scroll(-1); break;
143
+ case 'end': c.follow(); break;
144
+ case 'enter': break; // no stdin to send a line to
145
+ default: return false;
146
+ }
147
+ state.requestRender();
148
+ return true;
149
+ }