@pellux/goodvibes-tui 0.18.20 → 0.18.23
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.
- package/CHANGELOG.md +120 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-rendering.ts +20 -6
- package/src/input/commands/session.ts +0 -1
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +121 -119
- package/src/input/keybindings.ts +30 -0
- package/src/panels/approval-panel.ts +54 -82
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/local-auth-panel.ts +76 -93
- package/src/panels/mcp-panel.ts +108 -155
- package/src/panels/ops-control-panel.ts +50 -85
- package/src/panels/panel-manager.ts +22 -2
- package/src/panels/plugins-panel.ts +36 -60
- package/src/panels/routes-panel.ts +89 -141
- package/src/panels/scrollable-list-panel.ts +45 -14
- package/src/panels/security-panel.ts +101 -137
- package/src/panels/services-panel.ts +58 -102
- package/src/panels/settings-sync-panel.ts +76 -122
- package/src/panels/subscription-panel.ts +63 -86
- package/src/panels/tasks-panel.ts +129 -179
- package/src/panels/watchers-panel.ts +88 -137
- package/src/renderer/buffer.ts +11 -0
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +37 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/version.ts +1 -1
|
@@ -52,144 +52,157 @@ export function handleGlobalShortcutToken(
|
|
|
52
52
|
): boolean {
|
|
53
53
|
if (token.type !== 'key') return false;
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
state.
|
|
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 (
|
|
61
|
-
state.
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/input/handler.ts
CHANGED
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
} from './handler-picker-routes.ts';
|
|
82
82
|
import { handleGlobalShortcutToken } from './handler-shortcuts.ts';
|
|
83
83
|
import { feedInputTokens } from './handler-feed.ts';
|
|
84
|
+
import { buildInitialFeedContext } from './feed-context-factory.ts';
|
|
84
85
|
import { handlePanelIntegrationAction as runPanelIntegrationAction } from './panel-integration-actions.ts';
|
|
85
86
|
import type { Panel } from '../panels/types.ts';
|
|
86
87
|
import type { UiRuntimeServices } from '../runtime/ui-services.ts';
|
|
@@ -112,6 +113,8 @@ export class InputHandler {
|
|
|
112
113
|
private pasteRegistry = new Map<string, string>();
|
|
113
114
|
private nextPasteId = 1;
|
|
114
115
|
private lastCtrlCTime = 0;
|
|
116
|
+
/** Long-lived feed context — reused across every feed() call to avoid per-keystroke allocation. */
|
|
117
|
+
private feedContext!: import('./handler-feed.ts').InputFeedContext;
|
|
115
118
|
private commandRegistry: CommandRegistry | null = null;
|
|
116
119
|
private commandContext: CommandContext | undefined = undefined;
|
|
117
120
|
public autocomplete: AutocompleteEngine | null = null;
|
|
@@ -208,32 +211,110 @@ export class InputHandler {
|
|
|
208
211
|
this.bookmarkModal = new BookmarkModal(uiServices.shell.bookmarkManager);
|
|
209
212
|
this.sessionPickerModal = new SessionPickerModal(uiServices.sessions.sessionManager);
|
|
210
213
|
this.profilePickerModal = new ProfilePickerModal(uiServices.shell.profileManager);
|
|
214
|
+
this.initFeedContext();
|
|
211
215
|
}
|
|
212
216
|
|
|
213
217
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
218
|
+
* initFeedContext — Build the long-lived InputFeedContext once via factory.
|
|
219
|
+
* See feed-context-factory.ts for full field documentation.
|
|
216
220
|
*/
|
|
217
|
-
|
|
218
|
-
this.
|
|
221
|
+
private initFeedContext(): void {
|
|
222
|
+
this.feedContext = buildInitialFeedContext(
|
|
223
|
+
{
|
|
224
|
+
prompt: this.prompt, cursorPos: this.cursorPos, commandMode: this.commandMode,
|
|
225
|
+
panelFocused: this.panelFocused, indicatorFocused: this.indicatorFocused,
|
|
226
|
+
helpOverlayActive: this.helpOverlayActive, helpScrollOffset: this.helpScrollOffset,
|
|
227
|
+
shortcutsOverlayActive: this.shortcutsOverlayActive, shortcutsScrollOffset: this.shortcutsScrollOffset,
|
|
228
|
+
nextPasteId: this.nextPasteId, nextImageId: this.nextImageId,
|
|
229
|
+
mouseDownRow: this.mouseDownRow, mouseDownCol: this.mouseDownCol,
|
|
230
|
+
contentWidth: this.contentWidth, selectionCallback: this.selectionCallback,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
selection: this.selection,
|
|
234
|
+
pasteRegistry: this.pasteRegistry,
|
|
235
|
+
imageRegistry: this.imageRegistry,
|
|
236
|
+
selectionModal: this.selectionModal,
|
|
237
|
+
bookmarkModal: this.bookmarkModal,
|
|
238
|
+
settingsModal: this.settingsModal,
|
|
239
|
+
sessionPickerModal: this.sessionPickerModal,
|
|
240
|
+
profilePickerModal: this.profilePickerModal,
|
|
241
|
+
historySearch: this.historySearch,
|
|
242
|
+
commandRegistry: this.commandRegistry,
|
|
243
|
+
commandContext: this.commandContext,
|
|
244
|
+
autocomplete: this.autocomplete,
|
|
245
|
+
filePicker: this.filePicker,
|
|
246
|
+
modelPicker: this.modelPicker,
|
|
247
|
+
processModal: this.processModal,
|
|
248
|
+
liveTailModal: this.liveTailModal,
|
|
249
|
+
agentDetailModal: this.agentDetailModal,
|
|
250
|
+
contextInspectorModal: this.contextInspectorModal,
|
|
251
|
+
blockActionsMenu: this.blockActionsMenu,
|
|
252
|
+
searchManager: this.searchManager,
|
|
253
|
+
modalStack: this.modalStack,
|
|
254
|
+
inputHistory: this.inputHistory,
|
|
255
|
+
conversationManager: this.conversationManager,
|
|
256
|
+
panelManager: this.uiServices.shell.panelManager,
|
|
257
|
+
keybindingsManager: this.uiServices.shell.keybindingsManager,
|
|
258
|
+
getHistory: this.getHistory,
|
|
259
|
+
getViewportHeight: this.getViewportHeight,
|
|
260
|
+
getScrollTop: this.getScrollTop,
|
|
261
|
+
scroll: this.scroll,
|
|
262
|
+
exitApp: this.exitApp,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
modalOpened: (name: string) => this.modalOpened(name),
|
|
266
|
+
handleEscape: () => { this.handleEscape(); this.syncFeedContextMutableFields(); },
|
|
267
|
+
handleCopy: () => this.handleCopy(),
|
|
268
|
+
handleCtrlC: () => { this.handleCtrlC(); this.syncFeedContextMutableFields(); },
|
|
269
|
+
handleBlockCopy: () => this.handleBlockCopy(),
|
|
270
|
+
handleBookmark: () => this.handleBookmark(),
|
|
271
|
+
handleBlockSave: () => this.handleBlockSave(),
|
|
272
|
+
handleDiffApply: () => this.handleDiffApply(),
|
|
273
|
+
handleUndo: () => { this.handleUndo(); this.syncFeedContextMutableFields(); },
|
|
274
|
+
handleRedo: () => { this.handleRedo(); this.syncFeedContextMutableFields(); },
|
|
275
|
+
handlePaste: () => { this.handlePaste(); this.syncFeedContextMutableFields(); },
|
|
276
|
+
saveUndoState: () => this.saveUndoState(),
|
|
277
|
+
ensureInputCursorVisible: (contentWidth?: number) => this.ensureInputCursorVisible(contentWidth),
|
|
278
|
+
registerPaste: (content: string) => this.registerPaste(content),
|
|
279
|
+
executeBlockAction: (id: string) => this.executeBlockAction(id),
|
|
280
|
+
cyclePanelTab: (direction: 'next' | 'prev') => this.cyclePanelTab(direction),
|
|
281
|
+
onPanelInputConsumed: (activePanel: Panel | null, key: string) => this.handlePanelIntegrationAction(activePanel, key),
|
|
282
|
+
getWrappedPromptInfo: (contentWidth: number) => this.getWrappedPromptInfo(contentWidth),
|
|
283
|
+
moveCursorVertical: (direction: -1 | 1) => this.moveCursorVertical(direction),
|
|
284
|
+
handlePathCompletion: () => this.handlePathCompletion(),
|
|
285
|
+
handleBlockToggle: () => this.handleBlockToggle(),
|
|
286
|
+
findMarkerAtPos: (pos: number) => this.findMarkerAtPos(pos),
|
|
287
|
+
cleanupMarkerRegistry: (text: string) => this.cleanupMarkerRegistry(text),
|
|
288
|
+
expandPrompt: (text: string) => this.expandPrompt(text),
|
|
289
|
+
},
|
|
290
|
+
);
|
|
219
291
|
}
|
|
220
292
|
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
293
|
+
/** Sync mutable handler fields back into feedContext after in-feed mutations. */
|
|
294
|
+
private syncFeedContextMutableFields(): void {
|
|
295
|
+
const ctx = this.feedContext;
|
|
296
|
+
ctx.prompt = this.prompt; ctx.cursorPos = this.cursorPos;
|
|
297
|
+
ctx.commandMode = this.commandMode; ctx.panelFocused = this.panelFocused;
|
|
298
|
+
ctx.indicatorFocused = this.indicatorFocused;
|
|
299
|
+
ctx.helpOverlayActive = this.helpOverlayActive; ctx.helpScrollOffset = this.helpScrollOffset;
|
|
300
|
+
ctx.shortcutsOverlayActive = this.shortcutsOverlayActive; ctx.shortcutsScrollOffset = this.shortcutsScrollOffset;
|
|
301
|
+
ctx.selectionCallback = this.selectionCallback;
|
|
302
|
+
ctx.nextPasteId = this.nextPasteId; ctx.nextImageId = this.nextImageId;
|
|
303
|
+
ctx.mouseDownRow = this.mouseDownRow; ctx.mouseDownCol = this.mouseDownCol;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Wire in the InputHistory instance. Optional; disables history navigation if unset. */
|
|
307
|
+
public setHistory(history: InputHistory): void { this.inputHistory = history; }
|
|
308
|
+
|
|
309
|
+
/** Wire in the slash command registry and context. Must be called before commands work. */
|
|
225
310
|
public setCommandRegistry(registry: CommandRegistry, context: CommandContext): void {
|
|
226
311
|
this.commandRegistry = registry;
|
|
227
312
|
this.commandContext = context;
|
|
228
313
|
this.autocomplete = new AutocompleteEngine(registry);
|
|
229
314
|
}
|
|
230
315
|
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
-
*/
|
|
234
|
-
public setConversationManager(cm: ConversationManager): void {
|
|
235
|
-
this.conversationManager = cm;
|
|
236
|
-
}
|
|
316
|
+
/** Wire in the conversation manager for block copy/apply/collapse. */
|
|
317
|
+
public setConversationManager(cm: ConversationManager): void { this.conversationManager = cm; }
|
|
237
318
|
|
|
238
319
|
/**
|
|
239
320
|
* openSelection - Open the generic selection modal with a callback.
|
|
@@ -464,6 +545,7 @@ export class InputHandler {
|
|
|
464
545
|
|
|
465
546
|
/**
|
|
466
547
|
* feed - Process raw stdin data through the tokenizer.
|
|
548
|
+
* Reuses the long-lived this.feedContext to avoid per-keystroke object allocation.
|
|
467
549
|
*/
|
|
468
550
|
public feed(data: string): void {
|
|
469
551
|
const immediateRequestRender = this.requestRender;
|
|
@@ -479,111 +561,31 @@ export class InputHandler {
|
|
|
479
561
|
|
|
480
562
|
this.requestRender = bufferedRequestRender;
|
|
481
563
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
context =
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
helpScrollOffset: this.helpScrollOffset,
|
|
508
|
-
shortcutsOverlayActive: this.shortcutsOverlayActive,
|
|
509
|
-
shortcutsScrollOffset: this.shortcutsScrollOffset,
|
|
510
|
-
nextPasteId: this.nextPasteId,
|
|
511
|
-
nextImageId: this.nextImageId,
|
|
512
|
-
mouseDownRow: this.mouseDownRow,
|
|
513
|
-
mouseDownCol: this.mouseDownCol,
|
|
514
|
-
contentWidth: this.contentWidth,
|
|
515
|
-
pasteRegistry: this.pasteRegistry,
|
|
516
|
-
imageRegistry: this.imageRegistry,
|
|
517
|
-
selection: this.selection,
|
|
518
|
-
selectionModal: this.selectionModal,
|
|
519
|
-
selectionCallback: this.selectionCallback,
|
|
520
|
-
bookmarkModal: this.bookmarkModal,
|
|
521
|
-
settingsModal: this.settingsModal,
|
|
522
|
-
sessionPickerModal: this.sessionPickerModal,
|
|
523
|
-
profilePickerModal: this.profilePickerModal,
|
|
524
|
-
historySearch: this.historySearch,
|
|
525
|
-
commandRegistry: this.commandRegistry,
|
|
526
|
-
commandContext: this.commandContext,
|
|
527
|
-
autocomplete: this.autocomplete,
|
|
528
|
-
filePicker: this.filePicker,
|
|
529
|
-
modelPicker: this.modelPicker,
|
|
530
|
-
processModal: this.processModal,
|
|
531
|
-
liveTailModal: this.liveTailModal,
|
|
532
|
-
agentDetailModal: this.agentDetailModal,
|
|
533
|
-
contextInspectorModal: this.contextInspectorModal,
|
|
534
|
-
blockActionsMenu: this.blockActionsMenu,
|
|
535
|
-
searchManager: this.searchManager,
|
|
536
|
-
modalStack: this.modalStack,
|
|
537
|
-
inputHistory: this.inputHistory,
|
|
538
|
-
conversationManager: this.conversationManager,
|
|
539
|
-
getHistory: this.getHistory,
|
|
540
|
-
getViewportHeight: this.getViewportHeight,
|
|
541
|
-
getScrollTop: this.getScrollTop,
|
|
542
|
-
scroll: this.scroll,
|
|
543
|
-
requestRender: bufferedRequestRender,
|
|
544
|
-
modalOpened: (name: string) => this.modalOpened(name),
|
|
545
|
-
handleEscape: () => {
|
|
546
|
-
this.handleEscape();
|
|
547
|
-
syncFeedContextFromHandler();
|
|
548
|
-
},
|
|
549
|
-
handleCopy: () => this.handleCopy(),
|
|
550
|
-
handleCtrlC: () => {
|
|
551
|
-
this.handleCtrlC();
|
|
552
|
-
syncFeedContextFromHandler();
|
|
553
|
-
},
|
|
554
|
-
handleBlockCopy: () => this.handleBlockCopy(),
|
|
555
|
-
handleBookmark: () => this.handleBookmark(),
|
|
556
|
-
handleBlockSave: () => this.handleBlockSave(),
|
|
557
|
-
handleDiffApply: () => this.handleDiffApply(),
|
|
558
|
-
handleUndo: () => {
|
|
559
|
-
this.handleUndo();
|
|
560
|
-
syncFeedContextFromHandler();
|
|
561
|
-
},
|
|
562
|
-
handleRedo: () => {
|
|
563
|
-
this.handleRedo();
|
|
564
|
-
syncFeedContextFromHandler();
|
|
565
|
-
},
|
|
566
|
-
handlePaste: () => {
|
|
567
|
-
this.handlePaste();
|
|
568
|
-
syncFeedContextFromHandler();
|
|
569
|
-
},
|
|
570
|
-
saveUndoState: () => this.saveUndoState(),
|
|
571
|
-
ensureInputCursorVisible: (contentWidth?: number) => this.ensureInputCursorVisible(contentWidth),
|
|
572
|
-
registerPaste: (content: string) => this.registerPaste(content),
|
|
573
|
-
executeBlockAction: (id: string) => this.executeBlockAction(id),
|
|
574
|
-
cyclePanelTab: (direction: 'next' | 'prev') => this.cyclePanelTab(direction),
|
|
575
|
-
onPanelInputConsumed: (activePanel: Panel | null, key: string) => this.handlePanelIntegrationAction(activePanel, key),
|
|
576
|
-
panelManager: this.uiServices.shell.panelManager,
|
|
577
|
-
keybindingsManager: this.uiServices.shell.keybindingsManager,
|
|
578
|
-
getWrappedPromptInfo: (contentWidth: number) => this.getWrappedPromptInfo(contentWidth),
|
|
579
|
-
moveCursorVertical: (direction: -1 | 1) => this.moveCursorVertical(direction),
|
|
580
|
-
handlePathCompletion: () => this.handlePathCompletion(),
|
|
581
|
-
handleBlockToggle: () => this.handleBlockToggle(),
|
|
582
|
-
findMarkerAtPos: (pos: number) => this.findMarkerAtPos(pos),
|
|
583
|
-
cleanupMarkerRegistry: (text: string) => this.cleanupMarkerRegistry(text),
|
|
584
|
-
expandPrompt: (text: string) => this.expandPrompt(text),
|
|
585
|
-
exitApp: this.exitApp,
|
|
586
|
-
};
|
|
564
|
+
const context = this.feedContext;
|
|
565
|
+
// Sync mutable scalars from handler into the reused context.
|
|
566
|
+
context.prompt = this.prompt;
|
|
567
|
+
context.cursorPos = this.cursorPos;
|
|
568
|
+
context.commandMode = this.commandMode;
|
|
569
|
+
context.panelFocused = this.panelFocused;
|
|
570
|
+
context.indicatorFocused = this.indicatorFocused;
|
|
571
|
+
context.helpOverlayActive = this.helpOverlayActive;
|
|
572
|
+
context.helpScrollOffset = this.helpScrollOffset;
|
|
573
|
+
context.shortcutsOverlayActive = this.shortcutsOverlayActive;
|
|
574
|
+
context.shortcutsScrollOffset = this.shortcutsScrollOffset;
|
|
575
|
+
context.selectionCallback = this.selectionCallback;
|
|
576
|
+
context.nextPasteId = this.nextPasteId;
|
|
577
|
+
context.nextImageId = this.nextImageId;
|
|
578
|
+
context.mouseDownRow = this.mouseDownRow;
|
|
579
|
+
context.mouseDownCol = this.mouseDownCol;
|
|
580
|
+
context.contentWidth = this.contentWidth;
|
|
581
|
+
// Sync semi-stable refs that may be wired after construction.
|
|
582
|
+
context.commandRegistry = this.commandRegistry;
|
|
583
|
+
context.commandContext = this.commandContext;
|
|
584
|
+
context.autocomplete = this.autocomplete;
|
|
585
|
+
context.inputHistory = this.inputHistory;
|
|
586
|
+
context.conversationManager = this.conversationManager;
|
|
587
|
+
// Swap requestRender to buffered version for this feed.
|
|
588
|
+
context.requestRender = bufferedRequestRender;
|
|
587
589
|
this.syncFeedSelectionCallback = (callback) => {
|
|
588
590
|
context.selectionCallback = callback;
|
|
589
591
|
};
|
package/src/input/keybindings.ts
CHANGED
|
@@ -138,11 +138,14 @@ function resolveKeybindingsPath(options?: KeybindingsManagerOptions): string {
|
|
|
138
138
|
export class KeybindingsManager {
|
|
139
139
|
private bindings: Record<KeyAction, KeyCombo[]>;
|
|
140
140
|
private configPath: string;
|
|
141
|
+
/** Inverted lookup map: composite key → KeyAction. Built in buildLookupMap(). */
|
|
142
|
+
private lookupMap = new Map<string, KeyAction>();
|
|
141
143
|
|
|
142
144
|
constructor(options: KeybindingsManagerOptions) {
|
|
143
145
|
this.configPath = resolveKeybindingsPath(options);
|
|
144
146
|
// Start with deep copy of defaults
|
|
145
147
|
this.bindings = this.cloneDefaults();
|
|
148
|
+
this.buildLookupMap();
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
private cloneDefaults(): Record<KeyAction, KeyCombo[]> {
|
|
@@ -184,6 +187,33 @@ export class KeybindingsManager {
|
|
|
184
187
|
} catch (err) {
|
|
185
188
|
logger.debug('keybindings: failed to load config file', { path: this.configPath, err: summarizeError(err) });
|
|
186
189
|
}
|
|
190
|
+
this.buildLookupMap();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* buildLookupMap — Rebuild the inverted lookup map from the current bindings table.
|
|
195
|
+
* Called after constructor init and after loadFromDisk().
|
|
196
|
+
* Map key format: "logicalName:ctrl:shift:alt" (booleans as 0/1).
|
|
197
|
+
* Last writer wins for duplicate combos (deterministic: iterate actions in order).
|
|
198
|
+
*/
|
|
199
|
+
private buildLookupMap(): void {
|
|
200
|
+
this.lookupMap.clear();
|
|
201
|
+
for (const [action, combos] of Object.entries(this.bindings) as [KeyAction, KeyCombo[]][]) {
|
|
202
|
+
for (const combo of combos) {
|
|
203
|
+
const key = `${combo.key}:${combo.ctrl ? 1 : 0}:${combo.shift ? 1 : 0}:${combo.alt ? 1 : 0}`;
|
|
204
|
+
this.lookupMap.set(key, action);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* lookup — O(1) keybinding lookup by token.
|
|
211
|
+
* Returns the matching KeyAction, or null if no binding matches.
|
|
212
|
+
*/
|
|
213
|
+
lookup(token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean }): KeyAction | null {
|
|
214
|
+
if (!token.logicalName) return null;
|
|
215
|
+
const key = `${token.logicalName}:${token.ctrl ? 1 : 0}:${token.shift ? 1 : 0}:${token.alt ? 1 : 0}`;
|
|
216
|
+
return this.lookupMap.get(key) ?? null;
|
|
187
217
|
}
|
|
188
218
|
|
|
189
219
|
private validateCombos(combos: unknown[]): combos is KeyCombo[] {
|