wave-code 0.8.1 → 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.
@@ -1,1024 +0,0 @@
1
- import { FileItem } from "../components/FileSelector.js";
2
- import {
3
- searchFiles as searchFilesUtil,
4
- PermissionMode,
5
- Logger,
6
- PromptHistoryManager,
7
- } from "wave-agent-sdk";
8
- import { readClipboardImage } from "../utils/clipboard.js";
9
- import type { Key } from "ink";
10
-
11
- export interface AttachedImage {
12
- id: number;
13
- path: string;
14
- mimeType: string;
15
- }
16
-
17
- export interface InputManagerCallbacks {
18
- onInputTextChange?: (text: string) => void;
19
- onCursorPositionChange?: (position: number) => void;
20
- onFileSelectorStateChange?: (
21
- show: boolean,
22
- files: FileItem[],
23
- query: string,
24
- position: number,
25
- ) => void;
26
- onCommandSelectorStateChange?: (
27
- show: boolean,
28
- query: string,
29
- position: number,
30
- ) => void;
31
- onHistorySearchStateChange?: (show: boolean, query: string) => void;
32
- onBackgroundTaskManagerStateChange?: (show: boolean) => void;
33
- onMcpManagerStateChange?: (show: boolean) => void;
34
- onRewindManagerStateChange?: (show: boolean) => void;
35
- onHelpStateChange?: (show: boolean) => void;
36
- onStatusCommandStateChange?: (show: boolean) => void;
37
- onImagesStateChange?: (images: AttachedImage[]) => void;
38
- onSendMessage?: (
39
- content: string,
40
- images?: Array<{ path: string; mimeType: string }>,
41
- ) => void | Promise<void>;
42
- onHasSlashCommand?: (commandId: string) => boolean;
43
- onAbortMessage?: () => void;
44
- onClearMessages?: () => void;
45
- onBackgroundCurrentTask?: () => void;
46
- onResetHistoryNavigation?: () => void;
47
- onPermissionModeChange?: (mode: PermissionMode) => void;
48
- logger?: Logger;
49
- }
50
-
51
- export class InputManager {
52
- // Core input state
53
- private inputText: string = "";
54
- private cursorPosition: number = 0;
55
-
56
- // File selector state
57
- private showFileSelector: boolean = false;
58
- private atPosition: number = -1;
59
- private fileSearchQuery: string = "";
60
- private filteredFiles: FileItem[] = [];
61
- private fileSearchDebounceTimer: NodeJS.Timeout | null = null;
62
-
63
- // Command selector state
64
- private showCommandSelector: boolean = false;
65
- private slashPosition: number = -1;
66
- private commandSearchQuery: string = "";
67
-
68
- // History search state
69
- private showHistorySearch: boolean = false;
70
- private historySearchQuery: string = "";
71
-
72
- // Paste debounce state
73
- private pasteDebounceTimer: NodeJS.Timeout | null = null;
74
- private pasteBuffer: string = "";
75
- private initialPasteCursorPosition: number = 0;
76
- private isPasting: boolean = false;
77
-
78
- // Long text compression state
79
- private longTextCounter: number = 0;
80
- private longTextMap: Map<string, string> = new Map();
81
-
82
- // Image management state
83
- private attachedImages: AttachedImage[] = [];
84
- private imageIdCounter: number = 1;
85
-
86
- // Additional UI state
87
- private showBackgroundTaskManager: boolean = false;
88
- private showMcpManager: boolean = false;
89
- private showRewindManager: boolean = false;
90
- private showHelp: boolean = false;
91
- private showStatusCommand: boolean = false;
92
-
93
- // Permission mode state
94
- private permissionMode: PermissionMode = "default";
95
-
96
- // Flag to prevent handleInput conflicts when selector selection occurs
97
- private selectorJustUsed: boolean = false;
98
-
99
- private callbacks: InputManagerCallbacks;
100
- private logger?: Logger;
101
-
102
- constructor(callbacks: InputManagerCallbacks = {}) {
103
- this.callbacks = callbacks;
104
- this.logger = callbacks.logger;
105
- }
106
-
107
- // Update callbacks
108
- updateCallbacks(callbacks: Partial<InputManagerCallbacks>) {
109
- this.callbacks = { ...this.callbacks, ...callbacks };
110
- if (callbacks.logger) {
111
- this.logger = callbacks.logger;
112
- }
113
- }
114
-
115
- // Core input methods
116
- getInputText(): string {
117
- return this.inputText;
118
- }
119
-
120
- setInputText(text: string): void {
121
- this.inputText = text;
122
- this.callbacks.onInputTextChange?.(text);
123
- }
124
-
125
- getCursorPosition(): number {
126
- return this.cursorPosition;
127
- }
128
-
129
- setCursorPosition(position: number): void {
130
- this.cursorPosition = Math.max(
131
- 0,
132
- Math.min(this.inputText.length, position),
133
- );
134
- this.callbacks.onCursorPositionChange?.(this.cursorPosition);
135
- }
136
-
137
- insertTextAtCursor(
138
- text: string,
139
- callback?: (newText: string, newCursorPosition: number) => void,
140
- ): void {
141
- const beforeCursor = this.inputText.substring(0, this.cursorPosition);
142
- const afterCursor = this.inputText.substring(this.cursorPosition);
143
- const newText = beforeCursor + text + afterCursor;
144
- const newCursorPosition = this.cursorPosition + text.length;
145
-
146
- this.inputText = newText;
147
- this.cursorPosition = newCursorPosition;
148
-
149
- this.callbacks.onInputTextChange?.(newText);
150
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
151
-
152
- callback?.(newText, newCursorPosition);
153
- }
154
-
155
- deleteCharAtCursor(
156
- callback?: (newText: string, newCursorPosition: number) => void,
157
- ): void {
158
- if (this.cursorPosition > 0) {
159
- const beforeCursor = this.inputText.substring(0, this.cursorPosition - 1);
160
- const afterCursor = this.inputText.substring(this.cursorPosition);
161
- const newText = beforeCursor + afterCursor;
162
- const newCursorPosition = this.cursorPosition - 1;
163
-
164
- this.inputText = newText;
165
- this.cursorPosition = newCursorPosition;
166
-
167
- this.callbacks.onInputTextChange?.(newText);
168
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
169
-
170
- callback?.(newText, newCursorPosition);
171
- }
172
- }
173
-
174
- clearInput(): void {
175
- this.inputText = "";
176
- this.cursorPosition = 0;
177
- this.callbacks.onInputTextChange?.("");
178
- this.callbacks.onCursorPositionChange?.(0);
179
- }
180
-
181
- moveCursorLeft(): void {
182
- this.setCursorPosition(this.cursorPosition - 1);
183
- }
184
-
185
- moveCursorRight(): void {
186
- this.setCursorPosition(this.cursorPosition + 1);
187
- }
188
-
189
- // File selector methods
190
- private async searchFiles(query: string): Promise<void> {
191
- try {
192
- const fileItems = await searchFilesUtil(query);
193
- this.filteredFiles = fileItems;
194
- this.callbacks.onFileSelectorStateChange?.(
195
- this.showFileSelector,
196
- this.filteredFiles,
197
- this.fileSearchQuery,
198
- this.atPosition,
199
- );
200
- } catch (error) {
201
- console.error("File search error:", error);
202
- this.filteredFiles = [];
203
- this.callbacks.onFileSelectorStateChange?.(
204
- this.showFileSelector,
205
- [],
206
- this.fileSearchQuery,
207
- this.atPosition,
208
- );
209
- }
210
- }
211
-
212
- private debouncedSearchFiles(query: string): void {
213
- if (this.fileSearchDebounceTimer) {
214
- clearTimeout(this.fileSearchDebounceTimer);
215
- }
216
-
217
- const debounceDelay = parseInt(
218
- process.env.FILE_SELECTOR_DEBOUNCE_MS || "300",
219
- 10,
220
- );
221
- this.fileSearchDebounceTimer = setTimeout(() => {
222
- this.searchFiles(query);
223
- }, debounceDelay);
224
- }
225
-
226
- activateFileSelector(position: number): void {
227
- this.showFileSelector = true;
228
- this.atPosition = position;
229
- this.fileSearchQuery = "";
230
- this.filteredFiles = [];
231
-
232
- // Immediately trigger search to display initial file list
233
- this.searchFiles("");
234
-
235
- this.callbacks.onFileSelectorStateChange?.(
236
- true,
237
- this.filteredFiles,
238
- "",
239
- position,
240
- );
241
- }
242
-
243
- updateFileSearchQuery(query: string): void {
244
- this.fileSearchQuery = query;
245
- this.debouncedSearchFiles(query);
246
- }
247
-
248
- handleFileSelect(filePath: string): {
249
- newInput: string;
250
- newCursorPosition: number;
251
- } {
252
- if (this.atPosition >= 0) {
253
- const beforeAt = this.inputText.substring(0, this.atPosition);
254
- const afterQuery = this.inputText.substring(this.cursorPosition);
255
- const newInput = beforeAt + `${filePath} ` + afterQuery;
256
- const newCursorPosition = beforeAt.length + filePath.length + 1;
257
-
258
- this.inputText = newInput;
259
- this.cursorPosition = newCursorPosition;
260
-
261
- this.callbacks.onInputTextChange?.(newInput);
262
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
263
-
264
- // Cancel file selector AFTER updating the input
265
- this.handleCancelFileSelect();
266
-
267
- // Set flag to prevent handleInput from processing the same Enter key
268
- this.selectorJustUsed = true;
269
- // Reset flag after a short delay
270
- setTimeout(() => {
271
- this.selectorJustUsed = false;
272
- }, 0);
273
-
274
- return { newInput, newCursorPosition };
275
- }
276
- return { newInput: this.inputText, newCursorPosition: this.cursorPosition };
277
- }
278
-
279
- handleCancelFileSelect(): void {
280
- this.showFileSelector = false;
281
- this.atPosition = -1;
282
- this.fileSearchQuery = "";
283
- this.filteredFiles = [];
284
-
285
- this.callbacks.onFileSelectorStateChange?.(false, [], "", -1);
286
- }
287
-
288
- checkForAtDeletion(cursorPosition: number): boolean {
289
- if (this.showFileSelector && cursorPosition <= this.atPosition) {
290
- this.handleCancelFileSelect();
291
- return true;
292
- }
293
- return false;
294
- }
295
-
296
- // Command selector methods
297
- activateCommandSelector(position: number): void {
298
- this.showCommandSelector = true;
299
- this.slashPosition = position;
300
- this.commandSearchQuery = "";
301
-
302
- this.callbacks.onCommandSelectorStateChange?.(true, "", position);
303
- }
304
-
305
- updateCommandSearchQuery(query: string): void {
306
- this.commandSearchQuery = query;
307
- this.callbacks.onCommandSelectorStateChange?.(
308
- this.showCommandSelector,
309
- query,
310
- this.slashPosition,
311
- );
312
- }
313
-
314
- handleCommandSelect(command: string): {
315
- newInput: string;
316
- newCursorPosition: number;
317
- } {
318
- if (this.slashPosition >= 0) {
319
- // Replace command part, keep other content
320
- const beforeSlash = this.inputText.substring(0, this.slashPosition);
321
- const afterQuery = this.inputText.substring(this.cursorPosition);
322
- const newInput = beforeSlash + afterQuery;
323
- const newCursorPosition = beforeSlash.length;
324
-
325
- this.inputText = newInput;
326
- this.cursorPosition = newCursorPosition;
327
-
328
- // Execute command asynchronously
329
- (async () => {
330
- // First check if it's an agent command
331
- let commandExecuted = false;
332
- if (
333
- this.callbacks.onSendMessage &&
334
- this.callbacks.onHasSlashCommand?.(command)
335
- ) {
336
- // Execute complete command (replace partial input with complete command name)
337
- const fullCommand = `/${command}`;
338
- try {
339
- await this.callbacks.onSendMessage(fullCommand);
340
- commandExecuted = true;
341
- } catch (error) {
342
- console.error("Failed to execute slash command:", error);
343
- }
344
- }
345
-
346
- // If not an agent command or execution failed, check local commands
347
- if (!commandExecuted) {
348
- if (command === "clear") {
349
- this.callbacks.onClearMessages?.();
350
- commandExecuted = true;
351
- } else if (command === "tasks") {
352
- this.setShowBackgroundTaskManager(true);
353
- commandExecuted = true;
354
- } else if (command === "mcp") {
355
- this.setShowMcpManager(true);
356
- commandExecuted = true;
357
- } else if (command === "rewind") {
358
- this.setShowRewindManager(true);
359
- commandExecuted = true;
360
- } else if (command === "help") {
361
- this.setShowHelp(true);
362
- commandExecuted = true;
363
- } else if (command === "status") {
364
- this.setShowStatusCommand(true);
365
- commandExecuted = true;
366
- }
367
- }
368
- })();
369
-
370
- this.handleCancelCommandSelect();
371
-
372
- // Set flag to prevent handleInput from processing the same Enter key
373
- this.selectorJustUsed = true;
374
- setTimeout(() => {
375
- this.selectorJustUsed = false;
376
- }, 0);
377
-
378
- this.callbacks.onInputTextChange?.(newInput);
379
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
380
-
381
- return { newInput, newCursorPosition };
382
- }
383
- return { newInput: this.inputText, newCursorPosition: this.cursorPosition };
384
- }
385
-
386
- handleCommandInsert(command: string): {
387
- newInput: string;
388
- newCursorPosition: number;
389
- } {
390
- if (this.slashPosition >= 0) {
391
- const beforeSlash = this.inputText.substring(0, this.slashPosition);
392
- const afterQuery = this.inputText.substring(this.cursorPosition);
393
- const newInput = beforeSlash + `/${command} ` + afterQuery;
394
- const newCursorPosition = beforeSlash.length + command.length + 2;
395
-
396
- this.inputText = newInput;
397
- this.cursorPosition = newCursorPosition;
398
-
399
- this.handleCancelCommandSelect();
400
-
401
- // Set flag to prevent handleInput from processing the same Enter key
402
- this.selectorJustUsed = true;
403
- setTimeout(() => {
404
- this.selectorJustUsed = false;
405
- }, 0);
406
-
407
- this.callbacks.onInputTextChange?.(newInput);
408
- this.callbacks.onCursorPositionChange?.(newCursorPosition);
409
-
410
- return { newInput, newCursorPosition };
411
- }
412
- return { newInput: this.inputText, newCursorPosition: this.cursorPosition };
413
- }
414
-
415
- handleCancelCommandSelect(): void {
416
- this.showCommandSelector = false;
417
- this.slashPosition = -1;
418
- this.commandSearchQuery = "";
419
-
420
- this.callbacks.onCommandSelectorStateChange?.(false, "", -1);
421
- }
422
-
423
- checkForSlashDeletion(cursorPosition: number): boolean {
424
- if (this.showCommandSelector && cursorPosition <= this.slashPosition) {
425
- this.handleCancelCommandSelect();
426
- return true;
427
- }
428
- return false;
429
- }
430
-
431
- // Getter methods for state
432
- isFileSelectorActive(): boolean {
433
- return this.showFileSelector;
434
- }
435
-
436
- isCommandSelectorActive(): boolean {
437
- return this.showCommandSelector;
438
- }
439
-
440
- getFileSelectorState() {
441
- return {
442
- show: this.showFileSelector,
443
- files: this.filteredFiles,
444
- query: this.fileSearchQuery,
445
- position: this.atPosition,
446
- };
447
- }
448
-
449
- getCommandSelectorState() {
450
- return {
451
- show: this.showCommandSelector,
452
- query: this.commandSearchQuery,
453
- position: this.slashPosition,
454
- };
455
- }
456
-
457
- // Update search queries for active selectors
458
- private updateSearchQueriesForActiveSelectors(
459
- inputText: string,
460
- cursorPosition: number,
461
- ): void {
462
- if (this.showFileSelector && this.atPosition >= 0) {
463
- const queryStart = this.atPosition + 1;
464
- const queryEnd = cursorPosition;
465
- const newQuery = inputText.substring(queryStart, queryEnd);
466
- this.updateFileSearchQuery(newQuery);
467
- } else if (this.showCommandSelector && this.slashPosition >= 0) {
468
- const queryStart = this.slashPosition + 1;
469
- const queryEnd = cursorPosition;
470
- const newQuery = inputText.substring(queryStart, queryEnd);
471
- this.updateCommandSearchQuery(newQuery);
472
- }
473
- }
474
-
475
- // Handle special character input that might trigger selectors
476
- handleSpecialCharInput(char: string): void {
477
- if (char === "@") {
478
- this.activateFileSelector(this.cursorPosition - 1);
479
- } else if (
480
- char === "/" &&
481
- !this.showFileSelector &&
482
- this.cursorPosition === 1
483
- ) {
484
- // Don't activate command selector when file selector is active
485
- // Only activate command selector if '/' is at the start of input
486
- this.activateCommandSelector(this.cursorPosition - 1);
487
- } else {
488
- // Update search queries for active selectors
489
- this.updateSearchQueriesForActiveSelectors(
490
- this.inputText,
491
- this.cursorPosition,
492
- );
493
- }
494
- }
495
-
496
- // Long text compression methods
497
- generateCompressedText(originalText: string): string {
498
- this.longTextCounter += 1;
499
- const compressedLabel = `[LongText#${this.longTextCounter}]`;
500
- this.longTextMap.set(compressedLabel, originalText);
501
- return compressedLabel;
502
- }
503
-
504
- expandLongTextPlaceholders(text: string): string {
505
- let expandedText = text;
506
- const longTextRegex = /\[LongText#(\d+)\]/g;
507
- const matches = [...text.matchAll(longTextRegex)];
508
-
509
- for (const match of matches) {
510
- const placeholder = match[0];
511
- const originalText = this.longTextMap.get(placeholder);
512
- if (originalText) {
513
- expandedText = expandedText.replace(placeholder, originalText);
514
- }
515
- }
516
-
517
- return expandedText;
518
- }
519
-
520
- clearLongTextMap(): void {
521
- this.longTextMap.clear();
522
- }
523
-
524
- // Paste handling methods
525
- handlePasteInput(input: string): void {
526
- const inputString = input;
527
-
528
- // Detect if it's a paste operation (input contains multiple characters or newlines)
529
- const isPasteOperation =
530
- inputString.length > 1 ||
531
- inputString.includes("\n") ||
532
- inputString.includes("\r");
533
-
534
- if (isPasteOperation) {
535
- // Start or continue the debounce handling for paste operation
536
- if (!this.isPasting) {
537
- // Start new paste operation
538
- this.isPasting = true;
539
- this.pasteBuffer = inputString;
540
- this.initialPasteCursorPosition = this.cursorPosition;
541
- } else {
542
- // Continue paste operation, add new input to buffer
543
- this.pasteBuffer += inputString;
544
- }
545
-
546
- // Clear previous timer
547
- if (this.pasteDebounceTimer) {
548
- clearTimeout(this.pasteDebounceTimer);
549
- }
550
-
551
- // Set new timer, support environment variable configuration
552
- const pasteDebounceDelay = parseInt(
553
- process.env.PASTE_DEBOUNCE_MS || "30",
554
- 10,
555
- );
556
- this.pasteDebounceTimer = setTimeout(() => {
557
- // Process all paste content in buffer
558
- let processedInput = this.pasteBuffer.replace(/\r/g, "\n");
559
-
560
- // Check if long text compression is needed (over 200 characters)
561
- if (processedInput.length > 200) {
562
- const originalText = processedInput;
563
- const compressedLabel = this.generateCompressedText(originalText);
564
- processedInput = compressedLabel;
565
- }
566
-
567
- this.insertTextAtCursor(processedInput);
568
- this.callbacks.onResetHistoryNavigation?.();
569
-
570
- // Reset paste state
571
- this.isPasting = false;
572
- this.pasteBuffer = "";
573
- this.pasteDebounceTimer = null;
574
- }, pasteDebounceDelay);
575
- } else {
576
- // Handle single character input
577
- let char = inputString;
578
-
579
- // Check if it's Chinese exclamation mark, convert to English if at beginning
580
- if (char === "!" && this.cursorPosition === 0) {
581
- char = "!";
582
- }
583
-
584
- this.callbacks.onResetHistoryNavigation?.();
585
- this.insertTextAtCursor(char, () => {
586
- // Handle special character input - this will manage all selectors
587
- this.handleSpecialCharInput(char);
588
- });
589
- }
590
- }
591
-
592
- // Image management methods
593
- addImage(imagePath: string, mimeType: string): AttachedImage {
594
- const newImage: AttachedImage = {
595
- id: this.imageIdCounter,
596
- path: imagePath,
597
- mimeType,
598
- };
599
- this.attachedImages = [...this.attachedImages, newImage];
600
- this.imageIdCounter++;
601
- this.callbacks.onImagesStateChange?.(this.attachedImages);
602
- return newImage;
603
- }
604
-
605
- removeImage(imageId: number): void {
606
- this.attachedImages = this.attachedImages.filter(
607
- (img) => img.id !== imageId,
608
- );
609
- this.callbacks.onImagesStateChange?.(this.attachedImages);
610
- }
611
-
612
- clearImages(): void {
613
- this.attachedImages = [];
614
- this.callbacks.onImagesStateChange?.(this.attachedImages);
615
- }
616
-
617
- getAttachedImages(): AttachedImage[] {
618
- return this.attachedImages;
619
- }
620
-
621
- async handlePasteImage(): Promise<boolean> {
622
- try {
623
- const result = await readClipboardImage();
624
-
625
- if (result.success && result.imagePath && result.mimeType) {
626
- // Add image to manager
627
- const attachedImage = this.addImage(result.imagePath, result.mimeType);
628
-
629
- // Insert image placeholder at cursor position
630
- this.insertTextAtCursor(`[Image #${attachedImage.id}]`);
631
-
632
- return true;
633
- }
634
-
635
- return false;
636
- } catch (error) {
637
- console.warn("Failed to paste image from clipboard:", error);
638
- return false;
639
- }
640
- }
641
-
642
- // Task manager state methods
643
- getShowBackgroundTaskManager(): boolean {
644
- return this.showBackgroundTaskManager;
645
- }
646
-
647
- setShowBackgroundTaskManager(show: boolean): void {
648
- this.showBackgroundTaskManager = show;
649
- this.callbacks.onBackgroundTaskManagerStateChange?.(show);
650
- }
651
-
652
- getShowMcpManager(): boolean {
653
- return this.showMcpManager;
654
- }
655
-
656
- setShowMcpManager(show: boolean): void {
657
- this.showMcpManager = show;
658
- this.callbacks.onMcpManagerStateChange?.(show);
659
- }
660
-
661
- getShowRewindManager(): boolean {
662
- return this.showRewindManager;
663
- }
664
-
665
- setShowRewindManager(show: boolean): void {
666
- this.showRewindManager = show;
667
- this.callbacks.onRewindManagerStateChange?.(show);
668
- }
669
-
670
- getShowHelp(): boolean {
671
- return this.showHelp;
672
- }
673
-
674
- setShowHelp(show: boolean): void {
675
- this.showHelp = show;
676
- this.callbacks.onHelpStateChange?.(show);
677
- }
678
-
679
- getShowStatusCommand(): boolean {
680
- return this.showStatusCommand;
681
- }
682
-
683
- setShowStatusCommand(show: boolean): void {
684
- this.showStatusCommand = show;
685
- this.callbacks.onStatusCommandStateChange?.(show);
686
- }
687
-
688
- // Permission mode methods
689
- getPermissionMode(): PermissionMode {
690
- return this.permissionMode;
691
- }
692
-
693
- isAnySelectorOrManagerActive(): boolean {
694
- return (
695
- this.showFileSelector ||
696
- this.showCommandSelector ||
697
- this.showHistorySearch ||
698
- this.showBackgroundTaskManager ||
699
- this.showMcpManager ||
700
- this.showRewindManager ||
701
- this.showHelp ||
702
- this.showStatusCommand
703
- );
704
- }
705
-
706
- setPermissionMode(mode: PermissionMode): void {
707
- this.permissionMode = mode;
708
- }
709
-
710
- cyclePermissionMode(): void {
711
- const modes: PermissionMode[] = ["default", "acceptEdits", "plan"];
712
- const currentIndex = modes.indexOf(this.permissionMode);
713
- const nextIndex =
714
- currentIndex === -1 ? 0 : (currentIndex + 1) % modes.length;
715
- const nextMode = modes[nextIndex];
716
- this.logger?.debug("Cycling permission mode", {
717
- from: this.permissionMode,
718
- to: nextMode,
719
- });
720
- this.permissionMode = nextMode;
721
- this.callbacks.onPermissionModeChange?.(this.permissionMode);
722
- }
723
-
724
- // Handle submit logic
725
- async handleSubmit(
726
- attachedImages: Array<{ id: number; path: string; mimeType: string }>,
727
- isLoading: boolean = false,
728
- isCommandRunning: boolean = false,
729
- ): Promise<void> {
730
- // Prevent submission during loading or command execution
731
- if (isLoading || isCommandRunning) {
732
- return;
733
- }
734
-
735
- if (this.inputText.trim()) {
736
- // Extract image information
737
- const imageRegex = /\[Image #(\d+)\]/g;
738
- const matches = [...this.inputText.matchAll(imageRegex)];
739
- const referencedImages = matches
740
- .map((match) => {
741
- const imageId = parseInt(match[1], 10);
742
- return attachedImages.find((img) => img.id === imageId);
743
- })
744
- .filter(
745
- (img): img is { id: number; path: string; mimeType: string } =>
746
- img !== undefined,
747
- )
748
- .map((img) => ({ path: img.path, mimeType: img.mimeType }));
749
-
750
- // Remove image placeholders, expand long text placeholders, send message
751
- let cleanContent = this.inputText.replace(imageRegex, "").trim();
752
- cleanContent = this.expandLongTextPlaceholders(cleanContent);
753
-
754
- // Save to prompt history
755
- PromptHistoryManager.addEntry(cleanContent).catch((err: unknown) => {
756
- this.logger?.error("Failed to save prompt history", err);
757
- });
758
-
759
- this.callbacks.onSendMessage?.(
760
- cleanContent,
761
- referencedImages.length > 0 ? referencedImages : undefined,
762
- );
763
- this.clearInput();
764
- this.callbacks.onResetHistoryNavigation?.();
765
-
766
- // Clear long text mapping
767
- this.clearLongTextMap();
768
- }
769
- }
770
-
771
- // Handle selector input (when any selector is active)
772
- handleSelectorInput(input: string, key: Key): boolean {
773
- if (key.backspace || key.delete) {
774
- if (this.cursorPosition > 0) {
775
- this.deleteCharAtCursor((newInput, newCursorPosition) => {
776
- // Check for special character deletion
777
- this.checkForAtDeletion(newCursorPosition);
778
- this.checkForSlashDeletion(newCursorPosition);
779
-
780
- // Update search queries using the same logic as character input
781
- this.updateSearchQueriesForActiveSelectors(
782
- newInput,
783
- newCursorPosition,
784
- );
785
- });
786
- }
787
- return true;
788
- }
789
-
790
- // Arrow keys, Enter and Tab should be handled by selector components
791
- if (key.upArrow || key.downArrow || key.return || key.tab) {
792
- // Let selector component handle these keys, but prevent further processing
793
- // by returning true (indicating we've handled the input)
794
- return true;
795
- }
796
-
797
- if (
798
- input &&
799
- !key.ctrl &&
800
- !("alt" in key && key.alt) &&
801
- !key.meta &&
802
- !key.return &&
803
- !key.tab &&
804
- !key.escape &&
805
- !key.leftArrow &&
806
- !key.rightArrow &&
807
- !("home" in key && key.home) &&
808
- !("end" in key && key.end)
809
- ) {
810
- // Handle character input for search
811
- this.insertTextAtCursor(input, () => {
812
- // Special character handling is now managed by InputManager
813
- this.handleSpecialCharInput(input);
814
- });
815
- return true;
816
- }
817
-
818
- return false;
819
- }
820
-
821
- // History search methods
822
- activateHistorySearch(): void {
823
- this.showHistorySearch = true;
824
- this.historySearchQuery = "";
825
- this.callbacks.onHistorySearchStateChange?.(true, "");
826
- }
827
-
828
- updateHistorySearchQuery(query: string): void {
829
- this.historySearchQuery = query;
830
- this.callbacks.onHistorySearchStateChange?.(true, query);
831
- }
832
-
833
- handleHistorySearchSelect(prompt: string): void {
834
- this.inputText = prompt;
835
- this.cursorPosition = prompt.length;
836
- this.callbacks.onInputTextChange?.(prompt);
837
- this.callbacks.onCursorPositionChange?.(prompt.length);
838
- this.handleCancelHistorySearch();
839
- }
840
-
841
- handleCancelHistorySearch(): void {
842
- this.showHistorySearch = false;
843
- this.historySearchQuery = "";
844
- this.callbacks.onHistorySearchStateChange?.(false, "");
845
- }
846
-
847
- // Handle normal input (when no selector is active)
848
- async handleNormalInput(
849
- input: string,
850
- key: Key,
851
- attachedImages: Array<{ id: number; path: string; mimeType: string }>,
852
- isLoading: boolean = false,
853
- isCommandRunning: boolean = false,
854
- clearImages?: () => void,
855
- ): Promise<boolean> {
856
- if (key.return) {
857
- await this.handleSubmit(attachedImages, isLoading, isCommandRunning);
858
- clearImages?.();
859
- return true;
860
- }
861
-
862
- if (key.escape) {
863
- if (this.showFileSelector) {
864
- this.handleCancelFileSelect();
865
- } else if (this.showCommandSelector) {
866
- this.handleCancelCommandSelect();
867
- }
868
- return true;
869
- }
870
-
871
- if (key.backspace || key.delete) {
872
- if (this.cursorPosition > 0) {
873
- this.deleteCharAtCursor();
874
- this.callbacks.onResetHistoryNavigation?.();
875
-
876
- // Check if we deleted any special characters
877
- const newCursorPosition = this.cursorPosition - 1;
878
- this.checkForAtDeletion(newCursorPosition);
879
- this.checkForSlashDeletion(newCursorPosition);
880
- }
881
- return true;
882
- }
883
-
884
- if (key.leftArrow) {
885
- this.moveCursorLeft();
886
- return true;
887
- }
888
-
889
- if (key.rightArrow) {
890
- this.moveCursorRight();
891
- return true;
892
- }
893
-
894
- // Handle Ctrl+V for pasting images
895
- if (key.ctrl && input === "v") {
896
- this.handlePasteImage().catch((error) => {
897
- console.warn("Failed to handle paste image:", error);
898
- });
899
- return true;
900
- }
901
-
902
- // Handle Ctrl+R for history search
903
- if (key.ctrl && input === "r") {
904
- this.activateHistorySearch();
905
- return true;
906
- }
907
-
908
- // Handle Ctrl+B for backgrounding current task
909
- if (key.ctrl && input === "b") {
910
- this.callbacks.onBackgroundCurrentTask?.();
911
- return true;
912
- }
913
-
914
- // Handle typing input
915
- if (
916
- input &&
917
- !key.ctrl &&
918
- !("alt" in key && key.alt) &&
919
- !key.meta &&
920
- !key.return &&
921
- !key.escape &&
922
- !key.backspace &&
923
- !key.delete &&
924
- !key.leftArrow &&
925
- !key.rightArrow &&
926
- !("home" in key && key.home) &&
927
- !("end" in key && key.end)
928
- ) {
929
- this.handlePasteInput(input);
930
- return true;
931
- }
932
-
933
- return false;
934
- }
935
-
936
- // Main input handler - routes to appropriate handler based on state
937
- async handleInput(
938
- input: string,
939
- key: Key,
940
- attachedImages: Array<{ id: number; path: string; mimeType: string }>,
941
- isLoading: boolean = false,
942
- isCommandRunning: boolean = false,
943
- clearImages?: () => void,
944
- ): Promise<boolean> {
945
- // If selector was just used, ignore this input to prevent conflicts
946
- if (this.selectorJustUsed) {
947
- return true;
948
- }
949
-
950
- // Handle interrupt request - use Esc key to interrupt AI request or command
951
- if (
952
- key.escape &&
953
- (isLoading || isCommandRunning) &&
954
- !this.isAnySelectorOrManagerActive()
955
- ) {
956
- // Unified interrupt for AI message generation and command execution
957
- this.callbacks.onAbortMessage?.();
958
- return true;
959
- }
960
-
961
- // Handle Shift+Tab for permission mode cycling
962
- if (key.tab && key.shift) {
963
- this.logger?.debug("Shift+Tab detected, cycling permission mode");
964
- this.cyclePermissionMode();
965
- return true;
966
- }
967
-
968
- // Check if any selector is active
969
- if (this.isAnySelectorOrManagerActive()) {
970
- if (
971
- this.showBackgroundTaskManager ||
972
- this.showMcpManager ||
973
- this.showRewindManager ||
974
- this.showHelp ||
975
- this.showStatusCommand
976
- ) {
977
- // Task manager, MCP manager, Rewind, Help and Status don't need to handle input, handled by component itself
978
- // Return true to indicate we've "handled" it (by ignoring it) so it doesn't leak to normal input
979
- return true;
980
- }
981
-
982
- if (this.showHistorySearch) {
983
- if (key.escape) {
984
- this.handleCancelHistorySearch();
985
- return true;
986
- }
987
- if (key.backspace || key.delete) {
988
- if (this.historySearchQuery.length > 0) {
989
- this.updateHistorySearchQuery(this.historySearchQuery.slice(0, -1));
990
- }
991
- return true;
992
- }
993
- if (input && !key.ctrl && !key.meta && !key.return && !key.tab) {
994
- this.updateHistorySearchQuery(this.historySearchQuery + input);
995
- return true;
996
- }
997
- return true; // Let HistorySearch component handle arrows and Enter
998
- }
999
-
1000
- return this.handleSelectorInput(input, key);
1001
- } else {
1002
- return await this.handleNormalInput(
1003
- input,
1004
- key,
1005
- attachedImages,
1006
- isLoading,
1007
- isCommandRunning,
1008
- clearImages,
1009
- );
1010
- }
1011
- }
1012
-
1013
- // Cleanup method
1014
- destroy(): void {
1015
- if (this.fileSearchDebounceTimer) {
1016
- clearTimeout(this.fileSearchDebounceTimer);
1017
- this.fileSearchDebounceTimer = null;
1018
- }
1019
- if (this.pasteDebounceTimer) {
1020
- clearTimeout(this.pasteDebounceTimer);
1021
- this.pasteDebounceTimer = null;
1022
- }
1023
- }
1024
- }