@pellux/goodvibes-tui 0.18.20 → 0.19.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 (83) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +7 -3
  5. package/src/core/conversation-rendering.ts +22 -6
  6. package/src/core/orchestrator.ts +1 -1
  7. package/src/input/commands/diff-runtime.ts +6 -5
  8. package/src/input/commands/guidance-runtime.ts +1 -1
  9. package/src/input/commands/health-runtime.ts +2 -2
  10. package/src/input/commands/local-setup-review.ts +1 -1
  11. package/src/input/commands/session-content.ts +1 -1
  12. package/src/input/commands/session.ts +0 -1
  13. package/src/input/commands/shell-core.ts +3 -2
  14. package/src/input/commands/skills-runtime.ts +2 -2
  15. package/src/input/commands/subscription-runtime.ts +4 -4
  16. package/src/input/feed-context-factory.ts +236 -0
  17. package/src/input/handler-feed.ts +44 -6
  18. package/src/input/handler-shortcuts.ts +138 -125
  19. package/src/input/handler.ts +119 -119
  20. package/src/input/keybindings.ts +30 -0
  21. package/src/input/panel-integration-actions.ts +2 -1
  22. package/src/input/settings-modal-types.ts +60 -0
  23. package/src/input/settings-modal.ts +83 -65
  24. package/src/panels/agent-inspector-panel.ts +10 -9
  25. package/src/panels/agent-logs-panel.ts +26 -6
  26. package/src/panels/approval-panel.ts +55 -82
  27. package/src/panels/automation-control-panel.ts +120 -161
  28. package/src/panels/base-panel.ts +108 -3
  29. package/src/panels/communication-panel.ts +69 -107
  30. package/src/panels/context-visualizer-panel.ts +2 -0
  31. package/src/panels/control-plane-panel.ts +117 -172
  32. package/src/panels/diff-panel.ts +2 -0
  33. package/src/panels/file-explorer-panel.ts +51 -31
  34. package/src/panels/file-preview-panel.ts +57 -35
  35. package/src/panels/git-panel.ts +12 -13
  36. package/src/panels/hooks-panel.ts +103 -138
  37. package/src/panels/incident-review-panel.ts +59 -109
  38. package/src/panels/knowledge-panel.ts +75 -107
  39. package/src/panels/local-auth-panel.ts +77 -93
  40. package/src/panels/marketplace-panel.ts +51 -69
  41. package/src/panels/mcp-panel.ts +110 -155
  42. package/src/panels/memory-panel.ts +90 -158
  43. package/src/panels/ops-control-panel.ts +51 -85
  44. package/src/panels/orchestration-panel.ts +70 -51
  45. package/src/panels/panel-list-panel.ts +5 -4
  46. package/src/panels/panel-manager.ts +25 -2
  47. package/src/panels/plan-dashboard-panel.ts +2 -0
  48. package/src/panels/plugins-panel.ts +37 -60
  49. package/src/panels/polish.ts +51 -2
  50. package/src/panels/provider-accounts-panel.ts +1 -0
  51. package/src/panels/provider-health-panel.ts +6 -8
  52. package/src/panels/routes-panel.ts +91 -141
  53. package/src/panels/schedule-panel.ts +7 -6
  54. package/src/panels/scrollable-list-panel.ts +64 -16
  55. package/src/panels/security-panel.ts +118 -152
  56. package/src/panels/services-panel.ts +63 -105
  57. package/src/panels/session-browser-panel.ts +19 -18
  58. package/src/panels/settings-sync-panel.ts +79 -123
  59. package/src/panels/skills-panel.ts +114 -230
  60. package/src/panels/subscription-panel.ts +64 -86
  61. package/src/panels/system-messages-panel.ts +147 -141
  62. package/src/panels/tasks-panel.ts +130 -179
  63. package/src/panels/token-budget-panel.ts +2 -0
  64. package/src/panels/watchers-panel.ts +89 -137
  65. package/src/panels/worktree-panel.ts +1 -0
  66. package/src/panels/wrfc-panel.ts +2 -0
  67. package/src/renderer/agent-detail-modal.ts +2 -2
  68. package/src/renderer/ansi-sanitize.ts +76 -0
  69. package/src/renderer/buffer.ts +23 -1
  70. package/src/renderer/diff.ts +8 -0
  71. package/src/renderer/help-overlay.ts +48 -28
  72. package/src/renderer/markdown.ts +3 -145
  73. package/src/renderer/settings-modal-helpers.ts +27 -0
  74. package/src/renderer/settings-modal.ts +18 -1
  75. package/src/renderer/status-glyphs.ts +21 -0
  76. package/src/renderer/status-token.ts +4 -8
  77. package/src/renderer/tool-call.ts +4 -3
  78. package/src/runtime/bootstrap-core.ts +1 -1
  79. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  80. package/src/runtime/bootstrap.ts +7 -8
  81. package/src/runtime/diagnostics/panels/policy.ts +2 -1
  82. package/src/shell/ui-openers.ts +1 -1
  83. package/src/version.ts +1 -1
