wave-code 0.0.4 → 0.0.5

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