wave-code 0.0.4 → 0.0.6

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