@@ -0,0 +1,236 @@
1
+ /**
2
+ * feed-context-factory.ts — Construction and mutable-field sync for InputFeedContext.
3
+ *
4
+ * Extracted from handler.ts (Wave 4α review, 0.18.23) to keep handler.ts under the
5
+ * 800-line architecture cap.
6
+ *
7
+ * Two exported functions:
8
+ * - `buildInitialFeedContext` — assembles the long-lived InputFeedContext from
9
+ * pre-built field groups. Called once at InputHandler construction time.
10
+ * - `syncFeedContextMutableFields` — copies mutable scalar fields into the
11
+ * already-allocated context before each feed() dispatch.
12
+ */
13
+ import type { InputFeedContext } from './handler-feed.ts';
14
+ import type { SelectionManager } from './selection.ts';
15
+ import type { InfiniteBuffer } from '../core/history.ts';
16
+ import type { CommandRegistry, CommandContext } from './command-registry.ts';
17
+ import type { AutocompleteEngine } from './autocomplete.ts';
18
+ import type { FilePickerModal } from './file-picker.ts';
19
+ import type { ModelPickerModal } from './model-picker.ts';
20
+ import type { SelectionModal } from './selection-modal.ts';
21
+ import type { SelectionResult } from './selection-modal.ts';
22
+ import type { SearchManager } from './search.ts';
23
+ import type { InputHistory, HistorySearch } from './input-history.ts';
24
+ import type { ConversationManager } from '../core/conversation';
25
+ import type { ProcessModal } from '../renderer/process-modal.ts';
26
+ import type { LiveTailModal } from '../renderer/live-tail-modal.ts';
27
+ import type { BlockActionsMenu } from '../renderer/block-actions.ts';
28
+ import type { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
29
+ import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
30
+ import type { BookmarkModal } from './bookmark-modal.ts';
31
+ import type { SettingsModal } from './settings-modal.ts';
32
+ import type { SessionPickerModal } from './session-picker-modal.ts';
33
+ import type { ProfilePickerModal } from './profile-picker-modal.ts';
34
+ import type { WrappedPromptInfo } from './handler-prompt-buffer.ts';
35
+ import type { Panel } from '../panels/types.ts';
36
+ import type { PanelManager } from '../panels/panel-manager.ts';
37
+ import type { KeybindingsManager } from './keybindings.ts';
38
+
39
+ /**
40
+ * Initial mutable scalar values for InputFeedContext.
41
+ *
42
+ * **Mutable fields** (synced per-feed via syncFeedContextMutableFields or inside
43
+ * action closures that call syncFeedContextMutableFields):
44
+ * - `prompt`, `cursorPos` — current text buffer state
45
+ * - `commandMode`, `panelFocused`, `indicatorFocused` — focus-mode flags
46
+ * - `helpOverlayActive`, `helpScrollOffset` — help overlay state
47
+ * - `shortcutsOverlayActive`, `shortcutsScrollOffset` — shortcuts overlay state
48
+ * - `nextPasteId`, `nextImageId` — monotonically increasing ID counters
49
+ * - `mouseDownRow`, `mouseDownCol` — drag-tracking coordinates
50
+ * - `contentWidth` — reflow width, updated by setContentWidth()
51
+ * - `selectionCallback` — current selection modal callback (nullable)
52
+ */
53
+ export interface FeedContextMutableInit {
54
+ prompt: string;
55
+ cursorPos: number;
56
+ commandMode: boolean;
57
+ panelFocused: boolean;
58
+ indicatorFocused: boolean;
59
+ helpOverlayActive: boolean;
60
+ helpScrollOffset: number;
61
+ shortcutsOverlayActive: boolean;
62
+ shortcutsScrollOffset: number;
63
+ nextPasteId: number;
64
+ nextImageId: number;
65
+ mouseDownRow: number;
66
+ mouseDownCol: number;
67
+ contentWidth: number;
68
+ selectionCallback: ((result: SelectionResult | null) => void) | null;
69
+ }
70
+
71
+ /**
72
+ * Stable (readonly) service references for InputFeedContext.
73
+ *
74
+ * **Stable references** (set once at construction, never reallocated):
75
+ * - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
76
+ * - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
77
+ * `profilePickerModal` — modal objects constructed once
78
+ * - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`,
79
+ * `agentDetailModal`, `contextInspectorModal`, `blockActionsMenu`,
80
+ * `searchManager`, `historySearch` — service objects constructed once
81
+ * - `panelManager`, `keybindingsManager` — from uiServices, stable
82
+ * - `modalStack` — reference to the handler's shared array
83
+ * - `getHistory`, `getViewportHeight`, `getScrollTop`, `scroll`, `exitApp` — callbacks
84
+ * - `commandRegistry`, `commandContext`, `autocomplete`, `inputHistory`,
85
+ * `conversationManager` — wired after construction; synced at feed() entry only
86
+ * (not per-action) since no in-feed action changes them
87
+ *
88
+ * **Rationale:** per-feed mutation on a single object avoids per-keystroke GC pressure
89
+ * from ~80-field object allocation. Stable references are service handles that never
90
+ * need to change after construction.
91
+ */
92
+ export interface FeedContextStableRefs {
93
+ selection: SelectionManager;
94
+ pasteRegistry: Map<string, string>;
95
+ imageRegistry: Map<string, { data: string; mediaType: string }>;
96
+ selectionModal: SelectionModal;
97
+ bookmarkModal: BookmarkModal;
98
+ settingsModal: SettingsModal;
99
+ sessionPickerModal: SessionPickerModal;
100
+ profilePickerModal: ProfilePickerModal;
101
+ historySearch: HistorySearch;
102
+ commandRegistry: CommandRegistry | null;
103
+ commandContext: CommandContext | undefined;
104
+ autocomplete: AutocompleteEngine | null;
105
+ filePicker: FilePickerModal;
106
+ modelPicker: ModelPickerModal;
107
+ processModal: ProcessModal;
108
+ liveTailModal: LiveTailModal;
109
+ agentDetailModal: AgentDetailModal;
110
+ contextInspectorModal: ContextInspectorModal;
111
+ blockActionsMenu: BlockActionsMenu;
112
+ searchManager: SearchManager;
113
+ modalStack: string[];
114
+ inputHistory: InputHistory | null;
115
+ conversationManager: ConversationManager | null;
116
+ panelManager: PanelManager;
117
+ keybindingsManager: KeybindingsManager;
118
+ getHistory: () => InfiniteBuffer;
119
+ getViewportHeight: () => number;
120
+ getScrollTop: () => number;
121
+ scroll: (delta: number) => void;
122
+ exitApp: () => void;
123
+ }
124
+
125
+ /** Bound method closures for InputFeedContext. Built in handler.ts where private members are accessible. */
126
+ export interface FeedContextClosures {
127
+ modalOpened: (name: string) => void;
128
+ handleEscape: () => void;
129
+ handleCopy: () => void;
130
+ handleCtrlC: () => void;
131
+ handleBlockCopy: () => void;
132
+ handleBookmark: () => void;
133
+ handleBlockSave: () => void;
134
+ handleDiffApply: () => boolean;
135
+ handleUndo: () => void;
136
+ handleRedo: () => void;
137
+ handlePaste: () => void;
138
+ saveUndoState: () => void;
139
+ ensureInputCursorVisible: (contentWidth?: number) => void;
140
+ registerPaste: (content: string) => string;
141
+ executeBlockAction: (id: string) => void;
142
+ cyclePanelTab: (direction: 'next' | 'prev') => void;
143
+ onPanelInputConsumed: (activePanel: Panel | null, key: string) => void;
144
+ getWrappedPromptInfo: (contentWidth: number) => WrappedPromptInfo;
145
+ moveCursorVertical: (direction: -1 | 1) => boolean;
146
+ handlePathCompletion: () => boolean;
147
+ handleBlockToggle: () => void;
148
+ findMarkerAtPos: (pos: number) => { start: number; end: number } | null;
149
+ cleanupMarkerRegistry: (text: string) => void;
150
+ expandPrompt: (text: string) => string | import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[];
151
+ }
152
+
153
+ /**
154
+ * buildInitialFeedContext — Allocate the single InputFeedContext that lives for the
155
+ * lifetime of the InputHandler.
156
+ *
157
+ * Accepts pre-built field groups from handler.ts where private access is available.
158
+ * The resulting context object is mutated in place on every feed() call via
159
+ * syncFeedContextMutableFields — no re-allocation occurs per keystroke.
160
+ *
161
+ * @param mutable Initial mutable scalar values (synced per-feed).
162
+ * @param stable Stable service references (set once, never replaced).
163
+ * @param closures Pre-built bound method closures (built in handler.ts).
164
+ */
165
+ export function buildInitialFeedContext(
166
+ mutable: FeedContextMutableInit,
167
+ stable: FeedContextStableRefs,
168
+ closures: FeedContextClosures,
169
+ ): InputFeedContext {
170
+ const noop = (): void => {};
171
+ return {
172
+ // --- mutable scalars ---
173
+ ...mutable,
174
+ // --- requestRender: placeholder, swapped per-feed to buffered version ---
175
+ requestRender: noop,
176
+ // --- stable refs ---
177
+ ...stable,
178
+ // --- closures ---
179
+ ...closures,
180
+ };
181
+ }
182
+
183
+ /**
184
+ * syncFeedContextMutableFields — Copy mutable scalar fields into the reused
185
+ * InputFeedContext object.
186
+ *
187
+ * **Fields synced (updated on every call):**
188
+ * - `prompt` — current prompt text buffer
189
+ * - `cursorPos` — caret position within prompt
190
+ * - `commandMode` — whether command-mode prefix is active
191
+ * - `panelFocused` — whether the active panel owns keyboard focus
192
+ * - `indicatorFocused` — whether the status indicator owns focus
193
+ * - `helpOverlayActive` / `helpScrollOffset` — help overlay visibility and scroll
194
+ * - `shortcutsOverlayActive` / `shortcutsScrollOffset` — shortcuts overlay state
195
+ * - `selectionCallback` — the current in-flight selection modal callback
196
+ * - `nextPasteId` / `nextImageId` — monotonically increasing allocation counters
197
+ * - `mouseDownRow` / `mouseDownCol` — drag-start coordinates
198
+ *
199
+ * **Fields intentionally excluded (synced only at feed() entry, not here):**
200
+ * - `contentWidth` — semi-stable; only changes on terminal resize via setContentWidth().
201
+ * Synced at feed() entry because it is only relevant for layout, not action-reaction
202
+ * sequences within a single feed.
203
+ * - `commandRegistry`, `commandContext` — wired after construction via
204
+ * setCommandRegistry(); synced at feed() entry since no in-feed action changes them.
205
+ * - `autocomplete` — wired after construction; synced at feed() entry.
206
+ * - `inputHistory`, `conversationManager` — late-wired service handles; stable within
207
+ * a feed. Synced at feed() entry only.
208
+ * - `requestRender` — swapped to a buffered version at feed() entry and restored in
209
+ * the finally block; not managed by this function.
210
+ *
211
+ * Called from within action closures (`handleEscape`, `handleCtrlC`, `handleUndo`,
212
+ * `handleRedo`, `handlePaste`) that mutate handler state mid-feed, so subsequent
213
+ * route handlers see updated values without re-reading handler fields.
214
+ *
215
+ * @param fields Current mutable field values from the handler.
216
+ * @param ctx The InputFeedContext to update in place.
217
+ */
218
+ export function syncFeedContextMutableFields(
219
+ fields: FeedContextMutableInit,
220
+ ctx: InputFeedContext,
221
+ ): void {
222
+ ctx.prompt = fields.prompt;
223
+ ctx.cursorPos = fields.cursorPos;
224
+ ctx.commandMode = fields.commandMode;
225
+ ctx.panelFocused = fields.panelFocused;
226
+ ctx.indicatorFocused = fields.indicatorFocused;
227
+ ctx.helpOverlayActive = fields.helpOverlayActive;
228
+ ctx.helpScrollOffset = fields.helpScrollOffset;
229
+ ctx.shortcutsOverlayActive = fields.shortcutsOverlayActive;
230
+ ctx.shortcutsScrollOffset = fields.shortcutsScrollOffset;
231
+ ctx.selectionCallback = fields.selectionCallback;
232
+ ctx.nextPasteId = fields.nextPasteId;
233
+ ctx.nextImageId = fields.nextImageId;
234
+ ctx.mouseDownRow = fields.mouseDownRow;
235
+ ctx.mouseDownCol = fields.mouseDownCol;
236
+ }
@@ -40,6 +40,44 @@ import { SelectionManager } from './selection.ts';
40
40
  import type { PanelManager } from '../panels/panel-manager.ts';
41
41
  import type { KeybindingsManager } from './keybindings.ts';
42
42
 
43
+ /**
44
+ * InputFeedContext — The single long-lived context object passed to feedInputTokens
45
+ * on every keystroke. Allocated once at InputHandler construction; mutated in place
46
+ * per-feed to avoid per-keystroke GC pressure from ~80-field object allocation.
47
+ *
48
+ * **Mutable per-feed** (synced from handler at the top of every feed() call, and
49
+ * updated inside action closures via syncFeedContextMutableFields):
50
+ * - `prompt`, `cursorPos` — current text buffer state
51
+ * - `commandMode`, `panelFocused`, `indicatorFocused` — focus-mode flags
52
+ * - `helpOverlayActive`, `helpScrollOffset` — help overlay visibility and scroll
53
+ * - `shortcutsOverlayActive`, `shortcutsScrollOffset` — shortcuts overlay state
54
+ * - `nextPasteId`, `nextImageId` — monotonically increasing ID counters
55
+ * - `mouseDownRow`, `mouseDownCol` — drag-tracking coordinates
56
+ * - `contentWidth` — reflow width (semi-stable; synced at feed() entry only)
57
+ * - `selectionCallback` — current in-flight selection modal callback (nullable)
58
+ * - `requestRender` — swapped per-feed to a buffered version, restored after
59
+ *
60
+ * **Stable service handles** (set once at construction, never reallocated):
61
+ * - `commandRegistry`, `commandContext` — wired via setCommandRegistry() after
62
+ * construction; synced at feed() entry (not per-action) since no action changes them
63
+ * - `autocomplete` — wired after construction; synced at feed() entry
64
+ * - `inputHistory`, `conversationManager` — late-wired service handles; synced at
65
+ * feed() entry only since no in-feed action rewires them
66
+ * - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
67
+ * - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
68
+ * `profilePickerModal` — modal objects constructed once in InputHandler constructor
69
+ * - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`, `agentDetailModal`,
70
+ * `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
71
+ * service objects constructed once
72
+ * - `panelManager`, `keybindingsManager` — from uiServices, stable for app lifetime
73
+ * - `modalStack` — reference to the handler's shared array (mutated in place)
74
+ * - `getHistory`, `getViewportHeight`, `getScrollTop`, `scroll`, `exitApp` — stable
75
+ * callbacks bound in the InputHandler constructor
76
+ * - All method closures (`modalOpened`, `handleEscape`, etc.) — bound once at init
77
+ *
78
+ * **Rationale:** per-feed mutation avoids per-keystroke allocation cost; stable
79
+ * references are service handles whose identity never changes after construction.
80
+ */
43
81
  export interface InputFeedContext {
44
82
  prompt: string;
45
83
  cursorPos: number;
@@ -65,9 +103,9 @@ export interface InputFeedContext {
65
103
  readonly sessionPickerModal: SessionPickerModal;
66
104
  readonly profilePickerModal: ProfilePickerModal;
67
105
  readonly historySearch: HistorySearch;
68
- readonly commandRegistry: CommandRegistry | null;
69
- readonly commandContext: CommandContext | undefined;
70
- readonly autocomplete: AutocompleteEngine | null;
106
+ commandRegistry: CommandRegistry | null;
107
+ commandContext: CommandContext | undefined;
108
+ autocomplete: AutocompleteEngine | null;
71
109
  readonly filePicker: FilePickerModal;
72
110
  readonly modelPicker: ModelPickerModal;
73
111
  readonly processModal: ProcessModal;
@@ -79,13 +117,13 @@ export interface InputFeedContext {
79
117
  readonly panelManager: PanelManager;
80
118
  readonly keybindingsManager: KeybindingsManager;
81
119
  readonly modalStack: string[];
82
- readonly inputHistory: InputHistory | null;
83
- readonly conversationManager: ConversationManager | null;
120
+ inputHistory: InputHistory | null;
121
+ conversationManager: ConversationManager | null;
84
122
  readonly getHistory: () => InfiniteBuffer;
85
123
  readonly getViewportHeight: () => number;
86
124
  readonly getScrollTop: () => number;
87
125
  readonly scroll: (delta: number) => void;
88
- readonly requestRender: () => void;
126
+ requestRender: () => void;
89
127
  readonly modalOpened: (name: string) => void;
90
128
  readonly handleEscape: () => void;
91
129
  readonly handleCopy: () => void;
@@ -52,144 +52,157 @@ export function handleGlobalShortcutToken(
52
52
  ): boolean {
53
53
  if (token.type !== 'key') return false;
54
54
 
55
- const kb = state.keybindingsManager;
56
- if (kb.matches('copy-selection', token)) {
57
- state.handleCopy();
55
+ // Fast-path: bare pageup/pagedown have no keybinding entry.
56
+ if (token.logicalName === 'pageup') {
57
+ state.scroll(-Math.max(1, viewportHeight - 2));
58
58
  return true;
59
59
  }
60
- if (kb.matches('clear-cancel', token)) {
61
- state.handleCtrlC();
60
+ if (token.logicalName === 'pagedown') {
61
+ state.scroll(Math.max(1, viewportHeight - 2));
62
62
  return true;
63
63
  }
64
+ // Bare escape is also not in the keybinding table.
64
65
  if (token.logicalName === 'escape' && !state.panelFocused) {
65
66
  state.handleEscape();
66
67
  return true;
67
68
  }
68
- if (kb.matches('screen-clear', token)) {
69
- state.commandContext?.clearScreen?.();
70
- return true;
71
- }
72
- if (kb.matches('panel-close-all', token)) {
73
- const pm = state.panelManager;
74
- for (const p of pm.getAllOpen()) pm.close(p.id);
75
- pm.hide();
76
- state.requestRender();
77
- return true;
78
- }
79
- if (kb.matches('panel-close', token)) {
80
- const pm = state.panelManager;
81
- const active = pm.getActivePanel();
82
- if (active) {
83
- pm.close(active.id);
69
+
70
+ // O(1) lookup via inverted map.
71
+ const kb = state.keybindingsManager;
72
+ const action = kb.lookup(token);
73
+
74
+ switch (action) {
75
+ case 'copy-selection':
76
+ state.handleCopy();
77
+ return true;
78
+
79
+ case 'clear-cancel':
80
+ state.handleCtrlC();
81
+ return true;
82
+
83
+ case 'screen-clear':
84
+ state.commandContext?.clearScreen?.();
85
+ return true;
86
+
87
+ case 'panel-close-all': {
88
+ const pm = state.panelManager;
89
+ for (const p of pm.getAllOpen()) pm.close(p.id);
90
+ pm.hide();
84
91
  state.requestRender();
92
+ return true;
85
93
  }
86
- return true;
87
- }
88
- if (kb.matches('panel-picker', token)) {
89
- state.commandContext?.openPanelPicker?.();
90
- state.requestRender();
91
- return true;
92
- }
93
- if (kb.matches('panel-tab-next', token)) {
94
- state.cyclePanelTab('next');
95
- return true;
96
- }
97
- if (kb.matches('panel-tab-prev', token)) {
98
- state.cyclePanelTab('prev');
99
- return true;
100
- }
101
- if (kb.matches('history-search', token)) {
102
- state.historySearch.open(state.prompt);
103
- state.requestRender();
104
- return true;
105
- }
106
- if (kb.matches('search', token)) {
107
- if (state.searchManager.active) state.searchManager.close();
108
- else state.searchManager.open();
109
- state.requestRender();
110
- return true;
111
- }
112
- if (kb.matches('block-copy', token) && !state.commandMode) {
113
- state.handleBlockCopy();
114
- return true;
115
- }
116
- if (kb.matches('bookmark', token) && !state.commandMode) {
117
- state.handleBookmark();
118
- return true;
119
- }
120
- if (kb.matches('block-save', token) && !state.commandMode) {
121
- state.handleBlockSave();
122
- return true;
123
- }
124
- if (kb.matches('delete-word', token)) {
125
- state.saveUndoState();
126
- let pos = state.cursorPos;
127
- while (pos > 0 && state.prompt[pos - 1] === ' ') pos--;
128
- while (pos > 0 && state.prompt[pos - 1] !== ' ') pos--;
129
- state.prompt = state.prompt.slice(0, pos) + state.prompt.slice(state.cursorPos);
130
- state.cursorPos = pos;
131
- state.ensureInputCursorVisible();
132
- return true;
133
- }
134
- if (kb.matches('apply-diff-line-start', token)) {
135
- if (!state.commandMode && state.handleDiffApply()) return true;
136
- const info = state.getWrappedPromptInfo(state.contentWidth);
137
- state.cursorPos = info.wrappedLines.length > 1 ? info.segments[info.cursorWrappedLine].rawStart : 0;
138
- state.ensureInputCursorVisible();
139
- return true;
140
- }
141
- if (kb.matches('next-error-line-end', token)) {
142
- if (state.prompt === '' && !state.commandMode) {
143
- const nextLine = state.conversationManager?.nextErrorLine(state.getScrollTop()) ?? -1;
144
- if (nextLine >= 0) {
145
- state.scroll(nextLine - state.getScrollTop());
94
+
95
+ case 'panel-close': {
96
+ const pm = state.panelManager;
97
+ const active = pm.getActivePanel();
98
+ if (active) {
99
+ pm.close(active.id);
146
100
  state.requestRender();
147
- return true;
148
101
  }
102
+ return true;
149
103
  }
150
- const info = state.getWrappedPromptInfo(state.contentWidth);
151
- state.cursorPos = info.wrappedLines.length > 1
152
- ? info.segments[info.cursorWrappedLine].rawStart + info.segments[info.cursorWrappedLine].length
153
- : state.prompt.length;
154
- state.ensureInputCursorVisible();
155
- return true;
156
- }
157
- if (kb.matches('kill-line', token)) {
158
- state.saveUndoState();
159
- state.prompt = state.prompt.slice(0, state.cursorPos);
160
- state.ensureInputCursorVisible();
161
- return true;
162
- }
163
- if (kb.matches('clear-prompt', token)) {
164
- state.saveUndoState();
165
- state.prompt = '';
166
- state.cursorPos = 0;
167
- if (state.commandMode) {
168
- state.commandMode = false;
169
- state.autocomplete?.reset();
104
+
105
+ case 'panel-picker':
106
+ state.commandContext?.openPanelPicker?.();
107
+ state.requestRender();
108
+ return true;
109
+
110
+ case 'panel-tab-next':
111
+ state.cyclePanelTab('next');
112
+ return true;
113
+
114
+ case 'panel-tab-prev':
115
+ state.cyclePanelTab('prev');
116
+ return true;
117
+
118
+ case 'history-search':
119
+ state.historySearch.open(state.prompt);
120
+ state.requestRender();
121
+ return true;
122
+
123
+ case 'search':
124
+ if (state.searchManager.active) state.searchManager.close();
125
+ else state.searchManager.open();
126
+ state.requestRender();
127
+ return true;
128
+
129
+ case 'block-copy':
130
+ if (!state.commandMode) { state.handleBlockCopy(); return true; }
131
+ return false;
132
+
133
+ case 'bookmark':
134
+ if (!state.commandMode) { state.handleBookmark(); return true; }
135
+ return false;
136
+
137
+ case 'block-save':
138
+ if (!state.commandMode) { state.handleBlockSave(); return true; }
139
+ return false;
140
+
141
+ case 'delete-word': {
142
+ state.saveUndoState();
143
+ let pos = state.cursorPos;
144
+ while (pos > 0 && state.prompt[pos - 1] === ' ') pos--;
145
+ while (pos > 0 && state.prompt[pos - 1] !== ' ') pos--;
146
+ state.prompt = state.prompt.slice(0, pos) + state.prompt.slice(state.cursorPos);
147
+ state.cursorPos = pos;
148
+ state.ensureInputCursorVisible();
149
+ return true;
150
+ }
151
+
152
+ case 'apply-diff-line-start': {
153
+ if (!state.commandMode && state.handleDiffApply()) return true;
154
+ const info = state.getWrappedPromptInfo(state.contentWidth);
155
+ state.cursorPos = info.wrappedLines.length > 1 ? info.segments[info.cursorWrappedLine].rawStart : 0;
156
+ state.ensureInputCursorVisible();
157
+ return true;
170
158
  }
171
- return true;
172
- }
173
- if (kb.matches('undo', token)) {
174
- state.handleUndo();
175
- return true;
176
- }
177
- if (kb.matches('redo', token)) {
178
- state.handleRedo();
179
- return true;
180
- }
181
- if (kb.matches('paste', token)) {
182
- state.handlePaste();
183
- return true;
184
- }
185
- if (token.logicalName === 'pageup') {
186
- state.scroll(-Math.max(1, viewportHeight - 2));
187
- return true;
188
- }
189
- if (token.logicalName === 'pagedown') {
190
- state.scroll(Math.max(1, viewportHeight - 2));
191
- return true;
192
- }
193
159
 
194
- return false;
160
+ case 'next-error-line-end': {
161
+ if (state.prompt === '' && !state.commandMode) {
162
+ const nextLine = state.conversationManager?.nextErrorLine(state.getScrollTop()) ?? -1;
163
+ if (nextLine >= 0) {
164
+ state.scroll(nextLine - state.getScrollTop());
165
+ state.requestRender();
166
+ return true;
167
+ }
168
+ }
169
+ const info = state.getWrappedPromptInfo(state.contentWidth);
170
+ state.cursorPos = info.wrappedLines.length > 1
171
+ ? info.segments[info.cursorWrappedLine].rawStart + info.segments[info.cursorWrappedLine].length
172
+ : state.prompt.length;
173
+ state.ensureInputCursorVisible();
174
+ return true;
175
+ }
176
+
177
+ case 'kill-line':
178
+ state.saveUndoState();
179
+ state.prompt = state.prompt.slice(0, state.cursorPos);
180
+ state.ensureInputCursorVisible();
181
+ return true;
182
+
183
+ case 'clear-prompt':
184
+ state.saveUndoState();
185
+ state.prompt = '';
186
+ state.cursorPos = 0;
187
+ if (state.commandMode) {
188
+ state.commandMode = false;
189
+ state.autocomplete?.reset();
190
+ }
191
+ return true;
192
+
193
+ case 'undo':
194
+ state.handleUndo();
195
+ return true;
196
+
197
+ case 'redo':
198
+ state.handleRedo();
199
+ return true;
200
+
201
+ case 'paste':
202
+ state.handlePaste();
203
+ return true;
204
+
205
+ default:
206
+ return false;
207
+ }
195
208
  }