@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
@@ -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
+ }