wave-code 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +120 -0
  2. package/bin/wave-code.js +16 -0
  3. package/dist/cli.d.ts +6 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +62 -0
  6. package/dist/components/App.d.ts +8 -0
  7. package/dist/components/App.d.ts.map +1 -0
  8. package/dist/components/App.js +10 -0
  9. package/dist/components/BashHistorySelector.d.ts +10 -0
  10. package/dist/components/BashHistorySelector.d.ts.map +1 -0
  11. package/dist/components/BashHistorySelector.js +83 -0
  12. package/dist/components/BashShellManager.d.ts +6 -0
  13. package/dist/components/BashShellManager.d.ts.map +1 -0
  14. package/dist/components/BashShellManager.js +116 -0
  15. package/dist/components/ChatInterface.d.ts +3 -0
  16. package/dist/components/ChatInterface.d.ts.map +1 -0
  17. package/dist/components/ChatInterface.js +31 -0
  18. package/dist/components/CommandOutputDisplay.d.ts +9 -0
  19. package/dist/components/CommandOutputDisplay.d.ts.map +1 -0
  20. package/dist/components/CommandOutputDisplay.js +40 -0
  21. package/dist/components/CommandSelector.d.ts +11 -0
  22. package/dist/components/CommandSelector.d.ts.map +1 -0
  23. package/dist/components/CommandSelector.js +60 -0
  24. package/dist/components/CompressDisplay.d.ts +9 -0
  25. package/dist/components/CompressDisplay.d.ts.map +1 -0
  26. package/dist/components/CompressDisplay.js +17 -0
  27. package/dist/components/DiffViewer.d.ts +9 -0
  28. package/dist/components/DiffViewer.d.ts.map +1 -0
  29. package/dist/components/DiffViewer.js +221 -0
  30. package/dist/components/FileSelector.d.ts +13 -0
  31. package/dist/components/FileSelector.d.ts.map +1 -0
  32. package/dist/components/FileSelector.js +48 -0
  33. package/dist/components/InputBox.d.ts +23 -0
  34. package/dist/components/InputBox.d.ts.map +1 -0
  35. package/dist/components/InputBox.js +124 -0
  36. package/dist/components/McpManager.d.ts +10 -0
  37. package/dist/components/McpManager.d.ts.map +1 -0
  38. package/dist/components/McpManager.js +123 -0
  39. package/dist/components/MemoryDisplay.d.ts +8 -0
  40. package/dist/components/MemoryDisplay.d.ts.map +1 -0
  41. package/dist/components/MemoryDisplay.js +25 -0
  42. package/dist/components/MemoryTypeSelector.d.ts +8 -0
  43. package/dist/components/MemoryTypeSelector.d.ts.map +1 -0
  44. package/dist/components/MemoryTypeSelector.js +38 -0
  45. package/dist/components/MessageList.d.ts +12 -0
  46. package/dist/components/MessageList.d.ts.map +1 -0
  47. package/dist/components/MessageList.js +36 -0
  48. package/dist/components/ToolResultDisplay.d.ts +9 -0
  49. package/dist/components/ToolResultDisplay.d.ts.map +1 -0
  50. package/dist/components/ToolResultDisplay.js +52 -0
  51. package/dist/contexts/useAppConfig.d.ts +11 -0
  52. package/dist/contexts/useAppConfig.d.ts.map +1 -0
  53. package/dist/contexts/useAppConfig.js +13 -0
  54. package/dist/contexts/useChat.d.ts +36 -0
  55. package/dist/contexts/useChat.d.ts.map +1 -0
  56. package/dist/contexts/useChat.js +208 -0
  57. package/dist/hooks/useBashHistorySelector.d.ts +15 -0
  58. package/dist/hooks/useBashHistorySelector.d.ts.map +1 -0
  59. package/dist/hooks/useBashHistorySelector.js +61 -0
  60. package/dist/hooks/useCommandSelector.d.ts +24 -0
  61. package/dist/hooks/useCommandSelector.d.ts.map +1 -0
  62. package/dist/hooks/useCommandSelector.js +98 -0
  63. package/dist/hooks/useFileSelector.d.ts +16 -0
  64. package/dist/hooks/useFileSelector.d.ts.map +1 -0
  65. package/dist/hooks/useFileSelector.js +174 -0
  66. package/dist/hooks/useImageManager.d.ts +13 -0
  67. package/dist/hooks/useImageManager.d.ts.map +1 -0
  68. package/dist/hooks/useImageManager.js +46 -0
  69. package/dist/hooks/useInputHistory.d.ts +11 -0
  70. package/dist/hooks/useInputHistory.d.ts.map +1 -0
  71. package/dist/hooks/useInputHistory.js +64 -0
  72. package/dist/hooks/useInputKeyboardHandler.d.ts +83 -0
  73. package/dist/hooks/useInputKeyboardHandler.d.ts.map +1 -0
  74. package/dist/hooks/useInputKeyboardHandler.js +507 -0
  75. package/dist/hooks/useInputState.d.ts +14 -0
  76. package/dist/hooks/useInputState.d.ts.map +1 -0
  77. package/dist/hooks/useInputState.js +57 -0
  78. package/dist/hooks/useMemoryTypeSelector.d.ts +9 -0
  79. package/dist/hooks/useMemoryTypeSelector.d.ts.map +1 -0
  80. package/dist/hooks/useMemoryTypeSelector.js +27 -0
  81. package/dist/hooks/usePagination.d.ts +20 -0
  82. package/dist/hooks/usePagination.d.ts.map +1 -0
  83. package/dist/hooks/usePagination.js +168 -0
  84. package/dist/index.d.ts +5 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +91 -0
  87. package/dist/plain-cli.d.ts +7 -0
  88. package/dist/plain-cli.d.ts.map +1 -0
  89. package/dist/plain-cli.js +49 -0
  90. package/dist/utils/clipboard.d.ts +22 -0
  91. package/dist/utils/clipboard.d.ts.map +1 -0
  92. package/dist/utils/clipboard.js +347 -0
  93. package/dist/utils/constants.d.ts +17 -0
  94. package/dist/utils/constants.d.ts.map +1 -0
  95. package/dist/utils/constants.js +18 -0
  96. package/dist/utils/logger.d.ts +72 -0
  97. package/dist/utils/logger.d.ts.map +1 -0
  98. package/dist/utils/logger.js +245 -0
  99. package/package.json +60 -0
  100. package/src/cli.tsx +82 -0
  101. package/src/components/App.tsx +31 -0
  102. package/src/components/BashHistorySelector.tsx +163 -0
  103. package/src/components/BashShellManager.tsx +306 -0
  104. package/src/components/ChatInterface.tsx +88 -0
  105. package/src/components/CommandOutputDisplay.tsx +81 -0
  106. package/src/components/CommandSelector.tsx +144 -0
  107. package/src/components/CompressDisplay.tsx +58 -0
  108. package/src/components/DiffViewer.tsx +321 -0
  109. package/src/components/FileSelector.tsx +137 -0
  110. package/src/components/InputBox.tsx +310 -0
  111. package/src/components/McpManager.tsx +328 -0
  112. package/src/components/MemoryDisplay.tsx +62 -0
  113. package/src/components/MemoryTypeSelector.tsx +96 -0
  114. package/src/components/MessageList.tsx +215 -0
  115. package/src/components/ToolResultDisplay.tsx +138 -0
  116. package/src/contexts/useAppConfig.tsx +32 -0
  117. package/src/contexts/useChat.tsx +300 -0
  118. package/src/hooks/useBashHistorySelector.ts +77 -0
  119. package/src/hooks/useCommandSelector.ts +131 -0
  120. package/src/hooks/useFileSelector.ts +227 -0
  121. package/src/hooks/useImageManager.ts +64 -0
  122. package/src/hooks/useInputHistory.ts +74 -0
  123. package/src/hooks/useInputKeyboardHandler.ts +778 -0
  124. package/src/hooks/useInputState.ts +66 -0
  125. package/src/hooks/useMemoryTypeSelector.ts +40 -0
  126. package/src/hooks/usePagination.ts +203 -0
  127. package/src/index.ts +108 -0
  128. package/src/plain-cli.ts +66 -0
  129. package/src/utils/clipboard.ts +384 -0
  130. package/src/utils/constants.ts +22 -0
  131. package/src/utils/logger.ts +301 -0
