@pellux/goodvibes-tui 1.0.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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. package/src/renderer/qr-renderer.ts +0 -120
@@ -427,6 +427,8 @@ export function handleCtrlC(
427
427
  lastCtrlCTime: number,
428
428
  setLastCtrlCTime: (value: number) => void,
429
429
  setShowExitNotice: (value: boolean) => void,
430
+ lastCtrlCTimeoutId: ReturnType<typeof setTimeout> | null,
431
+ setLastCtrlCTimeoutId: (value: ReturnType<typeof setTimeout> | null) => void,
430
432
  ): void {
431
433
  if (prompt.length > 0) {
432
434
  saveUndoState();
@@ -436,17 +438,29 @@ export function handleCtrlC(
436
438
  }
437
439
  cancelGeneration?.();
438
440
  const now = Date.now();
441
+ // Clear any hide-timer pending from a prior press before deciding this
442
+ // one's outcome, whichever branch below runs. Without this, a hide-timer
443
+ // scheduled by an earlier press could still fire after a newer notice
444
+ // window opened, or after exitApp() was just called (if exitApp isn't
445
+ // synchronous) — flipping showExitNotice/requestRender during a shutdown
446
+ // the user already believes is in progress.
447
+ if (lastCtrlCTimeoutId !== null) {
448
+ clearTimeout(lastCtrlCTimeoutId);
449
+ setLastCtrlCTimeoutId(null);
450
+ }
439
451
  if (now - lastCtrlCTime < 1000) {
440
452
  exitApp();
441
- } else {
442
- setLastCtrlCTime(now);
443
- setShowExitNotice(true);
444
- requestRender();
445
- setTimeout(() => {
446
- setShowExitNotice(false);
447
- requestRender();
448
- }, 1000);
453
+ return;
449
454
  }
455
+ setLastCtrlCTime(now);
456
+ setShowExitNotice(true);
457
+ requestRender();
458
+ const timeoutId = setTimeout(() => {
459
+ setShowExitNotice(false);
460
+ setLastCtrlCTimeoutId(null);
461
+ requestRender();
462
+ }, 1000);
463
+ setLastCtrlCTimeoutId(timeoutId);
450
464
  }
451
465
 
452
466
  export function handleClipboardPaste(
@@ -30,6 +30,27 @@ export type PanelFocusRouteState = {
30
30
  handlePathCompletion: () => boolean;
31
31
  cyclePanelTab: (direction: 'next' | 'prev') => void;
32
32
  onPanelInputConsumed?: (activePanel: import('../panels/types.ts').Panel | null, key: string) => void;
33
+ /**
34
+ * True when THIS token is a paste: a single 'text' token whose value holds
35
+ * more than one character. The SDK tokenizer emits a bracketed paste
36
+ * (\x1b[?2004h, enabled in main.ts init) as exactly one such token; discrete
37
+ * keystrokes — even several batched into one feed() by render-tick latency —
38
+ * arrive as separate 1-char tokens. See the text-token branch below.
39
+ */
40
+ isPasteToken: boolean;
41
+ /**
42
+ * One-shot hint emitted when a paste is dropped because the focused panel has
43
+ * no text capture (Invariant A: focus must not silently flip to the composer).
44
+ */
45
+ onPasteDropped?: (panelName: string) => void;
46
+ /**
47
+ * True while a turn is actively streaming; gates Escape's cancel-first
48
+ * precedence (I6.1) ahead of the panel's own two-stage escape contract
49
+ * below.
50
+ */
51
+ isTurnActive: () => boolean;
52
+ /** Cancel the active turn. Wired to commandContext.cancelGeneration. */
53
+ cancelGeneration: () => void;
33
54
  };
34
55
 
35
56
  export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputToken): {
@@ -66,6 +87,25 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
66
87
  // (e.g. dismiss a confirm dialog or clear search). Only unfocus if the
67
88
  // panel returns false (unconsumed) or there is no active panel.
68
89
  if (token.logicalName === 'escape') {
90
+ // I6.1: while a turn is streaming, a focused panel must not be able to
91
+ // swallow the only way to cancel it (replay R5) — Escape's first job is
92
+ // always cancel-turn. The panel's own two-stage consume-or-unfocus
93
+ // contract below only runs once no turn is active, so a *second*
94
+ // Escape (now with the turn already cancelled) falls through to it
95
+ // normally. panelFocused is left unchanged here (panel stays open and
96
+ // focused) — the panel close moves to that second Escape, or to
97
+ // Ctrl+X (panel-close, keybindings.ts), which is handled earlier in
98
+ // handleGlobalShortcutToken and is unaffected by this branch. This
99
+ // deliberately calls cancelGeneration() directly instead of routing
100
+ // through the shared handleEscape() (handler-modal-stack.ts) — that
101
+ // function's fallthrough order (modal-pop -> active-modal-close ->
102
+ // clear-nonempty-prompt -> cancelGeneration) is not guaranteed to reach
103
+ // cancelGeneration first in every state combination.
104
+ if (state.isTurnActive()) {
105
+ state.cancelGeneration();
106
+ state.requestRender();
107
+ return { handled: true, panelFocused };
108
+ }
69
109
  const activePanel = state.panelManager.getActive();
70
110
  const panelConsumedEscape = activePanel?.handleInput?.('escape') ?? false;
71
111
  if (panelConsumedEscape) {
@@ -113,6 +153,20 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
113
153
 
114
154
  if (token.type === 'text' && token.value) {
115
155
  const activePanel = state.panelManager.getActive();
156
+ // Invariant A/B (W6.2). A paste (isPasteToken: one multi-char text token)
157
+ // into a focused text-capturing panel (a `/`-search or steer-draft field —
158
+ // isCapturingTextBurst) is forwarded verbatim below; into any other focused
159
+ // panel it is DROPPED with a one-shot hint — never exploded into per-char
160
+ // hotkeys, and never a silent focus flip to the composer (what the old
161
+ // per-feed char-sum burst guard did, which also misfired on two quick nav
162
+ // keys in one feed). Focus moves only on an explicit transfer verb, so
163
+ // panelFocused is left unchanged. Discrete 1-char keystrokes are not pastes
164
+ // and fall through to the per-char dispatch below, one at a time.
165
+ if (state.isPasteToken && !activePanel?.isCapturingTextBurst?.()) {
166
+ state.onPasteDropped?.(activePanel?.name ?? 'the panel');
167
+ state.requestRender();
168
+ return { handled: true, panelFocused };
169
+ }
116
170
  if (activePanel?.handleInput) {
117
171
  for (const ch of token.value) {
118
172
  activePanel.handleInput(ch);
@@ -128,7 +182,8 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
128
182
  export type IndicatorFocusRouteState = {
129
183
  indicatorFocused: boolean;
130
184
  modalOpened: (name: string) => void;
131
- processModal: { open: () => void };
185
+ /** W2.2: the footer process indicator's [Enter] opens the Fleet panel (F2 also opens the Fleet panel; the retired process modal was removed in W6.2). */
186
+ openFleetPanel: () => void;
132
187
  requestRender: () => void;
133
188
  };
134
189
 
@@ -149,8 +204,9 @@ export function handleIndicatorFocusToken(state: IndicatorFocusRouteState, token
149
204
  }
150
205
  if (token.logicalName === 'enter') {
151
206
  indicatorFocused = false;
152
- state.modalOpened('process');
153
- state.processModal.open();
207
+ // W2.2: repointed from the retired process modal (removed in W6.2) to the
208
+ // Fleet panel — F2 also opens the Fleet panel (handlePromptKeyToken f2).
209
+ state.openFleetPanel();
154
210
  state.requestRender();
155
211
  return { handled: true, indicatorFocused };
156
212
  }
@@ -229,7 +285,15 @@ export function handlePromptTextToken(state: TextRouteState, token: InputToken):
229
285
  }
230
286
  }
231
287
 
232
- if (prompt === '/' && state.commandRegistry) {
288
+ if (prompt === '/') {
289
+ // Arm commandMode as soon as the prompt becomes a bare '/', regardless of
290
+ // whether commandRegistry has been (re)attached yet — this is a one-shot
291
+ // transition (the only place commandMode ever becomes true), and gating
292
+ // it on commandRegistry meant a transient null during a modal/overlay
293
+ // handoff would permanently miss the window: every following keystroke
294
+ // is then processed as plain chat text with no way to recover. Dispatch
295
+ // (handleCommandModeToken, and the enter-key fallback below) still checks
296
+ // commandRegistry itself before actually running anything.
233
297
  commandMode = true;
234
298
  state.modalOpened('command');
235
299
  state.autocomplete?.update('');
@@ -257,9 +321,18 @@ export type KeyRouteState = {
257
321
  indicatorFocused: boolean;
258
322
  conversationManager: ConversationManager | null;
259
323
  commandContext: CommandContext | undefined;
324
+ /**
325
+ * Optional: only used by the enter-key desync safety net below (a stray
326
+ * slash-prefixed submission with commandMode somehow still false). When
327
+ * absent, that fallback simply doesn't trigger — the primary fix (arming
328
+ * commandMode on the '/' keystroke regardless of registry attachment, in
329
+ * handlePromptTextToken above) is what actually prevents the desync.
330
+ */
331
+ commandRegistry?: CommandRegistry | null;
260
332
  autocomplete: AutocompleteEngine | null;
261
333
  blockActionsMenu: { open: (block: BlockMeta) => void };
262
- processModal: { open: () => void };
334
+ /** W6.2 e: F2 opens+focuses the Fleet panel (which subsumes the retired process modal). */
335
+ openFleetPanel: () => void;
263
336
  modalOpened: (name: string) => void;
264
337
  saveUndoState: () => void;
265
338
  /** Break the undo coalescing group (call on cursor moves). */
@@ -351,6 +424,30 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
351
424
  return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
352
425
  }
353
426
  if (text) {
427
+ // Safety net for a desynced commandMode: text is command-shaped (starts
428
+ // with '/') but commandMode never armed — e.g. the '/' keystroke landed
429
+ // during a modal/overlay handoff window. handleCommandModeToken (the
430
+ // normal dispatch path for '/name ...') never runs in this state since
431
+ // it early-returns when commandMode is false, so without this the text
432
+ // would fall straight through to submitInput as ordinary chat. Re-derive
433
+ // intent from the literal text instead of trusting commandMode alone.
434
+ if (!commandMode && text.startsWith('/') && state.commandRegistry && state.commandContext?.executeCommand) {
435
+ const parts = text.slice(1).trim().split(/\s+/);
436
+ const name = parts[0];
437
+ const args = parts.slice(1);
438
+ prompt = '';
439
+ cursorPos = 0;
440
+ if (name) {
441
+ const executeCommand = state.commandContext.executeCommand;
442
+ void executeCommand(name, args).then((handled) => {
443
+ if (!handled) {
444
+ state.commandContext?.submitInput?.(text);
445
+ }
446
+ state.requestRender();
447
+ });
448
+ }
449
+ return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
450
+ }
354
451
  const expanded = state.expandPrompt(text);
355
452
  const historyRecallText = typeof expanded === 'string'
356
453
  ? expanded
@@ -508,8 +605,11 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
508
605
 
509
606
  if (token.logicalName === 'f2') {
510
607
  indicatorFocused = false;
511
- state.modalOpened('process');
512
- state.processModal.open();
608
+ // W6.2 e: F2 opens AND focuses the Fleet panel, which subsumes the deleted
609
+ // process modal (ProcessModal/AgentDetailModal/LiveTailModal — removed in
610
+ // W6.1 once this repoint made them unreachable). openFleetPanel sets
611
+ // panelFocused on the shared context directly, so nothing more is returned.
612
+ state.openFleetPanel();
513
613
  return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
514
614
  }
515
615
 
@@ -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,7 +179,34 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
180
179
  const lineCount = history.getLineCount();
181
180
  const keybindings = context.keybindingsManager;
182
181
 
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
+ };
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.
183
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
+
184
210
  if (token.type === 'key' && context.keybindingsManager.matches('clear-cancel', token)) {
185
211
  context.handleCtrlC();
186
212
  continue;
@@ -200,6 +226,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
200
226
  mcpWorkspace: context.mcpWorkspace,
201
227
  sessionPickerModal: context.sessionPickerModal,
202
228
  profilePickerModal: context.profilePickerModal,
229
+ configModal: context.configModal,
203
230
  onboardingWizard: context.onboardingWizard,
204
231
  helpOverlayActive: context.helpOverlayActive,
205
232
  helpScrollOffset: context.helpScrollOffset,
@@ -214,9 +241,6 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
214
241
  getViewportHeight: context.getViewportHeight,
215
242
  requestRender: context.requestRender,
216
243
  handleEscape: context.handleEscape,
217
- liveTailModal: context.liveTailModal,
218
- processModal: context.processModal,
219
- agentDetailModal: context.agentDetailModal,
220
244
  contextInspectorModal: context.contextInspectorModal,
221
245
  modalOpened: context.modalOpened,
222
246
  filePicker: context.filePicker,
@@ -320,6 +344,17 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
320
344
  panelManager: context.panelManager,
321
345
  keybindingsManager: context.keybindingsManager,
322
346
  onPanelInputConsumed: context.onPanelInputConsumed,
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?.(),
323
358
  }, token);
324
359
  context.panelFocused = panelRoute.panelFocused;
325
360
  if (panelRoute.handled) {
@@ -329,7 +364,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
329
364
  const indicatorRoute = handleIndicatorFocusToken({
330
365
  indicatorFocused: context.indicatorFocused,
331
366
  modalOpened: context.modalOpened,
332
- processModal: context.processModal,
367
+ openFleetPanel,
333
368
  requestRender: context.requestRender,
334
369
  }, token);
335
370
  context.indicatorFocused = indicatorRoute.indicatorFocused;
@@ -408,9 +443,10 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
408
443
  indicatorFocused: context.indicatorFocused,
409
444
  conversationManager: context.conversationManager,
410
445
  commandContext: context.commandContext,
446
+ commandRegistry: context.commandRegistry,
411
447
  autocomplete: context.autocomplete,
412
448
  blockActionsMenu: { open: (block: BlockMeta) => context.blockActionsMenu.open(block) },
413
- processModal: context.processModal,
449
+ openFleetPanel,
414
450
  modalOpened: context.modalOpened,
415
451
  saveUndoState: context.saveUndoState,
416
452
  breakUndoCoalesce: context.breakUndoCoalesce,
@@ -193,6 +193,8 @@ export function handleCtrlCForHandler(handler: InputHandler): void {
193
193
  handler.lastCtrlCTime,
194
194
  (value) => { handler.lastCtrlCTime = value; },
195
195
  (value) => { handler.showExitNotice = value; },
196
+ handler.lastCtrlCTimeoutId,
197
+ (value) => { handler.lastCtrlCTimeoutId = value; },
196
198
  );
197
199
  }
198
200
 
@@ -223,14 +225,12 @@ export function handleEscapeForHandler(handler: InputHandler): void {
223
225
  helpOverlayActive: handler.helpOverlayActive,
224
226
  shortcutsOverlayActive: handler.shortcutsOverlayActive,
225
227
  bookmarkModal: handler.bookmarkModal,
226
- agentDetailModal: handler.agentDetailModal,
227
- liveTailModal: handler.liveTailModal,
228
228
  settingsModal: handler.settingsModal,
229
229
  mcpWorkspace: handler.mcpWorkspace,
230
230
  sessionPickerModal: handler.sessionPickerModal,
231
231
  profilePickerModal: handler.profilePickerModal,
232
+ configModal: handler.configModal,
232
233
  contextInspectorModal: handler.contextInspectorModal,
233
- processModal: handler.processModal,
234
234
  modelPicker: handler.modelPicker,
235
235
  filePicker: handler.filePicker,
236
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,