@pellux/goodvibes-tui 1.1.0 → 1.7.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 (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -9,15 +9,13 @@ import type { SelectionResult } from './selection-modal.ts';
9
9
  import { SearchManager } from './search.ts';
10
10
  import type { InputHistory, HistorySearch } from './input-history.ts';
11
11
  import type { BlockMeta, ConversationManager } from '../core/conversation';
12
- import { ProcessModal } from '../renderer/process-modal.ts';
13
- import { LiveTailModal } from '../renderer/live-tail-modal.ts';
14
12
  import { BlockActionsMenu } from '../renderer/block-actions.ts';
15
- import { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
16
13
  import { ContextInspectorModal } from '../renderer/context-inspector.ts';
17
14
  import { BookmarkModal } from './bookmark-modal.ts';
18
15
  import { SettingsModal } from './settings-modal.ts';
19
16
  import type { McpWorkspace } from './mcp-workspace.ts';
20
17
  import { SessionPickerModal } from './session-picker-modal.ts';
18
+ import type { ConfigModal } from './config-modal.ts';
21
19
  import { ProfilePickerModal } from './profile-picker-modal.ts';
22
20
  import type { OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
23
21
  import type { OnboardingWizardAction } from './onboarding/onboarding-wizard.ts';
@@ -45,6 +43,7 @@ import type { PanelManager } from '../panels/panel-manager.ts';
45
43
  import type { KeybindingsManager } from './keybindings.ts';
46
44
  import type { ModelPickerTarget } from './model-picker.ts';
47
45
  import type { KillRing } from './kill-ring.ts';
46
+ import type { FocusTracker } from '../core/focus-tracker.ts';
48
47
 
49
48
  /**
50
49
  * InputFeedContext — The single long-lived context object passed to feedInputTokens
@@ -72,8 +71,8 @@ import type { KillRing } from './kill-ring.ts';
72
71
  * - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
73
72
  * - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
74
73
  * `profilePickerModal` — modal objects constructed once in InputHandler constructor
75
- * - `filePicker`, `modelPicker`, `onboardingWizard`, `processModal`, `liveTailModal`,
76
- * `agentDetailModal`, `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
74
+ * - `filePicker`, `modelPicker`, `onboardingWizard`,
75
+ * `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
77
76
  * service objects constructed once
78
77
  * - `panelManager`, `keybindingsManager` — from uiServices, stable for app lifetime
79
78
  * - `modalStack` — reference to the handler's shared array (mutated in place)
@@ -111,6 +110,7 @@ export interface InputFeedContext {
111
110
  readonly mcpWorkspace: McpWorkspace;
112
111
  readonly sessionPickerModal: SessionPickerModal;
113
112
  readonly profilePickerModal: ProfilePickerModal;
113
+ readonly configModal: ConfigModal;
114
114
  readonly historySearch: HistorySearch;
115
115
  commandRegistry: CommandRegistry | null;
116
116
  commandContext: CommandContext | undefined;
@@ -118,9 +118,6 @@ export interface InputFeedContext {
118
118
  readonly filePicker: FilePickerModal;
119
119
  readonly modelPicker: ModelPickerModal;
120
120
  readonly onboardingWizard: OnboardingWizardController;
121
- readonly processModal: ProcessModal;
122
- readonly liveTailModal: LiveTailModal;
123
- readonly agentDetailModal: AgentDetailModal;
124
121
  readonly contextInspectorModal: ContextInspectorModal;
125
122
  readonly blockActionsMenu: BlockActionsMenu;
126
123
  readonly searchManager: SearchManager;
@@ -131,6 +128,8 @@ export interface InputFeedContext {
131
128
  inputHistory: InputHistory | null;
132
129
  conversationManager: ConversationManager | null;
133
130
  readonly killRing: KillRing;
131
+ /** Terminal focus tracker (W2.3) — updated here from 'focus' tokens, read by the unfocused-alert notifiers in core/. */
132
+ readonly focusTracker: FocusTracker;
134
133
  readonly getHistory: () => InfiniteBuffer;
135
134
  readonly getViewportHeight: () => number;
136
135
  readonly getScrollTop: () => number;
@@ -180,17 +179,34 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
180
179
  const lineCount = history.getLineCount();
181
180
  const keybindings = context.keybindingsManager;
182
181
 
183
- // A single stdin chunk can tokenize into several 'text' tokens (fast-typed
184
- // burst) or one 'text' token with a multi-char value (bracketed paste).
185
- // Computed once per feed() call, not per token, so per-token cost stays
186
- // the same O(1) check it always was.
187
- let totalPrintableChars = 0;
188
- for (const t of tokens) {
189
- if (t.type === 'text') totalPrintableChars += t.value.length;
190
- }
191
- const isPrintableBurst = totalPrintableChars > 1;
182
+ // Shared opener for the Fleet panel: makes it visible AND transfers keyboard
183
+ // focus to it. panelManager.open() only makes the panel active focus is a
184
+ // separate axis (see PanelManager.focusPanels()/getFocusTarget()); without the
185
+ // focusPanels() call, j/k/i/K land silently in the composer until Tab. Used by
186
+ // the footer indicator's [Enter] and by F2 (W6.2 e: F2 and the footer
187
+ // indicator both subsume the retired process modal). Mirrors the Ctrl+P
188
+ // panel-picker launcher (ui-openers.ts openPanelPicker).
189
+ const openFleetPanel = (): void => {
190
+ context.panelManager.open('fleet');
191
+ context.panelManager.focusPanels();
192
+ context.panelFocused = true;
193
+ };
192
194
 
195
+ // Paste-ness is a per-TOKEN property, computed at the handlePanelFocusToken
196
+ // call below — never a per-feed character sum. The SDK tokenizer emits a
197
+ // bracketed paste (\x1b[?2004h, enabled in main.ts terminal init) as ONE
198
+ // 'text' token holding the whole payload, while discrete keystrokes — even
199
+ // several batched into one feed() by render-tick latency — arrive as
200
+ // separate 1-char 'text' tokens. The old per-feed sum misread two quick nav
201
+ // keystrokes (e.g. j then k in one drain) as a "burst" and yanked focus.
193
202
  for (const token of tokens) {
203
+ // Focus-reporting tokens (CSI ?1004h, W2.3) never reach the composer or any
204
+ // modal route — consumed here, first, unconditionally. No render needed.
205
+ if (token.type === 'focus') {
206
+ context.focusTracker.setFocused(token.action === 'in');
207
+ continue;
208
+ }
209
+
194
210
  if (token.type === 'key' && context.keybindingsManager.matches('clear-cancel', token)) {
195
211
  context.handleCtrlC();
196
212
  continue;
@@ -210,6 +226,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
210
226
  mcpWorkspace: context.mcpWorkspace,
211
227
  sessionPickerModal: context.sessionPickerModal,
212
228
  profilePickerModal: context.profilePickerModal,
229
+ configModal: context.configModal,
213
230
  onboardingWizard: context.onboardingWizard,
214
231
  helpOverlayActive: context.helpOverlayActive,
215
232
  helpScrollOffset: context.helpScrollOffset,
@@ -224,9 +241,6 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
224
241
  getViewportHeight: context.getViewportHeight,
225
242
  requestRender: context.requestRender,
226
243
  handleEscape: context.handleEscape,
227
- liveTailModal: context.liveTailModal,
228
- processModal: context.processModal,
229
- agentDetailModal: context.agentDetailModal,
230
244
  contextInspectorModal: context.contextInspectorModal,
231
245
  modalOpened: context.modalOpened,
232
246
  filePicker: context.filePicker,
@@ -330,7 +344,17 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
330
344
  panelManager: context.panelManager,
331
345
  keybindingsManager: context.keybindingsManager,
332
346
  onPanelInputConsumed: context.onPanelInputConsumed,
333
- isPrintableBurst,
347
+ // Per-token paste classification (Invariant B): a paste is a single
348
+ // 'text' token whose value holds more than one character.
349
+ isPasteToken: token.type === 'text' && token.value.length > 1,
350
+ // One-shot honesty hint when a paste is dropped into a non-capturing
351
+ // focused panel (Invariant A: focus never silently flips to the composer).
352
+ onPasteDropped: (panelName: string) =>
353
+ context.commandContext?.print(
354
+ `paste ignored — focus is on ${panelName}; Tab returns to composer`,
355
+ ),
356
+ isTurnActive: () => context.commandContext?.isGenerating?.() ?? false,
357
+ cancelGeneration: () => context.commandContext?.cancelGeneration?.(),
334
358
  }, token);
335
359
  context.panelFocused = panelRoute.panelFocused;
336
360
  if (panelRoute.handled) {
@@ -340,7 +364,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
340
364
  const indicatorRoute = handleIndicatorFocusToken({
341
365
  indicatorFocused: context.indicatorFocused,
342
366
  modalOpened: context.modalOpened,
343
- processModal: context.processModal,
367
+ openFleetPanel,
344
368
  requestRender: context.requestRender,
345
369
  }, token);
346
370
  context.indicatorFocused = indicatorRoute.indicatorFocused;
@@ -422,7 +446,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
422
446
  commandRegistry: context.commandRegistry,
423
447
  autocomplete: context.autocomplete,
424
448
  blockActionsMenu: { open: (block: BlockMeta) => context.blockActionsMenu.open(block) },
425
- processModal: context.processModal,
449
+ openFleetPanel,
426
450
  modalOpened: context.modalOpened,
427
451
  saveUndoState: context.saveUndoState,
428
452
  breakUndoCoalesce: context.breakUndoCoalesce,
@@ -225,14 +225,12 @@ export function handleEscapeForHandler(handler: InputHandler): void {
225
225
  helpOverlayActive: handler.helpOverlayActive,
226
226
  shortcutsOverlayActive: handler.shortcutsOverlayActive,
227
227
  bookmarkModal: handler.bookmarkModal,
228
- agentDetailModal: handler.agentDetailModal,
229
- liveTailModal: handler.liveTailModal,
230
228
  settingsModal: handler.settingsModal,
231
229
  mcpWorkspace: handler.mcpWorkspace,
232
230
  sessionPickerModal: handler.sessionPickerModal,
233
231
  profilePickerModal: handler.profilePickerModal,
232
+ configModal: handler.configModal,
234
233
  contextInspectorModal: handler.contextInspectorModal,
235
- processModal: handler.processModal,
236
234
  modelPicker: handler.modelPicker,
237
235
  filePicker: handler.filePicker,
238
236
  blockActionsMenu: handler.blockActionsMenu,
@@ -1,6 +1,7 @@
1
1
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
2
2
  import type { SelectionResult, SelectionAction } from './selection-modal.ts';
3
3
  import type { CommandContext } from './command-registry.ts';
4
+ import type { ConfigModal } from './config-modal.ts';
4
5
  import { openTtsProviderPicker, openTtsVoicePicker } from './tts-settings-actions.ts';
5
6
  import { isTextBackspace } from './delete-key-policy.ts';
6
7
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
@@ -533,3 +534,67 @@ export function handleProfilePickerToken(state: ProfilePickerRouteState, token:
533
534
  state.requestRender();
534
535
  return true;
535
536
  }
537
+
538
+ type ConfigModalRouteState = {
539
+ configModal: ConfigModal;
540
+ commandContext?: CommandContext;
541
+ requestRender: () => void;
542
+ handleEscape: () => void;
543
+ };
544
+
545
+ /**
546
+ * Route a key to the generic config-modal host (W6.1 MIGRATE-TO-MODAL
547
+ * surfaces). The host owns the reserved navigation keys (Esc, up/down/j/k tab
548
+ * left/right); every other key is offered to the active surface's declarative
549
+ * action table (fireAction handles the two-press confirm for destructive
550
+ * actions). Any unrecognised key is absorbed so the modal stays modal — this is
551
+ * the same "active modal captures all keys" shape as the other modal routers.
552
+ */
553
+ export function handleConfigModalToken(state: ConfigModalRouteState, token: InputToken): boolean {
554
+ if (!state.configModal.active) return false;
555
+
556
+ if (token.type === 'key') {
557
+ switch (token.logicalName) {
558
+ case 'escape':
559
+ state.handleEscape();
560
+ return true;
561
+ case 'up':
562
+ state.configModal.moveUp();
563
+ state.requestRender();
564
+ return true;
565
+ case 'down':
566
+ state.configModal.moveDown();
567
+ state.requestRender();
568
+ return true;
569
+ case 'left':
570
+ state.configModal.prevTab();
571
+ state.requestRender();
572
+ return true;
573
+ case 'right':
574
+ case 'tab':
575
+ state.configModal.nextTab();
576
+ state.requestRender();
577
+ return true;
578
+ }
579
+ } else if (token.type === 'text') {
580
+ if (token.value === 'j') { state.configModal.moveDown(); state.requestRender(); return true; }
581
+ if (token.value === 'k') { state.configModal.moveUp(); state.requestRender(); return true; }
582
+ }
583
+
584
+ const actionKey = token.type === 'key' ? (token.logicalName ?? '') : token.type === 'text' ? token.value : '';
585
+ const fired = actionKey.length > 0 && state.configModal.fireAction(actionKey, {
586
+ print: (message: string) => state.commandContext?.print(message),
587
+ executeCommand: state.commandContext?.executeCommand,
588
+ openModal: state.commandContext?.openModal,
589
+ });
590
+ if (fired) {
591
+ state.requestRender();
592
+ return true;
593
+ }
594
+
595
+ // Unhandled key while a config modal is open: drop any pending confirm and
596
+ // absorb the key (the modal owns the keyboard while it is active).
597
+ state.configModal.clearConfirmOnMiss();
598
+ state.requestRender();
599
+ return true;
600
+ }
@@ -40,12 +40,12 @@ export type EscapeState = ModalStackState & {
40
40
  selectionCallback: ((result: SelectionResult | null) => void) | null;
41
41
  bookmarkModal: ModalStackState['bookmarkModal'] & { open: () => void };
42
42
  contextInspectorModal: ModalStackState['contextInspectorModal'] & { open: () => void };
43
- processModal: ModalStackState['processModal'] & { open: () => void };
44
43
  settingsModal: ModalStackState['settingsModal'] & {
45
44
  editingMode: boolean;
46
45
  cancelEdit: () => void;
47
46
  };
48
47
  mcpWorkspace?: ModalStackState['mcpWorkspace'];
48
+ configModal: ModalStackState['configModal'] & { reopen: () => void };
49
49
  selectionModal: ModalStackState['selectionModal'];
50
50
  autocompleteReset: () => void;
51
51
  autocompleteUpdate?: (query: string) => void;
@@ -120,14 +120,12 @@ export function handleEscape(state: EscapeState): {
120
120
  shortcutsScrollOffset = 0;
121
121
  },
122
122
  closeBookmark: () => state.bookmarkModal.close(),
123
- closeAgentDetail: () => state.agentDetailModal.close(),
124
- closeLiveTail: () => state.liveTailModal.close(),
125
123
  closeSettings: () => state.settingsModal.close(),
126
124
  closeMcpWorkspace: () => state.mcpWorkspace?.close(),
127
125
  closeSessionPicker: () => state.sessionPickerModal.close(),
128
126
  closeProfilePicker: () => state.profilePickerModal.close(),
127
+ closeConfigModal: () => state.configModal.close(),
129
128
  closeContextInspector: () => state.contextInspectorModal.close(),
130
- closeProcess: () => state.processModal.close(),
131
129
  closeModelPicker: () => {
132
130
  state.modelPicker.close();
133
131
  state.restoreOnboardingModelPickerCancelState?.();
@@ -161,8 +159,8 @@ export function handleEscape(state: EscapeState): {
161
159
  openHelp: () => { helpOverlayActive = true; },
162
160
  openShortcuts: () => { shortcutsOverlayActive = true; },
163
161
  openBookmark: () => state.bookmarkModal.open(),
164
- openProcess: () => state.processModal.open(),
165
162
  openContextInspector: () => state.contextInspectorModal.open(),
163
+ openConfigModal: () => state.configModal.reopen(),
166
164
  openMcpWorkspace: () => state.mcpWorkspace?.reopen(),
167
165
  openOnboarding: () => state.onboardingWizard?.reopen(),
168
166
  openCommandMode: () => {
@@ -5,13 +5,11 @@ import type { BookmarkModal } from './bookmark-modal.ts';
5
5
  import type { SettingsModal } from './settings-modal.ts';
6
6
  import type { SessionPickerModal } from './session-picker-modal.ts';
7
7
  import type { ProfilePickerModal } from './profile-picker-modal.ts';
8
+ import type { ConfigModal } from './config-modal.ts';
8
9
  import type { HistorySearch } from './input-history.ts';
9
10
  import type { ModelPickerModal } from './model-picker.ts';
10
11
  import { handleMcpWorkspaceToken, type McpWorkspace } from './mcp-workspace.ts';
11
12
  import type { CommandContext } from './command-registry.ts';
12
- import type { LiveTailModal } from '../renderer/live-tail-modal.ts';
13
- import type { ProcessModal } from '../renderer/process-modal.ts';
14
- import type { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
15
13
  import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
16
14
  import type { FilePickerModal } from './file-picker.ts';
17
15
  import type { BlockActionsMenu, BlockActionId } from '../renderer/block-actions.ts';
@@ -21,6 +19,7 @@ import { handleHistorySearchToken, handleOverlayToken, handleSearchModeToken } f
21
19
  import { handleOnboardingWizardToken } from './onboarding/handler-onboarding-routes.ts';
22
20
  import {
23
21
  handleBookmarkModalToken,
22
+ handleConfigModalToken,
24
23
  handleProfilePickerToken,
25
24
  handleSelectionModalToken,
26
25
  handleSessionPickerToken,
@@ -30,9 +29,7 @@ import {
30
29
  handleBlockActionsToken,
31
30
  handleEscapeOnlyModalToken,
32
31
  handleFilePickerToken,
33
- handleLiveTailToken,
34
32
  handleModelPickerToken,
35
- handleProcessModalToken,
36
33
  } from './handler-picker-routes.ts';
37
34
 
38
35
  export type ModalTokenRouteState = {
@@ -47,6 +44,7 @@ export type ModalTokenRouteState = {
47
44
  mcpWorkspace: McpWorkspace;
48
45
  sessionPickerModal: SessionPickerModal;
49
46
  profilePickerModal: ProfilePickerModal;
47
+ configModal: ConfigModal;
50
48
  onboardingWizard: OnboardingWizardController;
51
49
  helpOverlayActive: boolean;
52
50
  helpScrollOffset: number;
@@ -61,9 +59,6 @@ export type ModalTokenRouteState = {
61
59
  getViewportHeight: () => number;
62
60
  requestRender: () => void;
63
61
  handleEscape: () => void;
64
- liveTailModal: LiveTailModal;
65
- processModal: ProcessModal;
66
- agentDetailModal: AgentDetailModal;
67
62
  contextInspectorModal: ContextInspectorModal;
68
63
  modalOpened: (name: string) => void;
69
64
  filePicker: FilePickerModal;
@@ -177,6 +172,15 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
177
172
  return withState(state, true);
178
173
  }
179
174
 
175
+ if (handleConfigModalToken({
176
+ configModal: state.configModal,
177
+ commandContext: state.commandContext,
178
+ requestRender: state.requestRender,
179
+ handleEscape: state.handleEscape,
180
+ }, token)) {
181
+ return withState(state, true);
182
+ }
183
+
180
184
  const overlayState = {
181
185
  helpOverlayActive: state.helpOverlayActive,
182
186
  helpScrollOffset: state.helpScrollOffset,
@@ -223,36 +227,10 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
223
227
  return withState(state, true);
224
228
  }
225
229
 
226
- if (handleLiveTailToken({
227
- liveTailModal: state.liveTailModal,
228
- processModal: state.processModal,
229
- requestRender: state.requestRender,
230
- handleEscape: state.handleEscape,
231
- }, token)) {
232
- return withState(state, true);
233
- }
234
-
235
- // Agent detail modal: route c + confirm keys before escape-close.
236
- // handleKey() consumes confirm-flow keys (y, Enter, n, Esc) and the 'c'
237
- // initiator; unhandled keys (including Esc when no confirm is pending)
238
- // fall through to escape-close below.
239
- if (state.agentDetailModal.active) {
240
- const keyStr: string =
241
- token.type === 'key' ? (token.logicalName ?? '') :
242
- token.type === 'text' ? token.value : '';
243
- if (keyStr && state.agentDetailModal.handleKey(keyStr)) {
244
- state.requestRender();
245
- return withState(state, true);
246
- }
247
- // 'c' was not consumed (non-cancellable), or any other key.
248
- // Esc closes the modal; all other keys are absorbed by the active modal.
249
- if (token.type === 'key' && token.logicalName === 'escape') {
250
- state.handleEscape();
251
- return withState(state, true);
252
- }
253
- state.requestRender();
254
- return withState(state, true);
255
- }
230
+ // W6.1 retirement: the live-tail, agent-detail, and process-modal token
231
+ // routes were removed with those modals (F2 now opens Fleet, which subsumes
232
+ // the live process tree; the three modals were structurally unreachable after
233
+ // the F2 repoint and were deleted).
256
234
 
257
235
  if (handleEscapeOnlyModalToken({
258
236
  active: state.contextInspectorModal.active,
@@ -262,17 +240,6 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
262
240
  return withState(state, true);
263
241
  }
264
242
 
265
- if (handleProcessModalToken({
266
- processModal: state.processModal,
267
- liveTailModal: state.liveTailModal,
268
- agentDetailModal: state.agentDetailModal,
269
- modalOpened: state.modalOpened,
270
- requestRender: state.requestRender,
271
- handleEscape: state.handleEscape,
272
- }, token)) {
273
- return withState(state, true);
274
- }
275
-
276
243
  const filePickerState = {
277
244
  filePicker: state.filePicker,
278
245
  prompt: state.prompt,
@@ -5,7 +5,6 @@ import type { CapabilityFilter, CategoryFilter, ModelPickerModal } from './model
5
5
  import { MODEL_PICKER_CHROME_LINES } from '../renderer/model-picker-overlay.ts';
6
6
  import { resolveAndValidatePath } from '@pellux/goodvibes-sdk/platform/utils';
7
7
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
8
- import type { ProcessEntry } from '../renderer/process-modal.ts';
9
8
  import type { BlockActionId } from '../renderer/block-actions.ts';
10
9
 
11
10
  type ModelPickerRouteState = {
@@ -93,6 +92,13 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
93
92
  }
94
93
  state.modelPicker.close();
95
94
  if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
95
+ } else if (mode === 'embeddingProvider') {
96
+ const selectedProvider = state.modelPicker.embeddingProviders[idx];
97
+ if (selectedProvider) {
98
+ state.commandContext?.completeEmbeddingProviderSelection?.(selectedProvider.id);
99
+ }
100
+ state.modelPicker.close();
101
+ if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
96
102
  } else if (mode === 'contextCap') {
97
103
  const capModel = state.modelPicker.contextCapPendingModel;
98
104
  if (capModel) {
@@ -201,99 +207,10 @@ function cycleCapabilityFilter(modelPicker: ModelPickerModal): void {
201
207
  modelPicker.setCapabilityFilter(cycle[(cur + 1) % cycle.length]!);
202
208
  }
203
209
 
204
- type ProcessRouteState = {
205
- processModal: {
206
- active: boolean;
207
- moveUp: () => void;
208
- moveDown: () => void;
209
- getSelected: () => ProcessEntry | undefined;
210
- close: () => void;
211
- open: () => void;
212
- killSelected: () => boolean;
213
- refresh: () => void;
214
- };
215
- liveTailModal: {
216
- open: (entry: ProcessEntry) => void;
217
- };
218
- agentDetailModal: {
219
- open: (id: string) => void;
220
- };
221
- modalOpened: (name: string) => void;
222
- requestRender: () => void;
223
- handleEscape: () => void;
224
- };
225
-
226
- export function handleProcessModalToken(state: ProcessRouteState, token: InputToken): boolean {
227
- if (!state.processModal.active) return false;
228
-
229
- if (token.type === 'key') {
230
- if (token.logicalName === 'escape') {
231
- state.handleEscape();
232
- return true;
233
- }
234
- if (token.logicalName === 'up') state.processModal.moveUp();
235
- else if (token.logicalName === 'down') state.processModal.moveDown();
236
- else if (token.logicalName === 'enter') {
237
- const entry = state.processModal.getSelected();
238
- if (entry) {
239
- if (entry.type === 'agent') {
240
- state.modalOpened('agentDetail');
241
- state.processModal.close();
242
- state.agentDetailModal.open(entry.id);
243
- } else {
244
- state.modalOpened('liveTail');
245
- state.processModal.close();
246
- state.liveTailModal.open(entry);
247
- }
248
- }
249
- }
250
- } else if (token.type === 'text' && token.value === 'k') {
251
- const killed = state.processModal.killSelected();
252
- if (killed) state.processModal.refresh();
253
- }
254
-
255
- state.requestRender();
256
- return true;
257
- }
258
-
259
- type LiveTailRouteState = {
260
- liveTailModal: {
261
- active: boolean;
262
- scrollUp: () => void;
263
- scrollDown: () => void;
264
- killProcess: () => void;
265
- close: () => void;
266
- };
267
- processModal: {
268
- open: () => void;
269
- };
270
- requestRender: () => void;
271
- handleEscape: () => void;
272
- };
273
-
274
- export function handleLiveTailToken(state: LiveTailRouteState, token: InputToken): boolean {
275
- if (!state.liveTailModal.active) return false;
276
-
277
- const killAndReturn = (): void => {
278
- state.liveTailModal.killProcess();
279
- state.handleEscape();
280
- };
281
-
282
- if (token.type === 'key') {
283
- if (token.logicalName === 'escape') {
284
- state.handleEscape();
285
- return true;
286
- }
287
- if (token.logicalName === 'up') state.liveTailModal.scrollUp();
288
- else if (token.logicalName === 'down') state.liveTailModal.scrollDown();
289
- else if (token.logicalName === 'k') killAndReturn();
290
- } else if (token.type === 'text' && token.value === 'k') {
291
- killAndReturn();
292
- }
293
-
294
- state.requestRender();
295
- return true;
296
- }
210
+ // W6.2 e / W6.1 retirement: the process-modal and live-tail-modal token routes
211
+ // were removed with those modals (F2 now opens Fleet, which subsumes the live
212
+ // process tree). ProcessModal/AgentDetailModal/LiveTailModal were structurally
213
+ // unreachable after the F2 repoint and were deleted.
297
214
 
298
215
  type EscapeOnlyModalRouteState = {
299
216
  active: boolean;
@@ -55,13 +55,16 @@ export function handleGlobalShortcutToken(
55
55
  ): boolean {
56
56
  if (token.type !== 'key') return false;
57
57
 
58
- // Fast-path: bare pageup/pagedown have no keybinding entry.
59
- if (token.logicalName === 'pageup') {
58
+ // Fast-path: BARE pageup/pagedown scroll the transcript. The `!token.ctrl`
59
+ // guard is load-bearing: Ctrl+PageUp/PageDown are the panel-tab-prev/next
60
+ // chords, so they must fall through to the keybinding lookup below instead of
61
+ // being swallowed here as a scroll.
62
+ if (token.logicalName === 'pageup' && !token.ctrl) {
60
63
  if (state.panelFocused) return false;
61
64
  state.scroll(-Math.max(1, viewportHeight - 2));
62
65
  return true;
63
66
  }
64
- if (token.logicalName === 'pagedown') {
67
+ if (token.logicalName === 'pagedown' && !token.ctrl) {
65
68
  if (state.panelFocused) return false;
66
69
  state.scroll(Math.max(1, viewportHeight - 2));
67
70
  return true;
@@ -101,6 +104,13 @@ export function handleGlobalShortcutToken(
101
104
  case 'panel-close': {
102
105
  const pm = state.panelManager;
103
106
  const active = pm.getActivePanel();
107
+ // Wave-3: give the active panel a chance to consume Ctrl+X for an
108
+ // in-panel action (FleetPanel session-tab detach) before it closes the
109
+ // panel outright — see Panel.interceptPanelClose's doc comment.
110
+ if (active?.interceptPanelClose?.()) {
111
+ state.requestRender();
112
+ return true;
113
+ }
104
114
  if (active) {
105
115
  pm.close(active.id);
106
116
  state.requestRender();
@@ -160,17 +170,17 @@ export function handleGlobalShortcutToken(
160
170
  }
161
171
 
162
172
  case 'panel-ops': {
163
- // Ctrl+O: open the Ops Control panel. Prefer the command-context callback
164
- // (wired only when the operator-control-plane feature flag is on) so it
165
- // reuses whatever show/open semantics bootstrap.ts set up; otherwise fall
166
- // back to opening the always-registered 'ops-control' panel type directly
167
- // it renders an honest not-configured state when its deps are absent.
168
- if (state.commandContext?.openOpsPanel) {
169
- state.commandContext.openOpsPanel();
170
- } else {
171
- state.panelManager.open('ops-control');
172
- state.requestRender();
173
- }
173
+ // Ctrl+O: open AND focus the Fleet panel. The former Ops Control panel was
174
+ // retired to an 'ops-control' -> 'fleet' alias (W6.1); rather than route
175
+ // through the now-aliased openOpsPanel callback (which opens without
176
+ // transferring focus), open 'fleet' directly and grab focus — mirroring
177
+ // the footer indicator's [Enter] -> openFleetPanel path so j/k/i/K land in
178
+ // the panel immediately instead of the composer.
179
+ const pm = state.panelManager;
180
+ pm.open('fleet');
181
+ pm.focusPanels();
182
+ state.panelFocused = true;
183
+ state.requestRender();
174
184
  return true;
175
185
  }
176
186
 
@@ -23,14 +23,12 @@ import type { SelectionManager } from './selection.ts';
23
23
  import type { InfiniteBuffer } from '../core/history.ts';
24
24
  import type { AutocompleteEngine } from './autocomplete.ts';
25
25
  import type { BookmarkModal } from './bookmark-modal.ts';
26
- import type { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
27
- import type { LiveTailModal } from '../renderer/live-tail-modal.ts';
28
26
  import type { SettingsModal } from './settings-modal.ts';
29
27
  import type { McpWorkspace } from './mcp-workspace.ts';
30
28
  import type { SessionPickerModal } from './session-picker-modal.ts';
29
+ import type { ConfigModal } from './config-modal.ts';
31
30
  import type { ProfilePickerModal } from './profile-picker-modal.ts';
32
31
  import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
33
- import type { ProcessModal } from '../renderer/process-modal.ts';
34
32
  import type { FilePickerModal } from './file-picker.ts';
35
33
  import type { BlockActionsMenu } from '../renderer/block-actions.ts';
36
34
  import type { SelectionModal } from './selection-modal.ts';
@@ -105,14 +103,12 @@ export interface InputHandlerLike {
105
103
 
106
104
  // ── Modal objects ─────────────────────────────────────────────────────────
107
105
  bookmarkModal: BookmarkModal;
108
- agentDetailModal: AgentDetailModal;
109
- liveTailModal: LiveTailModal;
110
106
  settingsModal: SettingsModal;
111
107
  mcpWorkspace: McpWorkspace;
112
108
  sessionPickerModal: SessionPickerModal;
113
109
  profilePickerModal: ProfilePickerModal;
110
+ configModal: ConfigModal;
114
111
  contextInspectorModal: ContextInspectorModal;
115
- processModal: ProcessModal;
116
112
  modelPicker: ModelPickerModal;
117
113
  filePicker: FilePickerModal;
118
114
  blockActionsMenu: BlockActionsMenu;