@@ -0,0 +1,778 @@
1
+ import { useCallback, useRef, useEffect } from "react";
2
+ import { useInput, Key } from "ink";
3
+ import { logger } from "../utils/logger.js";
4
+
5
+ interface KeyboardHandlerProps {
6
+ inputText: string;
7
+ setInputText: (text: string) => void;
8
+ cursorPosition: number;
9
+ setCursorPosition: (position: number) => void;
10
+ moveCursorLeft: () => void;
11
+ moveCursorRight: () => void;
12
+ moveCursorToStart: () => void;
13
+ moveCursorToEnd: () => void;
14
+ deleteCharAtCursor: () => void;
15
+ insertTextAtCursor: (text: string) => void;
16
+ clearInput: () => void;
17
+ resetHistoryNavigation: () => void;
18
+ navigateHistory: (
19
+ direction: "up" | "down",
20
+ inputText: string,
21
+ activateBashMode?: () => void,
22
+ ) => { newInput: string; newCursorPosition: number };
23
+ handlePasteImage: () => Promise<boolean>;
24
+ attachedImages: Array<{ id: number; path: string; mimeType: string }>;
25
+ clearImages: () => void;
26
+
27
+ // File selector
28
+ showFileSelector: boolean;
29
+ activateFileSelector: (position: number) => void;
30
+ handleFileSelect: (
31
+ filePath: string,
32
+ inputText: string,
33
+ cursorPosition: number,
34
+ ) => { newInput: string; newCursorPosition: number };
35
+ handleCancelFileSelect: () => void;
36
+ updateSearchQuery: (query: string) => void;
37
+ checkForAtDeletion: (cursorPosition: number) => boolean;
38
+ atPosition: number;
39
+
40
+ // Command selector
41
+ showCommandSelector: boolean;
42
+ activateCommandSelector: (position: number) => void;
43
+ handleCommandSelect: (
44
+ command: string,
45
+ inputText: string,
46
+ cursorPosition: number,
47
+ ) => { newInput: string; newCursorPosition: number };
48
+ handleCommandInsert: (
49
+ command: string,
50
+ inputText: string,
51
+ cursorPosition: number,
52
+ ) => { newInput: string; newCursorPosition: number };
53
+ handleCancelCommandSelect: () => void;
54
+ updateCommandSearchQuery: (query: string) => void;
55
+ checkForSlashDeletion: (cursorPosition: number) => boolean;
56
+ slashPosition: number;
57
+
58
+ // Bash history selector
59
+ showBashHistorySelector: boolean;
60
+ activateBashHistorySelector: (position: number) => void;
61
+ handleBashHistorySelect: (
62
+ command: string,
63
+ inputText: string,
64
+ cursorPosition: number,
65
+ ) => { newInput: string; newCursorPosition: number };
66
+ handleBashHistoryExecute: (command: string) => string;
67
+ handleCancelBashHistorySelect: () => void;
68
+ updateBashHistorySearchQuery: (query: string) => void;
69
+ checkForExclamationDeletion: (cursorPosition: number) => boolean;
70
+ exclamationPosition: number;
71
+
72
+ // Memory type selector
73
+ showMemoryTypeSelector: boolean;
74
+ activateMemoryTypeSelector: (message: string) => void;
75
+ handleMemoryTypeSelect: (type: "project" | "user") => void;
76
+
77
+ // Bash shell manager
78
+ showBashManager: boolean;
79
+
80
+ // MCP manager
81
+ showMcpManager: boolean;
82
+
83
+ // Chat actions
84
+ isCommandRunning: boolean;
85
+ isLoading: boolean;
86
+ sendMessage: (
87
+ message: string,
88
+ images?: Array<{ path: string; mimeType: string }>,
89
+ ) => void;
90
+ abortMessage: () => void;
91
+ saveMemory: (message: string, type: "project" | "user") => Promise<void>;
92
+ }
93
+
94
+ export const useInputKeyboardHandler = (props: KeyboardHandlerProps) => {
95
+ const {
96
+ inputText,
97
+ setInputText,
98
+ cursorPosition,
99
+ setCursorPosition,
100
+ moveCursorLeft,
101
+ moveCursorRight,
102
+ moveCursorToStart,
103
+ moveCursorToEnd,
104
+ deleteCharAtCursor,
105
+ insertTextAtCursor,
106
+ clearInput,
107
+ resetHistoryNavigation,
108
+ navigateHistory,
109
+ handlePasteImage,
110
+ attachedImages,
111
+ clearImages,
112
+ showFileSelector,
113
+ activateFileSelector,
114
+ handleFileSelect,
115
+ handleCancelFileSelect,
116
+ updateSearchQuery,
117
+ checkForAtDeletion,
118
+ atPosition,
119
+ showCommandSelector,
120
+ activateCommandSelector,
121
+ handleCommandSelect,
122
+ handleCommandInsert,
123
+ handleCancelCommandSelect,
124
+ updateCommandSearchQuery,
125
+ checkForSlashDeletion,
126
+ slashPosition,
127
+ showBashHistorySelector,
128
+ activateBashHistorySelector,
129
+ handleBashHistorySelect,
130
+ handleBashHistoryExecute,
131
+ handleCancelBashHistorySelect,
132
+ updateBashHistorySearchQuery,
133
+ checkForExclamationDeletion,
134
+ exclamationPosition,
135
+ showMemoryTypeSelector,
136
+ activateMemoryTypeSelector,
137
+ handleMemoryTypeSelect,
138
+ showBashManager,
139
+ showMcpManager,
140
+ isCommandRunning,
141
+ isLoading,
142
+ sendMessage,
143
+ abortMessage,
144
+ saveMemory,
145
+ } = props;
146
+
147
+ // Debounce for paste operations
148
+ const pasteDebounceRef = useRef<{
149
+ timer: NodeJS.Timeout | null;
150
+ buffer: string;
151
+ initialCursorPosition: number;
152
+ isPasting: boolean;
153
+ }>({
154
+ timer: null,
155
+ buffer: "",
156
+ initialCursorPosition: 0,
157
+ isPasting: false,
158
+ });
159
+
160
+ // Long text compression management
161
+ const longTextCounterRef = useRef<number>(0);
162
+ const longTextMapRef = useRef<Map<string, string>>(new Map());
163
+
164
+ const generateCompressedText = (originalText: string): string => {
165
+ longTextCounterRef.current += 1;
166
+ const compressedLabel = `[LongText#${longTextCounterRef.current}]`;
167
+ longTextMapRef.current.set(compressedLabel, originalText);
168
+ return compressedLabel;
169
+ };
170
+
171
+ const expandLongTextPlaceholders = (text: string): string => {
172
+ let expandedText = text;
173
+ const longTextRegex = /\[LongText#(\d+)\]/g;
174
+ const matches = [...text.matchAll(longTextRegex)];
175
+
176
+ for (const match of matches) {
177
+ const placeholder = match[0];
178
+ const originalText = longTextMapRef.current.get(placeholder);
179
+ if (originalText) {
180
+ expandedText = expandedText.replace(placeholder, originalText);
181
+ }
182
+ }
183
+
184
+ return expandedText;
185
+ };
186
+
187
+ // Cleanup on unmount
188
+ useEffect(() => {
189
+ const currentDebounceRef = pasteDebounceRef.current;
190
+ return () => {
191
+ if (currentDebounceRef.timer) {
192
+ clearTimeout(currentDebounceRef.timer);
193
+ }
194
+ };
195
+ }, []);
196
+
197
+ const handleSelectorInput = useCallback(
198
+ (input: string, key: Key) => {
199
+ if (key.backspace || key.delete) {
200
+ if (cursorPosition > 0) {
201
+ const newInput =
202
+ inputText.substring(0, cursorPosition - 1) +
203
+ inputText.substring(cursorPosition);
204
+ setInputText(newInput);
205
+ setCursorPosition(cursorPosition - 1);
206
+
207
+ // Update search query
208
+ if (atPosition >= 0) {
209
+ const queryStart = atPosition + 1;
210
+ const queryEnd = cursorPosition - 1;
211
+ if (queryEnd <= atPosition) {
212
+ // Deleted @ symbol, close file selector
213
+ handleCancelFileSelect();
214
+ } else {
215
+ const newQuery = newInput.substring(queryStart, queryEnd);
216
+ updateSearchQuery(newQuery);
217
+ }
218
+ } else if (slashPosition >= 0) {
219
+ const queryStart = slashPosition + 1;
220
+ const queryEnd = cursorPosition - 1;
221
+ if (queryEnd <= slashPosition) {
222
+ // Deleted / symbol, close command selector
223
+ handleCancelCommandSelect();
224
+ } else {
225
+ const newQuery = newInput.substring(queryStart, queryEnd);
226
+ updateCommandSearchQuery(newQuery);
227
+ }
228
+ } else if (exclamationPosition >= 0) {
229
+ const queryStart = exclamationPosition + 1;
230
+ const queryEnd = cursorPosition - 1;
231
+ if (queryEnd <= exclamationPosition) {
232
+ // Deleted ! symbol, close bash history selector
233
+ handleCancelBashHistorySelect();
234
+ } else {
235
+ const newQuery = newInput.substring(queryStart, queryEnd);
236
+ updateBashHistorySearchQuery(newQuery);
237
+ }
238
+ }
239
+ }
240
+ return;
241
+ }
242
+
243
+ // Arrow keys should be handled by selector components, no need to filter here
244
+ if (key.upArrow || key.downArrow) {
245
+ // Let selector component handle arrow key navigation
246
+ return;
247
+ }
248
+
249
+ if (
250
+ input &&
251
+ !key.ctrl &&
252
+ !("alt" in key && key.alt) &&
253
+ !key.meta &&
254
+ !key.return &&
255
+ !key.escape &&
256
+ !key.leftArrow &&
257
+ !key.rightArrow &&
258
+ !("home" in key && key.home) &&
259
+ !("end" in key && key.end)
260
+ ) {
261
+ // Handle character input for search
262
+ const char = input;
263
+ const newInput =
264
+ inputText.substring(0, cursorPosition) +
265
+ char +
266
+ inputText.substring(cursorPosition);
267
+ setInputText(newInput);
268
+ setCursorPosition(cursorPosition + input.length);
269
+
270
+ // Update search query
271
+ if (atPosition >= 0) {
272
+ const queryStart = atPosition + 1;
273
+ const queryEnd = cursorPosition + input.length;
274
+ const newQuery = newInput.substring(queryStart, queryEnd);
275
+ updateSearchQuery(newQuery);
276
+ } else if (slashPosition >= 0) {
277
+ const queryStart = slashPosition + 1;
278
+ const queryEnd = cursorPosition + input.length;
279
+ const newQuery = newInput.substring(queryStart, queryEnd);
280
+ updateCommandSearchQuery(newQuery);
281
+ } else if (exclamationPosition >= 0) {
282
+ const queryStart = exclamationPosition + 1;
283
+ const queryEnd = cursorPosition + input.length;
284
+ const newQuery = newInput.substring(queryStart, queryEnd);
285
+ updateBashHistorySearchQuery(newQuery);
286
+ }
287
+ }
288
+ },
289
+ [
290
+ inputText,
291
+ cursorPosition,
292
+ setInputText,
293
+ setCursorPosition,
294
+ atPosition,
295
+ slashPosition,
296
+ exclamationPosition,
297
+ handleCancelFileSelect,
298
+ handleCancelCommandSelect,
299
+ handleCancelBashHistorySelect,
300
+ updateSearchQuery,
301
+ updateCommandSearchQuery,
302
+ updateBashHistorySearchQuery,
303
+ ],
304
+ );
305
+
306
+ const handleNormalInput = useCallback(
307
+ async (input: string, key: Key) => {
308
+ if (key.return) {
309
+ // Prevent submission during loading or command execution
310
+ if (isLoading || isCommandRunning) {
311
+ return;
312
+ }
313
+
314
+ if (inputText.trim()) {
315
+ const trimmedInput = inputText.trim();
316
+
317
+ // Check if it's a memory message (starts with # and only one line)
318
+ if (trimmedInput.startsWith("#") && !trimmedInput.includes("\n")) {
319
+ // Activate memory type selector
320
+ activateMemoryTypeSelector(trimmedInput);
321
+ return;
322
+ }
323
+
324
+ // Extract image information
325
+ const imageRegex = /\[Image #(\d+)\]/g;
326
+ const matches = [...inputText.matchAll(imageRegex)];
327
+ const referencedImages = matches
328
+ .map((match) => {
329
+ const imageId = parseInt(match[1], 10);
330
+ return attachedImages.find((img) => img.id === imageId);
331
+ })
332
+ .filter(
333
+ (img): img is { id: number; path: string; mimeType: string } =>
334
+ img !== undefined,
335
+ )
336
+ .map((img) => ({ path: img.path, mimeType: img.mimeType }));
337
+
338
+ // Remove image placeholders, expand long text placeholders, send message
339
+ let cleanContent = inputText.replace(imageRegex, "").trim();
340
+ cleanContent = expandLongTextPlaceholders(cleanContent);
341
+
342
+ sendMessage(
343
+ cleanContent,
344
+ referencedImages.length > 0 ? referencedImages : undefined,
345
+ );
346
+ clearInput();
347
+ clearImages();
348
+ resetHistoryNavigation();
349
+
350
+ // Clear long text mapping
351
+ longTextMapRef.current.clear();
352
+ }
353
+ return;
354
+ }
355
+
356
+ if (key.escape) {
357
+ if (showFileSelector) {
358
+ handleCancelFileSelect();
359
+ } else if (showCommandSelector) {
360
+ handleCancelCommandSelect();
361
+ } else if (showBashHistorySelector) {
362
+ handleCancelBashHistorySelect();
363
+ }
364
+ return;
365
+ }
366
+
367
+ if (key.backspace || key.delete) {
368
+ if (cursorPosition > 0) {
369
+ deleteCharAtCursor();
370
+ resetHistoryNavigation();
371
+
372
+ // Check if we deleted any special characters
373
+ const newCursorPosition = cursorPosition - 1;
374
+ checkForAtDeletion(newCursorPosition);
375
+ checkForSlashDeletion(newCursorPosition);
376
+ checkForExclamationDeletion(newCursorPosition);
377
+ }
378
+ return;
379
+ }
380
+
381
+ if (key.leftArrow) {
382
+ moveCursorLeft();
383
+ return;
384
+ }
385
+
386
+ if (key.rightArrow) {
387
+ moveCursorRight();
388
+ return;
389
+ }
390
+
391
+ if (("home" in key && key.home) || (key.ctrl && input === "a")) {
392
+ moveCursorToStart();
393
+ return;
394
+ }
395
+
396
+ if (("end" in key && key.end) || (key.ctrl && input === "e")) {
397
+ moveCursorToEnd();
398
+ return;
399
+ }
400
+
401
+ // Handle Ctrl+V for pasting images
402
+ if (key.ctrl && input === "v") {
403
+ handlePasteImage().catch((error) => {
404
+ console.warn("Failed to handle paste image:", error);
405
+ });
406
+ return;
407
+ }
408
+
409
+ // Handle up/down keys for history navigation (only when no selector is active)
410
+ if (
411
+ key.upArrow &&
412
+ !showFileSelector &&
413
+ !showCommandSelector &&
414
+ !showBashHistorySelector
415
+ ) {
416
+ const { newInput, newCursorPosition } = navigateHistory(
417
+ "up",
418
+ inputText,
419
+ );
420
+ setInputText(newInput);
421
+ setCursorPosition(newCursorPosition);
422
+ return;
423
+ }
424
+
425
+ if (
426
+ key.downArrow &&
427
+ !showFileSelector &&
428
+ !showCommandSelector &&
429
+ !showBashHistorySelector
430
+ ) {
431
+ const { newInput, newCursorPosition } = navigateHistory(
432
+ "down",
433
+ inputText,
434
+ );
435
+ setInputText(newInput);
436
+ setCursorPosition(newCursorPosition);
437
+ return;
438
+ }
439
+
440
+ // Handle typing input
441
+ if (
442
+ input &&
443
+ !key.ctrl &&
444
+ !("alt" in key && key.alt) &&
445
+ !key.meta &&
446
+ !key.return &&
447
+ !key.escape &&
448
+ !key.backspace &&
449
+ !key.delete &&
450
+ !key.leftArrow &&
451
+ !key.rightArrow &&
452
+ !("home" in key && key.home) &&
453
+ !("end" in key && key.end)
454
+ ) {
455
+ const inputString = input;
456
+
457
+ // Detect if it's a paste operation (input contains multiple characters or newlines)
458
+ const isPasteOperation =
459
+ inputString.length > 1 ||
460
+ inputString.includes("\n") ||
461
+ inputString.includes("\r");
462
+
463
+ if (isPasteOperation) {
464
+ logger.debug("[InputBox] 🔍 Detected paste operation:", {
465
+ inputLength: inputString.length,
466
+ input:
467
+ inputString.substring(0, 50) +
468
+ (inputString.length > 50 ? "..." : ""),
469
+ hasNewlines:
470
+ inputString.includes("\n") || inputString.includes("\r"),
471
+ });
472
+
473
+ // Start or continue the debounce handling for paste operation
474
+ if (!pasteDebounceRef.current.isPasting) {
475
+ // Start new paste operation
476
+ logger.debug(
477
+ "[InputBox] 🚀 Starting new paste operation - initializing debounce buffer",
478
+ );
479
+ pasteDebounceRef.current.isPasting = true;
480
+ pasteDebounceRef.current.buffer = inputString;
481
+ pasteDebounceRef.current.initialCursorPosition = cursorPosition;
482
+ } else {
483
+ // Continue paste operation, add new input to buffer
484
+ logger.debug("[InputBox] 📝 Merging paste content to buffer:", {
485
+ previousBufferLength: pasteDebounceRef.current.buffer.length,
486
+ newInputLength: inputString.length,
487
+ newTotalLength:
488
+ pasteDebounceRef.current.buffer.length + inputString.length,
489
+ });
490
+ pasteDebounceRef.current.buffer += inputString;
491
+ }
492
+
493
+ // Clear previous timer
494
+ if (pasteDebounceRef.current.timer) {
495
+ logger.debug(
496
+ "[InputBox] ⏰ Clearing previous debounce timer, resetting 30ms delay",
497
+ );
498
+ clearTimeout(pasteDebounceRef.current.timer);
499
+ }
500
+
501
+ // Set new 30ms timer
502
+ pasteDebounceRef.current.timer = setTimeout(() => {
503
+ logger.debug(
504
+ "[InputBox] ✅ Debounce complete - processing merged paste content:",
505
+ {
506
+ finalBufferLength: pasteDebounceRef.current.buffer.length,
507
+ content:
508
+ pasteDebounceRef.current.buffer.substring(0, 100) +
509
+ (pasteDebounceRef.current.buffer.length > 100 ? "..." : ""),
510
+ },
511
+ );
512
+
513
+ // Process all paste content in buffer
514
+ let processedInput = pasteDebounceRef.current.buffer.replace(
515
+ /\r/g,
516
+ "\n",
517
+ );
518
+
519
+ // Check if long text compression is needed (over 200 characters)
520
+ if (processedInput.length > 200) {
521
+ const originalText = processedInput;
522
+ const compressedLabel = generateCompressedText(originalText);
523
+ logger.info(
524
+ "[InputBox] 📦 Long text compression: originalLength:",
525
+ originalText.length,
526
+ "compressedLabel:",
527
+ compressedLabel,
528
+ "preview:",
529
+ originalText.substring(0, 50) + "...",
530
+ );
531
+ processedInput = compressedLabel;
532
+ }
533
+
534
+ insertTextAtCursor(processedInput);
535
+ resetHistoryNavigation();
536
+
537
+ // Reset paste state
538
+ pasteDebounceRef.current.isPasting = false;
539
+ pasteDebounceRef.current.buffer = "";
540
+ pasteDebounceRef.current.timer = null;
541
+
542
+ logger.debug(
543
+ "[InputBox] 🎯 Paste debounce processing complete, state reset",
544
+ );
545
+ }, 30);
546
+ } else {
547
+ // Handle single character input
548
+ let char = inputString;
549
+
550
+ // Check if it's Chinese exclamation mark, convert to English if at beginning
551
+ if (char === "!" && cursorPosition === 0) {
552
+ char = "!";
553
+ }
554
+
555
+ // First update input text and cursor position
556
+ const newInputText =
557
+ inputText.substring(0, cursorPosition) +
558
+ char +
559
+ inputText.substring(cursorPosition);
560
+ const newCursorPosition = cursorPosition + char.length;
561
+
562
+ insertTextAtCursor(char);
563
+ resetHistoryNavigation();
564
+
565
+ // Check special characters and set corresponding selectors
566
+ if (char === "@") {
567
+ activateFileSelector(cursorPosition);
568
+ } else if (char === "/") {
569
+ activateCommandSelector(cursorPosition);
570
+ } else if (char === "!" && cursorPosition === 0) {
571
+ // ! must be the first character to trigger bash selector
572
+ activateBashHistorySelector(cursorPosition);
573
+ } else if (char === "#" && cursorPosition === 0) {
574
+ // # at beginning position, will be auto-detected as memory message when sent
575
+ logger.debug(
576
+ "[InputBox] 📝 Memory message detection, input starts with #",
577
+ );
578
+ } else if (showFileSelector && atPosition >= 0) {
579
+ // Update search query
580
+ const queryStart = atPosition + 1;
581
+ const queryEnd = newCursorPosition;
582
+ const newQuery = newInputText.substring(queryStart, queryEnd);
583
+ updateSearchQuery(newQuery);
584
+ } else if (showCommandSelector && slashPosition >= 0) {
585
+ // Update command search query
586
+ const queryStart = slashPosition + 1;
587
+ const queryEnd = newCursorPosition;
588
+ const newQuery = newInputText.substring(queryStart, queryEnd);
589
+ updateCommandSearchQuery(newQuery);
590
+ } else if (showBashHistorySelector && exclamationPosition >= 0) {
591
+ // Update bash history search query
592
+ const queryStart = exclamationPosition + 1;
593
+ const queryEnd = newCursorPosition;
594
+ const newQuery = newInputText.substring(queryStart, queryEnd);
595
+ updateBashHistorySearchQuery(newQuery);
596
+ }
597
+ }
598
+ }
599
+ },
600
+ [
601
+ inputText,
602
+ cursorPosition,
603
+ sendMessage,
604
+ clearInput,
605
+ resetHistoryNavigation,
606
+ showFileSelector,
607
+ showCommandSelector,
608
+ showBashHistorySelector,
609
+ handleCancelFileSelect,
610
+ handleCancelCommandSelect,
611
+ handleCancelBashHistorySelect,
612
+ deleteCharAtCursor,
613
+ checkForAtDeletion,
614
+ checkForSlashDeletion,
615
+ checkForExclamationDeletion,
616
+ moveCursorLeft,
617
+ moveCursorRight,
618
+ moveCursorToStart,
619
+ moveCursorToEnd,
620
+ navigateHistory,
621
+ setInputText,
622
+ setCursorPosition,
623
+ insertTextAtCursor,
624
+ activateFileSelector,
625
+ activateCommandSelector,
626
+ activateBashHistorySelector,
627
+ atPosition,
628
+ slashPosition,
629
+ exclamationPosition,
630
+ updateSearchQuery,
631
+ updateCommandSearchQuery,
632
+ updateBashHistorySearchQuery,
633
+ attachedImages,
634
+ clearImages,
635
+ handlePasteImage,
636
+ activateMemoryTypeSelector,
637
+ isLoading,
638
+ isCommandRunning,
639
+ ],
640
+ );
641
+
642
+ useInput((input, key) => {
643
+ // Handle interrupt request - use Esc key to interrupt AI request or command
644
+ if (key.escape && (isLoading || isCommandRunning)) {
645
+ // Unified interrupt for AI message generation and command execution
646
+ if (typeof abortMessage === "function") {
647
+ abortMessage();
648
+ }
649
+ return;
650
+ }
651
+
652
+ // During loading or command execution, except for Esc key, other input operations continue normally
653
+ // but will prevent Enter submission in handleNormalInput
654
+
655
+ if (
656
+ showFileSelector ||
657
+ showCommandSelector ||
658
+ showBashHistorySelector ||
659
+ showMemoryTypeSelector ||
660
+ showBashManager ||
661
+ showMcpManager
662
+ ) {
663
+ if (showMemoryTypeSelector || showBashManager || showMcpManager) {
664
+ // Memory type selector, bash manager and MCP manager don't need to handle input, handled by component itself
665
+ return;
666
+ }
667
+ handleSelectorInput(input, key);
668
+ } else {
669
+ handleNormalInput(input, key);
670
+ }
671
+ });
672
+
673
+ return {
674
+ handleFileSelect: useCallback(
675
+ (filePath: string) => {
676
+ const { newInput, newCursorPosition } = handleFileSelect(
677
+ filePath,
678
+ inputText,
679
+ cursorPosition,
680
+ );
681
+ setInputText(newInput);
682
+ setCursorPosition(newCursorPosition);
683
+ },
684
+ [
685
+ handleFileSelect,
686
+ inputText,
687
+ cursorPosition,
688
+ setInputText,
689
+ setCursorPosition,
690
+ ],
691
+ ),
692
+
693
+ handleCommandSelect: useCallback(
694
+ (command: string) => {
695
+ const { newInput, newCursorPosition } = handleCommandSelect(
696
+ command,
697
+ inputText,
698
+ cursorPosition,
699
+ );
700
+ setInputText(newInput);
701
+ setCursorPosition(newCursorPosition);
702
+ },
703
+ [
704
+ handleCommandSelect,
705
+ inputText,
706
+ cursorPosition,
707
+ setInputText,
708
+ setCursorPosition,
709
+ ],
710
+ ),
711
+
712
+ handleCommandInsert: useCallback(
713
+ (command: string) => {
714
+ const { newInput, newCursorPosition } = handleCommandInsert(
715
+ command,
716
+ inputText,
717
+ cursorPosition,
718
+ );
719
+ setInputText(newInput);
720
+ setCursorPosition(newCursorPosition);
721
+ },
722
+ [
723
+ handleCommandInsert,
724
+ inputText,
725
+ cursorPosition,
726
+ setInputText,
727
+ setCursorPosition,
728
+ ],
729
+ ),
730
+
731
+ handleBashHistorySelect: useCallback(
732
+ (command: string) => {
733
+ const { newInput, newCursorPosition } = handleBashHistorySelect(
734
+ command,
735
+ inputText,
736
+ cursorPosition,
737
+ );
738
+ setInputText(newInput);
739
+ setCursorPosition(newCursorPosition);
740
+ },
741
+ [
742
+ handleBashHistorySelect,
743
+ inputText,
744
+ cursorPosition,
745
+ setInputText,
746
+ setCursorPosition,
747
+ ],
748
+ ),
749
+
750
+ handleBashHistoryExecute: useCallback(
751
+ (command: string) => {
752
+ const commandToExecute = handleBashHistoryExecute(command);
753
+ // Clear input box and execute command, ensure command starts with !
754
+ const bashCommand = commandToExecute.startsWith("!")
755
+ ? commandToExecute
756
+ : `!${commandToExecute}`;
757
+ setInputText("");
758
+ setCursorPosition(0);
759
+ sendMessage(bashCommand);
760
+ },
761
+ [handleBashHistoryExecute, setInputText, setCursorPosition, sendMessage],
762
+ ),
763
+
764
+ handleMemoryTypeSelect: useCallback(
765
+ async (type: "project" | "user") => {
766
+ const currentMessage = inputText.trim();
767
+ if (currentMessage.startsWith("#")) {
768
+ await saveMemory(currentMessage, type);
769
+ }
770
+ // Call the handler function from useMemoryTypeSelector to close the selector
771
+ handleMemoryTypeSelect(type);
772
+ // Clear input box
773
+ clearInput();
774
+ },
775
+ [inputText, saveMemory, handleMemoryTypeSelect, clearInput],
776
+ ),
777
+ };
778
+ };