wave-code 0.8.0 → 0.8.2

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 (46) hide show
  1. package/dist/components/ChatInterface.js +1 -1
  2. package/dist/components/CommandSelector.d.ts.map +1 -1
  3. package/dist/components/CommandSelector.js +1 -38
  4. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  5. package/dist/components/ConfirmationSelector.js +11 -3
  6. package/dist/components/HelpView.d.ts +2 -0
  7. package/dist/components/HelpView.d.ts.map +1 -1
  8. package/dist/components/HelpView.js +49 -5
  9. package/dist/components/InputBox.d.ts.map +1 -1
  10. package/dist/components/InputBox.js +1 -1
  11. package/dist/components/MessageList.d.ts +2 -2
  12. package/dist/components/MessageList.d.ts.map +1 -1
  13. package/dist/components/MessageList.js +5 -6
  14. package/dist/constants/commands.d.ts +3 -0
  15. package/dist/constants/commands.d.ts.map +1 -0
  16. package/dist/constants/commands.js +38 -0
  17. package/dist/contexts/useChat.d.ts.map +1 -1
  18. package/dist/contexts/useChat.js +2 -17
  19. package/dist/hooks/useInputManager.d.ts +7 -8
  20. package/dist/hooks/useInputManager.d.ts.map +1 -1
  21. package/dist/hooks/useInputManager.js +224 -232
  22. package/dist/managers/inputHandlers.d.ts +28 -0
  23. package/dist/managers/inputHandlers.d.ts.map +1 -0
  24. package/dist/managers/inputHandlers.js +378 -0
  25. package/dist/managers/inputReducer.d.ts +157 -0
  26. package/dist/managers/inputReducer.d.ts.map +1 -0
  27. package/dist/managers/inputReducer.js +242 -0
  28. package/dist/utils/highlightUtils.d.ts.map +1 -1
  29. package/dist/utils/highlightUtils.js +66 -42
  30. package/package.json +2 -2
  31. package/src/components/ChatInterface.tsx +1 -1
  32. package/src/components/CommandSelector.tsx +1 -40
  33. package/src/components/ConfirmationSelector.tsx +13 -3
  34. package/src/components/HelpView.tsx +129 -16
  35. package/src/components/InputBox.tsx +3 -1
  36. package/src/components/MessageList.tsx +5 -6
  37. package/src/constants/commands.ts +41 -0
  38. package/src/contexts/useChat.tsx +2 -17
  39. package/src/hooks/useInputManager.ts +352 -299
  40. package/src/managers/inputHandlers.ts +560 -0
  41. package/src/managers/inputReducer.ts +367 -0
  42. package/src/utils/highlightUtils.ts +66 -42
  43. package/dist/managers/InputManager.d.ts +0 -156
  44. package/dist/managers/InputManager.d.ts.map +0 -1
  45. package/dist/managers/InputManager.js +0 -749
  46. package/src/managers/InputManager.ts +0 -1024
