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.
- package/README.md +120 -0
- package/bin/wave-code.js +16 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/components/App.d.ts +8 -0
- package/dist/components/App.d.ts.map +1 -0
- package/dist/components/App.js +10 -0
- package/dist/components/BashHistorySelector.d.ts +10 -0
- package/dist/components/BashHistorySelector.d.ts.map +1 -0
- package/dist/components/BashHistorySelector.js +83 -0
- package/dist/components/BashShellManager.d.ts +6 -0
- package/dist/components/BashShellManager.d.ts.map +1 -0
- package/dist/components/BashShellManager.js +116 -0
- package/dist/components/ChatInterface.d.ts +3 -0
- package/dist/components/ChatInterface.d.ts.map +1 -0
- package/dist/components/ChatInterface.js +31 -0
- package/dist/components/CommandOutputDisplay.d.ts +9 -0
- package/dist/components/CommandOutputDisplay.d.ts.map +1 -0
- package/dist/components/CommandOutputDisplay.js +40 -0
- package/dist/components/CommandSelector.d.ts +11 -0
- package/dist/components/CommandSelector.d.ts.map +1 -0
- package/dist/components/CommandSelector.js +60 -0
- package/dist/components/CompressDisplay.d.ts +9 -0
- package/dist/components/CompressDisplay.d.ts.map +1 -0
- package/dist/components/CompressDisplay.js +17 -0
- package/dist/components/DiffViewer.d.ts +9 -0
- package/dist/components/DiffViewer.d.ts.map +1 -0
- package/dist/components/DiffViewer.js +221 -0
- package/dist/components/FileSelector.d.ts +13 -0
- package/dist/components/FileSelector.d.ts.map +1 -0
- package/dist/components/FileSelector.js +48 -0
- package/dist/components/InputBox.d.ts +23 -0
- package/dist/components/InputBox.d.ts.map +1 -0
- package/dist/components/InputBox.js +124 -0
- package/dist/components/McpManager.d.ts +10 -0
- package/dist/components/McpManager.d.ts.map +1 -0
- package/dist/components/McpManager.js +123 -0
- package/dist/components/MemoryDisplay.d.ts +8 -0
- package/dist/components/MemoryDisplay.d.ts.map +1 -0
- package/dist/components/MemoryDisplay.js +25 -0
- package/dist/components/MemoryTypeSelector.d.ts +8 -0
- package/dist/components/MemoryTypeSelector.d.ts.map +1 -0
- package/dist/components/MemoryTypeSelector.js +38 -0
- package/dist/components/MessageList.d.ts +12 -0
- package/dist/components/MessageList.d.ts.map +1 -0
- package/dist/components/MessageList.js +36 -0
- package/dist/components/ToolResultDisplay.d.ts +9 -0
- package/dist/components/ToolResultDisplay.d.ts.map +1 -0
- package/dist/components/ToolResultDisplay.js +52 -0
- package/dist/contexts/useAppConfig.d.ts +11 -0
- package/dist/contexts/useAppConfig.d.ts.map +1 -0
- package/dist/contexts/useAppConfig.js +13 -0
- package/dist/contexts/useChat.d.ts +36 -0
- package/dist/contexts/useChat.d.ts.map +1 -0
- package/dist/contexts/useChat.js +208 -0
- package/dist/hooks/useBashHistorySelector.d.ts +15 -0
- package/dist/hooks/useBashHistorySelector.d.ts.map +1 -0
- package/dist/hooks/useBashHistorySelector.js +61 -0
- package/dist/hooks/useCommandSelector.d.ts +24 -0
- package/dist/hooks/useCommandSelector.d.ts.map +1 -0
- package/dist/hooks/useCommandSelector.js +98 -0
- package/dist/hooks/useFileSelector.d.ts +16 -0
- package/dist/hooks/useFileSelector.d.ts.map +1 -0
- package/dist/hooks/useFileSelector.js +174 -0
- package/dist/hooks/useImageManager.d.ts +13 -0
- package/dist/hooks/useImageManager.d.ts.map +1 -0
- package/dist/hooks/useImageManager.js +46 -0
- package/dist/hooks/useInputHistory.d.ts +11 -0
- package/dist/hooks/useInputHistory.d.ts.map +1 -0
- package/dist/hooks/useInputHistory.js +64 -0
- package/dist/hooks/useInputKeyboardHandler.d.ts +83 -0
- package/dist/hooks/useInputKeyboardHandler.d.ts.map +1 -0
- package/dist/hooks/useInputKeyboardHandler.js +507 -0
- package/dist/hooks/useInputState.d.ts +14 -0
- package/dist/hooks/useInputState.d.ts.map +1 -0
- package/dist/hooks/useInputState.js +57 -0
- package/dist/hooks/useMemoryTypeSelector.d.ts +9 -0
- package/dist/hooks/useMemoryTypeSelector.d.ts.map +1 -0
- package/dist/hooks/useMemoryTypeSelector.js +27 -0
- package/dist/hooks/usePagination.d.ts +20 -0
- package/dist/hooks/usePagination.d.ts.map +1 -0
- package/dist/hooks/usePagination.js +168 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/plain-cli.d.ts +7 -0
- package/dist/plain-cli.d.ts.map +1 -0
- package/dist/plain-cli.js +49 -0
- package/dist/utils/clipboard.d.ts +22 -0
- package/dist/utils/clipboard.d.ts.map +1 -0
- package/dist/utils/clipboard.js +347 -0
- package/dist/utils/constants.d.ts +17 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +18 -0
- package/dist/utils/logger.d.ts +72 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +245 -0
- package/package.json +60 -0
- package/src/cli.tsx +82 -0
- package/src/components/App.tsx +31 -0
- package/src/components/BashHistorySelector.tsx +163 -0
- package/src/components/BashShellManager.tsx +306 -0
- package/src/components/ChatInterface.tsx +88 -0
- package/src/components/CommandOutputDisplay.tsx +81 -0
- package/src/components/CommandSelector.tsx +144 -0
- package/src/components/CompressDisplay.tsx +58 -0
- package/src/components/DiffViewer.tsx +321 -0
- package/src/components/FileSelector.tsx +137 -0
- package/src/components/InputBox.tsx +310 -0
- package/src/components/McpManager.tsx +328 -0
- package/src/components/MemoryDisplay.tsx +62 -0
- package/src/components/MemoryTypeSelector.tsx +96 -0
- package/src/components/MessageList.tsx +215 -0
- package/src/components/ToolResultDisplay.tsx +138 -0
- package/src/contexts/useAppConfig.tsx +32 -0
- package/src/contexts/useChat.tsx +300 -0
- package/src/hooks/useBashHistorySelector.ts +77 -0
- package/src/hooks/useCommandSelector.ts +131 -0
- package/src/hooks/useFileSelector.ts +227 -0
- package/src/hooks/useImageManager.ts +64 -0
- package/src/hooks/useInputHistory.ts +74 -0
- package/src/hooks/useInputKeyboardHandler.ts +778 -0
- package/src/hooks/useInputState.ts +66 -0
- package/src/hooks/useMemoryTypeSelector.ts +40 -0
- package/src/hooks/usePagination.ts +203 -0
- package/src/index.ts +108 -0
- package/src/plain-cli.ts +66 -0
- package/src/utils/clipboard.ts +384 -0
- package/src/utils/constants.ts +22 -0
- 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
|
+
};
|