@@ -1,749 +0,0 @@
1
- import { searchFiles as searchFilesUtil, PromptHistoryManager, } from "wave-agent-sdk";
2
- import { readClipboardImage } from "../utils/clipboard.js";
3
- export class InputManager {
4
- constructor(callbacks = {}) {
5
- // Core input state
6
- this.inputText = "";
7
- this.cursorPosition = 0;
8
- // File selector state
9
- this.showFileSelector = false;
10
- this.atPosition = -1;
11
- this.fileSearchQuery = "";
12
- this.filteredFiles = [];
13
- this.fileSearchDebounceTimer = null;
14
- // Command selector state
15
- this.showCommandSelector = false;
16
- this.slashPosition = -1;
17
- this.commandSearchQuery = "";
18
- // History search state
19
- this.showHistorySearch = false;
20
- this.historySearchQuery = "";
21
- // Paste debounce state
22
- this.pasteDebounceTimer = null;
23
- this.pasteBuffer = "";
24
- this.initialPasteCursorPosition = 0;
25
- this.isPasting = false;
26
- // Long text compression state
27
- this.longTextCounter = 0;
28
- this.longTextMap = new Map();
29
- // Image management state
30
- this.attachedImages = [];
31
- this.imageIdCounter = 1;
32
- // Additional UI state
33
- this.showBackgroundTaskManager = false;
34
- this.showMcpManager = false;
35
- this.showRewindManager = false;
36
- this.showHelp = false;
37
- this.showStatusCommand = false;
38
- // Permission mode state
39
- this.permissionMode = "default";
40
- // Flag to prevent handleInput conflicts when selector selection occurs
41
- this.selectorJustUsed = false;
42
- this.callbacks = callbacks;
43
- this.logger = callbacks.logger;
44
- }
45
- // Update callbacks
46
- updateCallbacks(callbacks) {
47
- this.callbacks = { ...this.callbacks, ...callbacks };
48
- if (callbacks.logger) {
49
- this.logger = callbacks.logger;
50
- }
51
- }
52
- // Core input methods
53
- getInputText() {
54
- return this.inputText;
55
- }
56
- setInputText(text) {
57
- this.inputText = text;
58
- this.callbacks.onInputTextChange?.(text);
59
- }
60
- getCursorPosition() {
61
- return this.cursorPosition;
62
- }
63
- setCursorPosition(position) {
64
- this.cursorPosition = Math.max(0, Math.min(this.inputText.length, position));
65
- this.callbacks.onCursorPositionChange?.(this.cursorPosition);
66
- }
67
- insertTextAtCursor(text, callback) {
68
- const beforeCursor = this.inputText.substring(0, this.cursorPosition);
69
- const afterCursor = this.inputText.substring(this.cursorPosition);
70
- const newText = beforeCursor + text + afterCursor;
71
- const newCursorPosition = this.cursorPosition + text.length;
72
- this.inputText = newText;
73
- this.cursorPosition = newCursorPosition;
74
- this.callbacks.onInputTextChange?.(newText);
75
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
76
- callback?.(newText, newCursorPosition);
77
- }
78
- deleteCharAtCursor(callback) {
79
- if (this.cursorPosition > 0) {
80
- const beforeCursor = this.inputText.substring(0, this.cursorPosition - 1);
81
- const afterCursor = this.inputText.substring(this.cursorPosition);
82
- const newText = beforeCursor + afterCursor;
83
- const newCursorPosition = this.cursorPosition - 1;
84
- this.inputText = newText;
85
- this.cursorPosition = newCursorPosition;
86
- this.callbacks.onInputTextChange?.(newText);
87
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
88
- callback?.(newText, newCursorPosition);
89
- }
90
- }
91
- clearInput() {
92
- this.inputText = "";
93
- this.cursorPosition = 0;
94
- this.callbacks.onInputTextChange?.("");
95
- this.callbacks.onCursorPositionChange?.(0);
96
- }
97
- moveCursorLeft() {
98
- this.setCursorPosition(this.cursorPosition - 1);
99
- }
100
- moveCursorRight() {
101
- this.setCursorPosition(this.cursorPosition + 1);
102
- }
103
- // File selector methods
104
- async searchFiles(query) {
105
- try {
106
- const fileItems = await searchFilesUtil(query);
107
- this.filteredFiles = fileItems;
108
- this.callbacks.onFileSelectorStateChange?.(this.showFileSelector, this.filteredFiles, this.fileSearchQuery, this.atPosition);
109
- }
110
- catch (error) {
111
- console.error("File search error:", error);
112
- this.filteredFiles = [];
113
- this.callbacks.onFileSelectorStateChange?.(this.showFileSelector, [], this.fileSearchQuery, this.atPosition);
114
- }
115
- }
116
- debouncedSearchFiles(query) {
117
- if (this.fileSearchDebounceTimer) {
118
- clearTimeout(this.fileSearchDebounceTimer);
119
- }
120
- const debounceDelay = parseInt(process.env.FILE_SELECTOR_DEBOUNCE_MS || "300", 10);
121
- this.fileSearchDebounceTimer = setTimeout(() => {
122
- this.searchFiles(query);
123
- }, debounceDelay);
124
- }
125
- activateFileSelector(position) {
126
- this.showFileSelector = true;
127
- this.atPosition = position;
128
- this.fileSearchQuery = "";
129
- this.filteredFiles = [];
130
- // Immediately trigger search to display initial file list
131
- this.searchFiles("");
132
- this.callbacks.onFileSelectorStateChange?.(true, this.filteredFiles, "", position);
133
- }
134
- updateFileSearchQuery(query) {
135
- this.fileSearchQuery = query;
136
- this.debouncedSearchFiles(query);
137
- }
138
- handleFileSelect(filePath) {
139
- if (this.atPosition >= 0) {
140
- const beforeAt = this.inputText.substring(0, this.atPosition);
141
- const afterQuery = this.inputText.substring(this.cursorPosition);
142
- const newInput = beforeAt + `${filePath} ` + afterQuery;
143
- const newCursorPosition = beforeAt.length + filePath.length + 1;
144
- this.inputText = newInput;
145
- this.cursorPosition = newCursorPosition;
146
- this.callbacks.onInputTextChange?.(newInput);
147
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
148
- // Cancel file selector AFTER updating the input
149
- this.handleCancelFileSelect();
150
- // Set flag to prevent handleInput from processing the same Enter key
151
- this.selectorJustUsed = true;
152
- // Reset flag after a short delay
153
- setTimeout(() => {
154
- this.selectorJustUsed = false;
155
- }, 0);
156
- return { newInput, newCursorPosition };
157
- }
158
- return { newInput: this.inputText, newCursorPosition: this.cursorPosition };
159
- }
160
- handleCancelFileSelect() {
161
- this.showFileSelector = false;
162
- this.atPosition = -1;
163
- this.fileSearchQuery = "";
164
- this.filteredFiles = [];
165
- this.callbacks.onFileSelectorStateChange?.(false, [], "", -1);
166
- }
167
- checkForAtDeletion(cursorPosition) {
168
- if (this.showFileSelector && cursorPosition <= this.atPosition) {
169
- this.handleCancelFileSelect();
170
- return true;
171
- }
172
- return false;
173
- }
174
- // Command selector methods
175
- activateCommandSelector(position) {
176
- this.showCommandSelector = true;
177
- this.slashPosition = position;
178
- this.commandSearchQuery = "";
179
- this.callbacks.onCommandSelectorStateChange?.(true, "", position);
180
- }
181
- updateCommandSearchQuery(query) {
182
- this.commandSearchQuery = query;
183
- this.callbacks.onCommandSelectorStateChange?.(this.showCommandSelector, query, this.slashPosition);
184
- }
185
- handleCommandSelect(command) {
186
- if (this.slashPosition >= 0) {
187
- // Replace command part, keep other content
188
- const beforeSlash = this.inputText.substring(0, this.slashPosition);
189
- const afterQuery = this.inputText.substring(this.cursorPosition);
190
- const newInput = beforeSlash + afterQuery;
191
- const newCursorPosition = beforeSlash.length;
192
- this.inputText = newInput;
193
- this.cursorPosition = newCursorPosition;
194
- // Execute command asynchronously
195
- (async () => {
196
- // First check if it's an agent command
197
- let commandExecuted = false;
198
- if (this.callbacks.onSendMessage &&
199
- this.callbacks.onHasSlashCommand?.(command)) {
200
- // Execute complete command (replace partial input with complete command name)
201
- const fullCommand = `/${command}`;
202
- try {
203
- await this.callbacks.onSendMessage(fullCommand);
204
- commandExecuted = true;
205
- }
206
- catch (error) {
207
- console.error("Failed to execute slash command:", error);
208
- }
209
- }
210
- // If not an agent command or execution failed, check local commands
211
- if (!commandExecuted) {
212
- if (command === "clear") {
213
- this.callbacks.onClearMessages?.();
214
- commandExecuted = true;
215
- }
216
- else if (command === "tasks") {
217
- this.setShowBackgroundTaskManager(true);
218
- commandExecuted = true;
219
- }
220
- else if (command === "mcp") {
221
- this.setShowMcpManager(true);
222
- commandExecuted = true;
223
- }
224
- else if (command === "rewind") {
225
- this.setShowRewindManager(true);
226
- commandExecuted = true;
227
- }
228
- else if (command === "help") {
229
- this.setShowHelp(true);
230
- commandExecuted = true;
231
- }
232
- else if (command === "status") {
233
- this.setShowStatusCommand(true);
234
- commandExecuted = true;
235
- }
236
- }
237
- })();
238
- this.handleCancelCommandSelect();
239
- // Set flag to prevent handleInput from processing the same Enter key
240
- this.selectorJustUsed = true;
241
- setTimeout(() => {
242
- this.selectorJustUsed = false;
243
- }, 0);
244
- this.callbacks.onInputTextChange?.(newInput);
245
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
246
- return { newInput, newCursorPosition };
247
- }
248
- return { newInput: this.inputText, newCursorPosition: this.cursorPosition };
249
- }
250
- handleCommandInsert(command) {
251
- if (this.slashPosition >= 0) {
252
- const beforeSlash = this.inputText.substring(0, this.slashPosition);
253
- const afterQuery = this.inputText.substring(this.cursorPosition);
254
- const newInput = beforeSlash + `/${command} ` + afterQuery;
255
- const newCursorPosition = beforeSlash.length + command.length + 2;
256
- this.inputText = newInput;
257
- this.cursorPosition = newCursorPosition;
258
- this.handleCancelCommandSelect();
259
- // Set flag to prevent handleInput from processing the same Enter key
260
- this.selectorJustUsed = true;
261
- setTimeout(() => {
262
- this.selectorJustUsed = false;
263
- }, 0);
264
- this.callbacks.onInputTextChange?.(newInput);
265
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
266
- return { newInput, newCursorPosition };
267
- }
268
- return { newInput: this.inputText, newCursorPosition: this.cursorPosition };
269
- }
270
- handleCancelCommandSelect() {
271
- this.showCommandSelector = false;
272
- this.slashPosition = -1;
273
- this.commandSearchQuery = "";
274
- this.callbacks.onCommandSelectorStateChange?.(false, "", -1);
275
- }
276
- checkForSlashDeletion(cursorPosition) {
277
- if (this.showCommandSelector && cursorPosition <= this.slashPosition) {
278
- this.handleCancelCommandSelect();
279
- return true;
280
- }
281
- return false;
282
- }
283
- // Getter methods for state
284
- isFileSelectorActive() {
285
- return this.showFileSelector;
286
- }
287
- isCommandSelectorActive() {
288
- return this.showCommandSelector;
289
- }
290
- getFileSelectorState() {
291
- return {
292
- show: this.showFileSelector,
293
- files: this.filteredFiles,
294
- query: this.fileSearchQuery,
295
- position: this.atPosition,
296
- };
297
- }
298
- getCommandSelectorState() {
299
- return {
300
- show: this.showCommandSelector,
301
- query: this.commandSearchQuery,
302
- position: this.slashPosition,
303
- };
304
- }
305
- // Update search queries for active selectors
306
- updateSearchQueriesForActiveSelectors(inputText, cursorPosition) {
307
- if (this.showFileSelector && this.atPosition >= 0) {
308
- const queryStart = this.atPosition + 1;
309
- const queryEnd = cursorPosition;
310
- const newQuery = inputText.substring(queryStart, queryEnd);
311
- this.updateFileSearchQuery(newQuery);
312
- }
313
- else if (this.showCommandSelector && this.slashPosition >= 0) {
314
- const queryStart = this.slashPosition + 1;
315
- const queryEnd = cursorPosition;
316
- const newQuery = inputText.substring(queryStart, queryEnd);
317
- this.updateCommandSearchQuery(newQuery);
318
- }
319
- }
320
- // Handle special character input that might trigger selectors
321
- handleSpecialCharInput(char) {
322
- if (char === "@") {
323
- this.activateFileSelector(this.cursorPosition - 1);
324
- }
325
- else if (char === "/" &&
326
- !this.showFileSelector &&
327
- this.cursorPosition === 1) {
328
- // Don't activate command selector when file selector is active
329
- // Only activate command selector if '/' is at the start of input
330
- this.activateCommandSelector(this.cursorPosition - 1);
331
- }
332
- else {
333
- // Update search queries for active selectors
334
- this.updateSearchQueriesForActiveSelectors(this.inputText, this.cursorPosition);
335
- }
336
- }
337
- // Long text compression methods
338
- generateCompressedText(originalText) {
339
- this.longTextCounter += 1;
340
- const compressedLabel = `[LongText#${this.longTextCounter}]`;
341
- this.longTextMap.set(compressedLabel, originalText);
342
- return compressedLabel;
343
- }
344
- expandLongTextPlaceholders(text) {
345
- let expandedText = text;
346
- const longTextRegex = /\[LongText#(\d+)\]/g;
347
- const matches = [...text.matchAll(longTextRegex)];
348
- for (const match of matches) {
349
- const placeholder = match[0];
350
- const originalText = this.longTextMap.get(placeholder);
351
- if (originalText) {
352
- expandedText = expandedText.replace(placeholder, originalText);
353
- }
354
- }
355
- return expandedText;
356
- }
357
- clearLongTextMap() {
358
- this.longTextMap.clear();
359
- }
360
- // Paste handling methods
361
- handlePasteInput(input) {
362
- const inputString = input;
363
- // Detect if it's a paste operation (input contains multiple characters or newlines)
364
- const isPasteOperation = inputString.length > 1 ||
365
- inputString.includes("\n") ||
366
- inputString.includes("\r");
367
- if (isPasteOperation) {
368
- // Start or continue the debounce handling for paste operation
369
- if (!this.isPasting) {
370
- // Start new paste operation
371
- this.isPasting = true;
372
- this.pasteBuffer = inputString;
373
- this.initialPasteCursorPosition = this.cursorPosition;
374
- }
375
- else {
376
- // Continue paste operation, add new input to buffer
377
- this.pasteBuffer += inputString;
378
- }
379
- // Clear previous timer
380
- if (this.pasteDebounceTimer) {
381
- clearTimeout(this.pasteDebounceTimer);
382
- }
383
- // Set new timer, support environment variable configuration
384
- const pasteDebounceDelay = parseInt(process.env.PASTE_DEBOUNCE_MS || "30", 10);
385
- this.pasteDebounceTimer = setTimeout(() => {
386
- // Process all paste content in buffer
387
- let processedInput = this.pasteBuffer.replace(/\r/g, "\n");
388
- // Check if long text compression is needed (over 200 characters)
389
- if (processedInput.length > 200) {
390
- const originalText = processedInput;
391
- const compressedLabel = this.generateCompressedText(originalText);
392
- processedInput = compressedLabel;
393
- }
394
- this.insertTextAtCursor(processedInput);
395
- this.callbacks.onResetHistoryNavigation?.();
396
- // Reset paste state
397
- this.isPasting = false;
398
- this.pasteBuffer = "";
399
- this.pasteDebounceTimer = null;
400
- }, pasteDebounceDelay);
401
- }
402
- else {
403
- // Handle single character input
404
- let char = inputString;
405
- // Check if it's Chinese exclamation mark, convert to English if at beginning
406
- if (char === "!" && this.cursorPosition === 0) {
407
- char = "!";
408
- }
409
- this.callbacks.onResetHistoryNavigation?.();
410
- this.insertTextAtCursor(char, () => {
411
- // Handle special character input - this will manage all selectors
412
- this.handleSpecialCharInput(char);
413
- });
414
- }
415
- }
416
- // Image management methods
417
- addImage(imagePath, mimeType) {
418
- const newImage = {
419
- id: this.imageIdCounter,
420
- path: imagePath,
421
- mimeType,
422
- };
423
- this.attachedImages = [...this.attachedImages, newImage];
424
- this.imageIdCounter++;
425
- this.callbacks.onImagesStateChange?.(this.attachedImages);
426
- return newImage;
427
- }
428
- removeImage(imageId) {
429
- this.attachedImages = this.attachedImages.filter((img) => img.id !== imageId);
430
- this.callbacks.onImagesStateChange?.(this.attachedImages);
431
- }
432
- clearImages() {
433
- this.attachedImages = [];
434
- this.callbacks.onImagesStateChange?.(this.attachedImages);
435
- }
436
- getAttachedImages() {
437
- return this.attachedImages;
438
- }
439
- async handlePasteImage() {
440
- try {
441
- const result = await readClipboardImage();
442
- if (result.success && result.imagePath && result.mimeType) {
443
- // Add image to manager
444
- const attachedImage = this.addImage(result.imagePath, result.mimeType);
445
- // Insert image placeholder at cursor position
446
- this.insertTextAtCursor(`[Image #${attachedImage.id}]`);
447
- return true;
448
- }
449
- return false;
450
- }
451
- catch (error) {
452
- console.warn("Failed to paste image from clipboard:", error);
453
- return false;
454
- }
455
- }
456
- // Task manager state methods
457
- getShowBackgroundTaskManager() {
458
- return this.showBackgroundTaskManager;
459
- }
460
- setShowBackgroundTaskManager(show) {
461
- this.showBackgroundTaskManager = show;
462
- this.callbacks.onBackgroundTaskManagerStateChange?.(show);
463
- }
464
- getShowMcpManager() {
465
- return this.showMcpManager;
466
- }
467
- setShowMcpManager(show) {
468
- this.showMcpManager = show;
469
- this.callbacks.onMcpManagerStateChange?.(show);
470
- }
471
- getShowRewindManager() {
472
- return this.showRewindManager;
473
- }
474
- setShowRewindManager(show) {
475
- this.showRewindManager = show;
476
- this.callbacks.onRewindManagerStateChange?.(show);
477
- }
478
- getShowHelp() {
479
- return this.showHelp;
480
- }
481
- setShowHelp(show) {
482
- this.showHelp = show;
483
- this.callbacks.onHelpStateChange?.(show);
484
- }
485
- getShowStatusCommand() {
486
- return this.showStatusCommand;
487
- }
488
- setShowStatusCommand(show) {
489
- this.showStatusCommand = show;
490
- this.callbacks.onStatusCommandStateChange?.(show);
491
- }
492
- // Permission mode methods
493
- getPermissionMode() {
494
- return this.permissionMode;
495
- }
496
- isAnySelectorOrManagerActive() {
497
- return (this.showFileSelector ||
498
- this.showCommandSelector ||
499
- this.showHistorySearch ||
500
- this.showBackgroundTaskManager ||
501
- this.showMcpManager ||
502
- this.showRewindManager ||
503
- this.showHelp ||
504
- this.showStatusCommand);
505
- }
506
- setPermissionMode(mode) {
507
- this.permissionMode = mode;
508
- }
509
- cyclePermissionMode() {
510
- const modes = ["default", "acceptEdits", "plan"];
511
- const currentIndex = modes.indexOf(this.permissionMode);
512
- const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % modes.length;
513
- const nextMode = modes[nextIndex];
514
- this.logger?.debug("Cycling permission mode", {
515
- from: this.permissionMode,
516
- to: nextMode,
517
- });
518
- this.permissionMode = nextMode;
519
- this.callbacks.onPermissionModeChange?.(this.permissionMode);
520
- }
521
- // Handle submit logic
522
- async handleSubmit(attachedImages, isLoading = false, isCommandRunning = false) {
523
- // Prevent submission during loading or command execution
524
- if (isLoading || isCommandRunning) {
525
- return;
526
- }
527
- if (this.inputText.trim()) {
528
- // Extract image information
529
- const imageRegex = /\[Image #(\d+)\]/g;
530
- const matches = [...this.inputText.matchAll(imageRegex)];
531
- const referencedImages = matches
532
- .map((match) => {
533
- const imageId = parseInt(match[1], 10);
534
- return attachedImages.find((img) => img.id === imageId);
535
- })
536
- .filter((img) => img !== undefined)
537
- .map((img) => ({ path: img.path, mimeType: img.mimeType }));
538
- // Remove image placeholders, expand long text placeholders, send message
539
- let cleanContent = this.inputText.replace(imageRegex, "").trim();
540
- cleanContent = this.expandLongTextPlaceholders(cleanContent);
541
- // Save to prompt history
542
- PromptHistoryManager.addEntry(cleanContent).catch((err) => {
543
- this.logger?.error("Failed to save prompt history", err);
544
- });
545
- this.callbacks.onSendMessage?.(cleanContent, referencedImages.length > 0 ? referencedImages : undefined);
546
- this.clearInput();
547
- this.callbacks.onResetHistoryNavigation?.();
548
- // Clear long text mapping
549
- this.clearLongTextMap();
550
- }
551
- }
552
- // Handle selector input (when any selector is active)
553
- handleSelectorInput(input, key) {
554
- if (key.backspace || key.delete) {
555
- if (this.cursorPosition > 0) {
556
- this.deleteCharAtCursor((newInput, newCursorPosition) => {
557
- // Check for special character deletion
558
- this.checkForAtDeletion(newCursorPosition);
559
- this.checkForSlashDeletion(newCursorPosition);
560
- // Update search queries using the same logic as character input
561
- this.updateSearchQueriesForActiveSelectors(newInput, newCursorPosition);
562
- });
563
- }
564
- return true;
565
- }
566
- // Arrow keys, Enter and Tab should be handled by selector components
567
- if (key.upArrow || key.downArrow || key.return || key.tab) {
568
- // Let selector component handle these keys, but prevent further processing
569
- // by returning true (indicating we've handled the input)
570
- return true;
571
- }
572
- if (input &&
573
- !key.ctrl &&
574
- !("alt" in key && key.alt) &&
575
- !key.meta &&
576
- !key.return &&
577
- !key.tab &&
578
- !key.escape &&
579
- !key.leftArrow &&
580
- !key.rightArrow &&
581
- !("home" in key && key.home) &&
582
- !("end" in key && key.end)) {
583
- // Handle character input for search
584
- this.insertTextAtCursor(input, () => {
585
- // Special character handling is now managed by InputManager
586
- this.handleSpecialCharInput(input);
587
- });
588
- return true;
589
- }
590
- return false;
591
- }
592
- // History search methods
593
- activateHistorySearch() {
594
- this.showHistorySearch = true;
595
- this.historySearchQuery = "";
596
- this.callbacks.onHistorySearchStateChange?.(true, "");
597
- }
598
- updateHistorySearchQuery(query) {
599
- this.historySearchQuery = query;
600
- this.callbacks.onHistorySearchStateChange?.(true, query);
601
- }
602
- handleHistorySearchSelect(prompt) {
603
- this.inputText = prompt;
604
- this.cursorPosition = prompt.length;
605
- this.callbacks.onInputTextChange?.(prompt);
606
- this.callbacks.onCursorPositionChange?.(prompt.length);
607
- this.handleCancelHistorySearch();
608
- }
609
- handleCancelHistorySearch() {
610
- this.showHistorySearch = false;
611
- this.historySearchQuery = "";
612
- this.callbacks.onHistorySearchStateChange?.(false, "");
613
- }
614
- // Handle normal input (when no selector is active)
615
- async handleNormalInput(input, key, attachedImages, isLoading = false, isCommandRunning = false, clearImages) {
616
- if (key.return) {
617
- await this.handleSubmit(attachedImages, isLoading, isCommandRunning);
618
- clearImages?.();
619
- return true;
620
- }
621
- if (key.escape) {
622
- if (this.showFileSelector) {
623
- this.handleCancelFileSelect();
624
- }
625
- else if (this.showCommandSelector) {
626
- this.handleCancelCommandSelect();
627
- }
628
- return true;
629
- }
630
- if (key.backspace || key.delete) {
631
- if (this.cursorPosition > 0) {
632
- this.deleteCharAtCursor();
633
- this.callbacks.onResetHistoryNavigation?.();
634
- // Check if we deleted any special characters
635
- const newCursorPosition = this.cursorPosition - 1;
636
- this.checkForAtDeletion(newCursorPosition);
637
- this.checkForSlashDeletion(newCursorPosition);
638
- }
639
- return true;
640
- }
641
- if (key.leftArrow) {
642
- this.moveCursorLeft();
643
- return true;
644
- }
645
- if (key.rightArrow) {
646
- this.moveCursorRight();
647
- return true;
648
- }
649
- // Handle Ctrl+V for pasting images
650
- if (key.ctrl && input === "v") {
651
- this.handlePasteImage().catch((error) => {
652
- console.warn("Failed to handle paste image:", error);
653
- });
654
- return true;
655
- }
656
- // Handle Ctrl+R for history search
657
- if (key.ctrl && input === "r") {
658
- this.activateHistorySearch();
659
- return true;
660
- }
661
- // Handle Ctrl+B for backgrounding current task
662
- if (key.ctrl && input === "b") {
663
- this.callbacks.onBackgroundCurrentTask?.();
664
- return true;
665
- }
666
- // Handle typing input
667
- if (input &&
668
- !key.ctrl &&
669
- !("alt" in key && key.alt) &&
670
- !key.meta &&
671
- !key.return &&
672
- !key.escape &&
673
- !key.backspace &&
674
- !key.delete &&
675
- !key.leftArrow &&
676
- !key.rightArrow &&
677
- !("home" in key && key.home) &&
678
- !("end" in key && key.end)) {
679
- this.handlePasteInput(input);
680
- return true;
681
- }
682
- return false;
683
- }
684
- // Main input handler - routes to appropriate handler based on state
685
- async handleInput(input, key, attachedImages, isLoading = false, isCommandRunning = false, clearImages) {
686
- // If selector was just used, ignore this input to prevent conflicts
687
- if (this.selectorJustUsed) {
688
- return true;
689
- }
690
- // Handle interrupt request - use Esc key to interrupt AI request or command
691
- if (key.escape &&
692
- (isLoading || isCommandRunning) &&
693
- !this.isAnySelectorOrManagerActive()) {
694
- // Unified interrupt for AI message generation and command execution
695
- this.callbacks.onAbortMessage?.();
696
- return true;
697
- }
698
- // Handle Shift+Tab for permission mode cycling
699
- if (key.tab && key.shift) {
700
- this.logger?.debug("Shift+Tab detected, cycling permission mode");
701
- this.cyclePermissionMode();
702
- return true;
703
- }
704
- // Check if any selector is active
705
- if (this.isAnySelectorOrManagerActive()) {
706
- if (this.showBackgroundTaskManager ||
707
- this.showMcpManager ||
708
- this.showRewindManager ||
709
- this.showHelp ||
710
- this.showStatusCommand) {
711
- // Task manager, MCP manager, Rewind, Help and Status don't need to handle input, handled by component itself
712
- // Return true to indicate we've "handled" it (by ignoring it) so it doesn't leak to normal input
713
- return true;
714
- }
715
- if (this.showHistorySearch) {
716
- if (key.escape) {
717
- this.handleCancelHistorySearch();
718
- return true;
719
- }
720
- if (key.backspace || key.delete) {
721
- if (this.historySearchQuery.length > 0) {
722
- this.updateHistorySearchQuery(this.historySearchQuery.slice(0, -1));
723
- }
724
- return true;
725
- }
726
- if (input && !key.ctrl && !key.meta && !key.return && !key.tab) {
727
- this.updateHistorySearchQuery(this.historySearchQuery + input);
728
- return true;
729
- }
730
- return true; // Let HistorySearch component handle arrows and Enter
731
- }
732
- return this.handleSelectorInput(input, key);
733
- }
734
- else {
735
- return await this.handleNormalInput(input, key, attachedImages, isLoading, isCommandRunning, clearImages);
736
- }
737
- }
738
- // Cleanup method
739
- destroy() {
740
- if (this.fileSearchDebounceTimer) {
741
- clearTimeout(this.fileSearchDebounceTimer);
742
- this.fileSearchDebounceTimer = null;
743
- }
744
- if (this.pasteDebounceTimer) {
745
- clearTimeout(this.pasteDebounceTimer);
746
- this.pasteDebounceTimer = null;
747
- }
748
- }
749
- }