wave-code 0.8.3 → 0.9.0
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/dist/components/ChatInterface.d.ts.map +1 -1
- package/dist/components/ChatInterface.js +26 -7
- package/dist/components/HelpView.d.ts.map +1 -1
- package/dist/components/HelpView.js +2 -0
- package/dist/components/InputBox.js +4 -3
- package/dist/components/MessageBlockItem.d.ts.map +1 -1
- package/dist/components/MessageBlockItem.js +1 -1
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +14 -4
- package/dist/components/QueuedMessageList.d.ts +3 -0
- package/dist/components/QueuedMessageList.d.ts.map +1 -0
- package/dist/components/QueuedMessageList.js +17 -0
- package/dist/components/ReasoningDisplay.d.ts +1 -0
- package/dist/components/ReasoningDisplay.d.ts.map +1 -1
- package/dist/components/ReasoningDisplay.js +3 -3
- package/dist/components/RewindCommand.d.ts.map +1 -1
- package/dist/components/RewindCommand.js +10 -4
- package/dist/components/ToolDisplay.js +1 -1
- package/dist/contexts/useChat.d.ts +7 -0
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +17 -1
- package/dist/hooks/useInputManager.d.ts +3 -3
- package/dist/hooks/useInputManager.d.ts.map +1 -1
- package/dist/hooks/useInputManager.js +10 -9
- package/dist/managers/inputHandlers.d.ts +7 -4
- package/dist/managers/inputHandlers.d.ts.map +1 -1
- package/dist/managers/inputHandlers.js +165 -42
- package/package.json +2 -2
- package/src/components/ChatInterface.tsx +42 -15
- package/src/components/HelpView.tsx +2 -0
- package/src/components/InputBox.tsx +17 -17
- package/src/components/MessageBlockItem.tsx +8 -3
- package/src/components/MessageList.tsx +16 -3
- package/src/components/QueuedMessageList.tsx +31 -0
- package/src/components/ReasoningDisplay.tsx +8 -2
- package/src/components/RewindCommand.tsx +21 -5
- package/src/components/ToolDisplay.tsx +2 -2
- package/src/contexts/useChat.tsx +29 -1
- package/src/hooks/useInputManager.ts +9 -21
- package/src/managers/inputHandlers.ts +197 -56
|
@@ -13,10 +13,7 @@ export const expandLongTextPlaceholders = (text, longTextMap) => {
|
|
|
13
13
|
}
|
|
14
14
|
return expandedText;
|
|
15
15
|
};
|
|
16
|
-
export const handleSubmit = async (state, dispatch, callbacks,
|
|
17
|
-
if (isLoading || isCommandRunning) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
16
|
+
export const handleSubmit = async (state, dispatch, callbacks, attachedImagesOverride) => {
|
|
20
17
|
if (state.inputText.trim()) {
|
|
21
18
|
const imageRegex = /\[Image #(\d+)\]/g;
|
|
22
19
|
const matches = [...state.inputText.matchAll(imageRegex)];
|
|
@@ -68,6 +65,55 @@ export const cyclePermissionMode = (currentMode, dispatch, callbacks) => {
|
|
|
68
65
|
dispatch({ type: "SET_PERMISSION_MODE", payload: nextMode });
|
|
69
66
|
callbacks.onPermissionModeChange?.(nextMode);
|
|
70
67
|
};
|
|
68
|
+
const SELECTOR_TRIGGERS = [
|
|
69
|
+
{
|
|
70
|
+
char: "@",
|
|
71
|
+
type: "ACTIVATE_FILE_SELECTOR",
|
|
72
|
+
shouldActivate: (char, pos, text) => char === "@" && (pos === 1 || /\s/.test(text[pos - 2])),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
char: "/",
|
|
76
|
+
type: "ACTIVATE_COMMAND_SELECTOR",
|
|
77
|
+
shouldActivate: (char, pos, text, state) => char === "/" &&
|
|
78
|
+
!state.showFileSelector &&
|
|
79
|
+
(pos === 1 || /\s/.test(text[pos - 2])),
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
const getProjectedState = (state, char) => {
|
|
83
|
+
const beforeCursor = state.inputText.substring(0, state.cursorPosition);
|
|
84
|
+
const afterCursor = state.inputText.substring(state.cursorPosition);
|
|
85
|
+
const newInputText = beforeCursor + char + afterCursor;
|
|
86
|
+
const newCursorPosition = state.cursorPosition + char.length;
|
|
87
|
+
return { newInputText, newCursorPosition };
|
|
88
|
+
};
|
|
89
|
+
export const getAtSelectorPosition = (text, cursorPosition) => {
|
|
90
|
+
let i = cursorPosition - 1;
|
|
91
|
+
while (i >= 0 && !/\s/.test(text[i])) {
|
|
92
|
+
if (text[i] === "@") {
|
|
93
|
+
// Check if this @ is at the start or preceded by whitespace
|
|
94
|
+
if (i === 0 || /\s/.test(text[i - 1])) {
|
|
95
|
+
return i;
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
i--;
|
|
100
|
+
}
|
|
101
|
+
return -1;
|
|
102
|
+
};
|
|
103
|
+
export const getSlashSelectorPosition = (text, cursorPosition) => {
|
|
104
|
+
let i = cursorPosition - 1;
|
|
105
|
+
while (i >= 0 && !/\s/.test(text[i])) {
|
|
106
|
+
if (text[i] === "/") {
|
|
107
|
+
// Check if this / is at the start or preceded by whitespace
|
|
108
|
+
if (i === 0 || /\s/.test(text[i - 1])) {
|
|
109
|
+
return i;
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
i--;
|
|
114
|
+
}
|
|
115
|
+
return -1;
|
|
116
|
+
};
|
|
71
117
|
export const updateSearchQueriesForActiveSelectors = (state, dispatch, inputText, cursorPosition) => {
|
|
72
118
|
if (state.showFileSelector && state.atPosition >= 0) {
|
|
73
119
|
const queryStart = state.atPosition + 1;
|
|
@@ -82,18 +128,45 @@ export const updateSearchQueriesForActiveSelectors = (state, dispatch, inputText
|
|
|
82
128
|
dispatch({ type: "SET_COMMAND_SEARCH_QUERY", payload: newQuery });
|
|
83
129
|
}
|
|
84
130
|
};
|
|
85
|
-
export const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
else if (char === "/" && !state.showFileSelector && cursorPosition === 1) {
|
|
131
|
+
export const processSelectorInput = (state, dispatch, char) => {
|
|
132
|
+
const { newInputText, newCursorPosition } = getProjectedState(state, char);
|
|
133
|
+
const trigger = SELECTOR_TRIGGERS.find((t) => t.shouldActivate(char, newCursorPosition, newInputText, state));
|
|
134
|
+
if (trigger) {
|
|
90
135
|
dispatch({
|
|
91
|
-
type:
|
|
92
|
-
payload:
|
|
136
|
+
type: trigger.type,
|
|
137
|
+
payload: newCursorPosition - 1,
|
|
93
138
|
});
|
|
94
139
|
}
|
|
95
140
|
else {
|
|
96
|
-
|
|
141
|
+
const atPos = getAtSelectorPosition(newInputText, newCursorPosition);
|
|
142
|
+
let showFileSelector = state.showFileSelector;
|
|
143
|
+
let atPosition = state.atPosition;
|
|
144
|
+
if (atPos !== -1 && !state.showFileSelector) {
|
|
145
|
+
dispatch({
|
|
146
|
+
type: "ACTIVATE_FILE_SELECTOR",
|
|
147
|
+
payload: atPos,
|
|
148
|
+
});
|
|
149
|
+
showFileSelector = true;
|
|
150
|
+
atPosition = atPos;
|
|
151
|
+
}
|
|
152
|
+
const slashPos = getSlashSelectorPosition(newInputText, newCursorPosition);
|
|
153
|
+
let showCommandSelector = state.showCommandSelector;
|
|
154
|
+
let slashPosition = state.slashPosition;
|
|
155
|
+
if (slashPos !== -1 && !state.showCommandSelector) {
|
|
156
|
+
dispatch({
|
|
157
|
+
type: "ACTIVATE_COMMAND_SELECTOR",
|
|
158
|
+
payload: slashPos,
|
|
159
|
+
});
|
|
160
|
+
showCommandSelector = true;
|
|
161
|
+
slashPosition = slashPos;
|
|
162
|
+
}
|
|
163
|
+
updateSearchQueriesForActiveSelectors({
|
|
164
|
+
...state,
|
|
165
|
+
showFileSelector,
|
|
166
|
+
atPosition,
|
|
167
|
+
showCommandSelector,
|
|
168
|
+
slashPosition,
|
|
169
|
+
}, dispatch, newInputText, newCursorPosition);
|
|
97
170
|
}
|
|
98
171
|
};
|
|
99
172
|
export const handlePasteInput = (state, dispatch, callbacks, input) => {
|
|
@@ -119,19 +192,22 @@ export const handlePasteInput = (state, dispatch, callbacks, input) => {
|
|
|
119
192
|
}
|
|
120
193
|
callbacks.onResetHistoryNavigation?.();
|
|
121
194
|
dispatch({ type: "INSERT_TEXT", payload: char });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
195
|
+
processSelectorInput(state, dispatch, char);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
export const getWordEnd = (text, startPos) => {
|
|
199
|
+
let i = startPos;
|
|
200
|
+
while (i < text.length && !/\s/.test(text[i])) {
|
|
201
|
+
i++;
|
|
128
202
|
}
|
|
203
|
+
return i;
|
|
129
204
|
};
|
|
130
205
|
export const handleCommandSelect = (state, dispatch, callbacks, command) => {
|
|
131
206
|
if (state.slashPosition >= 0) {
|
|
207
|
+
const wordEnd = getWordEnd(state.inputText, state.slashPosition);
|
|
132
208
|
const beforeSlash = state.inputText.substring(0, state.slashPosition);
|
|
133
|
-
const
|
|
134
|
-
const newInput = beforeSlash +
|
|
209
|
+
const afterWord = state.inputText.substring(wordEnd);
|
|
210
|
+
const newInput = beforeSlash + afterWord;
|
|
135
211
|
const newCursorPosition = beforeSlash.length;
|
|
136
212
|
dispatch({ type: "SET_INPUT_TEXT", payload: newInput });
|
|
137
213
|
dispatch({ type: "SET_CURSOR_POSITION", payload: newCursorPosition });
|
|
@@ -178,10 +254,11 @@ export const handleCommandSelect = (state, dispatch, callbacks, command) => {
|
|
|
178
254
|
};
|
|
179
255
|
export const handleFileSelect = (state, dispatch, callbacks, filePath) => {
|
|
180
256
|
if (state.atPosition >= 0) {
|
|
257
|
+
const wordEnd = getWordEnd(state.inputText, state.atPosition);
|
|
181
258
|
const beforeAt = state.inputText.substring(0, state.atPosition);
|
|
182
|
-
const
|
|
183
|
-
const newInput = beforeAt +
|
|
184
|
-
const newCursorPosition = beforeAt.length + filePath.length +
|
|
259
|
+
const afterWord = state.inputText.substring(wordEnd);
|
|
260
|
+
const newInput = beforeAt + `@${filePath} ` + afterWord;
|
|
261
|
+
const newCursorPosition = beforeAt.length + filePath.length + 2;
|
|
185
262
|
dispatch({ type: "SET_INPUT_TEXT", payload: newInput });
|
|
186
263
|
dispatch({ type: "SET_CURSOR_POSITION", payload: newCursorPosition });
|
|
187
264
|
dispatch({ type: "CANCEL_FILE_SELECTOR" });
|
|
@@ -224,6 +301,23 @@ export const handleSelectorInput = (state, dispatch, callbacks, input, key) => {
|
|
|
224
301
|
if (key.upArrow || key.downArrow || key.return || key.tab) {
|
|
225
302
|
return true;
|
|
226
303
|
}
|
|
304
|
+
if (key.leftArrow) {
|
|
305
|
+
const newCursorPosition = state.cursorPosition - 1;
|
|
306
|
+
dispatch({ type: "MOVE_CURSOR", payload: -1 });
|
|
307
|
+
checkForAtDeletion(state, dispatch, newCursorPosition);
|
|
308
|
+
checkForSlashDeletion(state, dispatch, newCursorPosition);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (key.rightArrow) {
|
|
312
|
+
const newCursorPosition = state.cursorPosition + 1;
|
|
313
|
+
dispatch({ type: "MOVE_CURSOR", payload: 1 });
|
|
314
|
+
checkForAtDeletion(state, dispatch, newCursorPosition);
|
|
315
|
+
checkForSlashDeletion(state, dispatch, newCursorPosition);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
if (input === " " && state.showFileSelector) {
|
|
319
|
+
dispatch({ type: "CANCEL_FILE_SELECTOR" });
|
|
320
|
+
}
|
|
227
321
|
if (input &&
|
|
228
322
|
!key.ctrl &&
|
|
229
323
|
!("alt" in key && key.alt) &&
|
|
@@ -236,19 +330,14 @@ export const handleSelectorInput = (state, dispatch, callbacks, input, key) => {
|
|
|
236
330
|
!("home" in key && key.home) &&
|
|
237
331
|
!("end" in key && key.end)) {
|
|
238
332
|
dispatch({ type: "INSERT_TEXT", payload: input });
|
|
239
|
-
|
|
240
|
-
const newCursorPosition = state.cursorPosition + input.length;
|
|
241
|
-
const beforeCursor = state.inputText.substring(0, state.cursorPosition);
|
|
242
|
-
const afterCursor = state.inputText.substring(state.cursorPosition);
|
|
243
|
-
const newInputText = beforeCursor + input + afterCursor;
|
|
244
|
-
handleSpecialCharInput(state, dispatch, input, newCursorPosition, newInputText);
|
|
333
|
+
processSelectorInput(state, dispatch, input);
|
|
245
334
|
return true;
|
|
246
335
|
}
|
|
247
336
|
return false;
|
|
248
337
|
};
|
|
249
|
-
export const handleNormalInput = async (state, dispatch, callbacks, input, key,
|
|
338
|
+
export const handleNormalInput = async (state, dispatch, callbacks, input, key, clearImages) => {
|
|
250
339
|
if (key.return) {
|
|
251
|
-
await handleSubmit(state, dispatch, callbacks
|
|
340
|
+
await handleSubmit(state, dispatch, callbacks);
|
|
252
341
|
clearImages?.();
|
|
253
342
|
return true;
|
|
254
343
|
}
|
|
@@ -264,10 +353,45 @@ export const handleNormalInput = async (state, dispatch, callbacks, input, key,
|
|
|
264
353
|
if (key.backspace || key.delete) {
|
|
265
354
|
if (state.cursorPosition > 0) {
|
|
266
355
|
const newCursorPosition = state.cursorPosition - 1;
|
|
356
|
+
const beforeCursor = state.inputText.substring(0, state.cursorPosition - 1);
|
|
357
|
+
const afterCursor = state.inputText.substring(state.cursorPosition);
|
|
358
|
+
const newInputText = beforeCursor + afterCursor;
|
|
267
359
|
dispatch({ type: "DELETE_CHAR" });
|
|
268
360
|
callbacks.onResetHistoryNavigation?.();
|
|
269
361
|
checkForAtDeletion(state, dispatch, newCursorPosition);
|
|
270
362
|
checkForSlashDeletion(state, dispatch, newCursorPosition);
|
|
363
|
+
// Reactivate file selector if cursor is now within an @word
|
|
364
|
+
const atPos = getAtSelectorPosition(newInputText, newCursorPosition);
|
|
365
|
+
let showFileSelector = state.showFileSelector;
|
|
366
|
+
let atPosition = state.atPosition;
|
|
367
|
+
if (atPos !== -1 && !state.showFileSelector) {
|
|
368
|
+
dispatch({
|
|
369
|
+
type: "ACTIVATE_FILE_SELECTOR",
|
|
370
|
+
payload: atPos,
|
|
371
|
+
});
|
|
372
|
+
showFileSelector = true;
|
|
373
|
+
atPosition = atPos;
|
|
374
|
+
}
|
|
375
|
+
const slashPos = getSlashSelectorPosition(newInputText, newCursorPosition);
|
|
376
|
+
let showCommandSelector = state.showCommandSelector;
|
|
377
|
+
let slashPosition = state.slashPosition;
|
|
378
|
+
if (slashPos !== -1 && !state.showCommandSelector) {
|
|
379
|
+
dispatch({
|
|
380
|
+
type: "ACTIVATE_COMMAND_SELECTOR",
|
|
381
|
+
payload: slashPos,
|
|
382
|
+
});
|
|
383
|
+
showCommandSelector = true;
|
|
384
|
+
slashPosition = slashPos;
|
|
385
|
+
}
|
|
386
|
+
updateSearchQueriesForActiveSelectors({
|
|
387
|
+
...state,
|
|
388
|
+
inputText: newInputText,
|
|
389
|
+
cursorPosition: newCursorPosition,
|
|
390
|
+
showFileSelector,
|
|
391
|
+
atPosition,
|
|
392
|
+
showCommandSelector,
|
|
393
|
+
slashPosition,
|
|
394
|
+
}, dispatch, newInputText, newCursorPosition);
|
|
271
395
|
}
|
|
272
396
|
return true;
|
|
273
397
|
}
|
|
@@ -310,20 +434,19 @@ export const handleNormalInput = async (state, dispatch, callbacks, input, key,
|
|
|
310
434
|
}
|
|
311
435
|
return false;
|
|
312
436
|
};
|
|
313
|
-
export const handleInput = async (state, dispatch, callbacks, input, key,
|
|
437
|
+
export const handleInput = async (state, dispatch, callbacks, input, key, clearImages) => {
|
|
314
438
|
if (state.selectorJustUsed) {
|
|
315
439
|
return true;
|
|
316
440
|
}
|
|
317
441
|
if (key.escape) {
|
|
318
|
-
if ((
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
state.showStatusCommand)) {
|
|
442
|
+
if (!(state.showFileSelector ||
|
|
443
|
+
state.showCommandSelector ||
|
|
444
|
+
state.showHistorySearch ||
|
|
445
|
+
state.showBackgroundTaskManager ||
|
|
446
|
+
state.showMcpManager ||
|
|
447
|
+
state.showRewindManager ||
|
|
448
|
+
state.showHelp ||
|
|
449
|
+
state.showStatusCommand)) {
|
|
327
450
|
callbacks.onAbortMessage?.();
|
|
328
451
|
return true;
|
|
329
452
|
}
|
|
@@ -373,6 +496,6 @@ export const handleInput = async (state, dispatch, callbacks, input, key, isLoad
|
|
|
373
496
|
return handleSelectorInput(state, dispatch, callbacks, input, key);
|
|
374
497
|
}
|
|
375
498
|
else {
|
|
376
|
-
return await handleNormalInput(state, dispatch, callbacks, input, key,
|
|
499
|
+
return await handleNormalInput(state, dispatch, callbacks, input, key, clearImages);
|
|
377
500
|
}
|
|
378
501
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"react": "^19.2.4",
|
|
40
40
|
"react-dom": "19.2.4",
|
|
41
41
|
"yargs": "^17.7.2",
|
|
42
|
-
"wave-agent-sdk": "0.
|
|
42
|
+
"wave-agent-sdk": "0.9.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/react": "^19.1.8",
|
|
@@ -4,6 +4,7 @@ import { MessageList } from "./MessageList.js";
|
|
|
4
4
|
import { InputBox } from "./InputBox.js";
|
|
5
5
|
import { LoadingIndicator } from "./LoadingIndicator.js";
|
|
6
6
|
import { TaskList } from "./TaskList.js";
|
|
7
|
+
import { QueuedMessageList } from "./QueuedMessageList.js";
|
|
7
8
|
import { ConfirmationDetails } from "./ConfirmationDetails.js";
|
|
8
9
|
import { ConfirmationSelector } from "./ConfirmationSelector.js";
|
|
9
10
|
|
|
@@ -14,6 +15,7 @@ export const ChatInterface: React.FC = () => {
|
|
|
14
15
|
const { stdout } = useStdout();
|
|
15
16
|
const [detailsHeight, setDetailsHeight] = useState(0);
|
|
16
17
|
const [selectorHeight, setSelectorHeight] = useState(0);
|
|
18
|
+
const [dynamicBlocksHeight, setDynamicBlocksHeight] = useState(0);
|
|
17
19
|
const [isConfirmationTooTall, setIsConfirmationTooTall] = useState(false);
|
|
18
20
|
|
|
19
21
|
const {
|
|
@@ -51,15 +53,36 @@ export const ChatInterface: React.FC = () => {
|
|
|
51
53
|
setSelectorHeight(height);
|
|
52
54
|
}, []);
|
|
53
55
|
|
|
56
|
+
const handleDynamicBlocksHeightMeasured = useCallback((height: number) => {
|
|
57
|
+
setDynamicBlocksHeight(height);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
54
60
|
useLayoutEffect(() => {
|
|
61
|
+
if (!isConfirmationVisible) {
|
|
62
|
+
setIsConfirmationTooTall(false);
|
|
63
|
+
setDetailsHeight(0);
|
|
64
|
+
setSelectorHeight(0);
|
|
65
|
+
setDynamicBlocksHeight(0);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (isConfirmationTooTall) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
const terminalHeight = stdout?.rows || 24;
|
|
56
|
-
const totalHeight = detailsHeight + selectorHeight;
|
|
74
|
+
const totalHeight = detailsHeight + selectorHeight + dynamicBlocksHeight;
|
|
57
75
|
if (totalHeight > terminalHeight) {
|
|
58
76
|
setIsConfirmationTooTall(true);
|
|
59
|
-
} else {
|
|
60
|
-
setIsConfirmationTooTall(false);
|
|
61
77
|
}
|
|
62
|
-
}, [
|
|
78
|
+
}, [
|
|
79
|
+
detailsHeight,
|
|
80
|
+
selectorHeight,
|
|
81
|
+
dynamicBlocksHeight,
|
|
82
|
+
stdout?.rows,
|
|
83
|
+
isConfirmationVisible,
|
|
84
|
+
isConfirmationTooTall,
|
|
85
|
+
]);
|
|
63
86
|
|
|
64
87
|
const handleConfirmationCancel = useCallback(() => {
|
|
65
88
|
if (isConfirmationTooTall) {
|
|
@@ -99,6 +122,7 @@ export const ChatInterface: React.FC = () => {
|
|
|
99
122
|
version={version}
|
|
100
123
|
workdir={workdir}
|
|
101
124
|
model={model}
|
|
125
|
+
onDynamicBlocksHeightMeasured={handleDynamicBlocksHeightMeasured}
|
|
102
126
|
/>
|
|
103
127
|
|
|
104
128
|
{(isLoading || isCommandRunning || isCompressing) &&
|
|
@@ -137,17 +161,20 @@ export const ChatInterface: React.FC = () => {
|
|
|
137
161
|
)}
|
|
138
162
|
|
|
139
163
|
{!isConfirmationVisible && !isExpanded && (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
<>
|
|
165
|
+
<QueuedMessageList />
|
|
166
|
+
<InputBox
|
|
167
|
+
isLoading={isLoading}
|
|
168
|
+
isCommandRunning={isCommandRunning}
|
|
169
|
+
sendMessage={sendMessage}
|
|
170
|
+
abortMessage={abortMessage}
|
|
171
|
+
mcpServers={mcpServers}
|
|
172
|
+
connectMcpServer={connectMcpServer}
|
|
173
|
+
disconnectMcpServer={disconnectMcpServer}
|
|
174
|
+
slashCommands={slashCommands}
|
|
175
|
+
hasSlashCommand={hasSlashCommand}
|
|
176
|
+
/>
|
|
177
|
+
</>
|
|
151
178
|
)}
|
|
152
179
|
</Box>
|
|
153
180
|
);
|
|
@@ -58,11 +58,13 @@ export const HelpView: React.FC<HelpViewProps> = ({
|
|
|
58
58
|
const helpItems = [
|
|
59
59
|
{ key: "@", description: "Reference files" },
|
|
60
60
|
{ key: "/", description: "Commands" },
|
|
61
|
+
{ key: "!", description: "Shell commands (e.g. !ls)" },
|
|
61
62
|
{ key: "Ctrl+R", description: "Search history" },
|
|
62
63
|
{ key: "Ctrl+O", description: "Expand/collapse messages" },
|
|
63
64
|
{ key: "Ctrl+T", description: "Toggle task list" },
|
|
64
65
|
{ key: "Ctrl+B", description: "Background current task" },
|
|
65
66
|
{ key: "Ctrl+V", description: "Paste image" },
|
|
67
|
+
{ key: "Ctrl+J", description: "Newline" },
|
|
66
68
|
{ key: "Shift+Tab", description: "Cycle permission mode" },
|
|
67
69
|
{
|
|
68
70
|
key: "Esc",
|
|
@@ -41,8 +41,6 @@ export interface InputBoxProps {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export const InputBox: React.FC<InputBoxProps> = ({
|
|
44
|
-
isLoading = false,
|
|
45
|
-
isCommandRunning = false,
|
|
46
44
|
sendMessage = () => {},
|
|
47
45
|
abortMessage = () => {},
|
|
48
46
|
mcpServers = [],
|
|
@@ -119,14 +117,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
119
117
|
|
|
120
118
|
// Use the InputManager's unified input handler
|
|
121
119
|
useInput(async (input, key) => {
|
|
122
|
-
await handleInput(
|
|
123
|
-
input,
|
|
124
|
-
key,
|
|
125
|
-
attachedImages,
|
|
126
|
-
isLoading,
|
|
127
|
-
isCommandRunning,
|
|
128
|
-
clearImages,
|
|
129
|
-
);
|
|
120
|
+
await handleInput(input, key, attachedImages, clearImages);
|
|
130
121
|
});
|
|
131
122
|
|
|
132
123
|
const handleRewindCancel = () => {
|
|
@@ -138,6 +129,9 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
138
129
|
const isPlaceholder = !inputText;
|
|
139
130
|
const placeholderText = INPUT_PLACEHOLDER_TEXT;
|
|
140
131
|
|
|
132
|
+
const isShellCommand =
|
|
133
|
+
inputText?.startsWith("!") && !inputText.includes("\n");
|
|
134
|
+
|
|
141
135
|
// handleCommandSelectorInsert is already memoized in useInputManager, no need to wrap again
|
|
142
136
|
|
|
143
137
|
// Split text into three parts: before cursor, cursor position, after cursor
|
|
@@ -254,13 +248,19 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
254
248
|
</Text>
|
|
255
249
|
</Box>
|
|
256
250
|
<Box paddingRight={1} justifyContent="space-between" width="100%">
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
251
|
+
{isShellCommand ? (
|
|
252
|
+
<Text color="gray">
|
|
253
|
+
Shell: <Text color="yellow">Run shell command</Text>
|
|
254
|
+
</Text>
|
|
255
|
+
) : (
|
|
256
|
+
<Text color="gray">
|
|
257
|
+
Mode:{" "}
|
|
258
|
+
<Text color={permissionMode === "plan" ? "yellow" : "cyan"}>
|
|
259
|
+
{permissionMode}
|
|
260
|
+
</Text>{" "}
|
|
261
|
+
(Shift+Tab to cycle)
|
|
262
|
+
</Text>
|
|
263
|
+
)}
|
|
264
264
|
</Box>
|
|
265
265
|
</Box>
|
|
266
266
|
)}
|
|
@@ -35,8 +35,11 @@ export const MessageBlockItem = ({
|
|
|
35
35
|
~{" "}
|
|
36
36
|
</Text>
|
|
37
37
|
)}
|
|
38
|
-
{message.role === "user" ? (
|
|
39
|
-
<Text
|
|
38
|
+
{message.role === "user" || isExpanded ? (
|
|
39
|
+
<Text
|
|
40
|
+
backgroundColor={message.role === "user" ? "gray" : undefined}
|
|
41
|
+
color="white"
|
|
42
|
+
>
|
|
40
43
|
{block.content}
|
|
41
44
|
</Text>
|
|
42
45
|
) : (
|
|
@@ -77,7 +80,9 @@ export const MessageBlockItem = ({
|
|
|
77
80
|
<CompressDisplay block={block} isExpanded={isExpanded} />
|
|
78
81
|
)}
|
|
79
82
|
|
|
80
|
-
{block.type === "reasoning" &&
|
|
83
|
+
{block.type === "reasoning" && (
|
|
84
|
+
<ReasoningDisplay block={block} isExpanded={isExpanded} />
|
|
85
|
+
)}
|
|
81
86
|
</Box>
|
|
82
87
|
);
|
|
83
88
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useLayoutEffect, useRef } from "react";
|
|
2
2
|
import os from "os";
|
|
3
|
-
import { Box, Text, Static } from "ink";
|
|
3
|
+
import { Box, Text, Static, measureElement } from "ink";
|
|
4
4
|
import type { Message } from "wave-agent-sdk";
|
|
5
5
|
import { MessageBlockItem } from "./MessageBlockItem.js";
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ export interface MessageListProps {
|
|
|
11
11
|
version?: string;
|
|
12
12
|
workdir?: string;
|
|
13
13
|
model?: string;
|
|
14
|
+
onDynamicBlocksHeightMeasured?: (height: number) => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export const MessageList = React.memo(
|
|
@@ -21,6 +22,7 @@ export const MessageList = React.memo(
|
|
|
21
22
|
version,
|
|
22
23
|
workdir,
|
|
23
24
|
model,
|
|
25
|
+
onDynamicBlocksHeightMeasured,
|
|
24
26
|
}: MessageListProps) => {
|
|
25
27
|
const welcomeMessage = (
|
|
26
28
|
<Box flexDirection="column" paddingTop={1}>
|
|
@@ -72,6 +74,17 @@ export const MessageList = React.memo(
|
|
|
72
74
|
const staticBlocks = blocksWithStatus.filter((b) => !b.isDynamic);
|
|
73
75
|
const dynamicBlocks = blocksWithStatus.filter((b) => b.isDynamic);
|
|
74
76
|
|
|
77
|
+
const dynamicBlocksRef = useRef(null);
|
|
78
|
+
|
|
79
|
+
useLayoutEffect(() => {
|
|
80
|
+
if (dynamicBlocksRef.current) {
|
|
81
|
+
const { height } = measureElement(dynamicBlocksRef.current);
|
|
82
|
+
onDynamicBlocksHeightMeasured?.(height);
|
|
83
|
+
} else {
|
|
84
|
+
onDynamicBlocksHeightMeasured?.(0);
|
|
85
|
+
}
|
|
86
|
+
}, [dynamicBlocks, isExpanded, onDynamicBlocksHeightMeasured]);
|
|
87
|
+
|
|
75
88
|
const staticItems = [
|
|
76
89
|
{ isWelcome: true, key: "welcome", block: undefined, message: undefined },
|
|
77
90
|
...staticBlocks.map((b) => ({ ...b, isWelcome: false })),
|
|
@@ -105,7 +118,7 @@ export const MessageList = React.memo(
|
|
|
105
118
|
|
|
106
119
|
{/* Dynamic blocks */}
|
|
107
120
|
{dynamicBlocks.length > 0 && (
|
|
108
|
-
<Box flexDirection="column">
|
|
121
|
+
<Box ref={dynamicBlocksRef} flexDirection="column">
|
|
109
122
|
{dynamicBlocks.map((item) => (
|
|
110
123
|
<MessageBlockItem
|
|
111
124
|
key={item.key}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useChat } from "../contexts/useChat.js";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
|
|
5
|
+
export const QueuedMessageList: React.FC = () => {
|
|
6
|
+
const { queuedMessages = [] } = useChat();
|
|
7
|
+
|
|
8
|
+
if (queuedMessages.length === 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Box flexDirection="column">
|
|
14
|
+
{queuedMessages.map((msg, index) => {
|
|
15
|
+
const content = msg.content.trim();
|
|
16
|
+
const hasImages = msg.images && msg.images.length > 0;
|
|
17
|
+
const displayText = content || (hasImages ? "[Images]" : "");
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box key={index}>
|
|
21
|
+
<Text color="gray" italic>
|
|
22
|
+
{displayText.length > 60
|
|
23
|
+
? `${displayText.substring(0, 57)}...`
|
|
24
|
+
: displayText}
|
|
25
|
+
</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
);
|
|
28
|
+
})}
|
|
29
|
+
</Box>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Box } from "ink";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
3
|
import type { ReasoningBlock } from "wave-agent-sdk";
|
|
4
4
|
import { Markdown } from "./Markdown.js";
|
|
5
5
|
|
|
6
6
|
interface ReasoningDisplayProps {
|
|
7
7
|
block: ReasoningBlock;
|
|
8
|
+
isExpanded?: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const ReasoningDisplay: React.FC<ReasoningDisplayProps> = ({
|
|
11
12
|
block,
|
|
13
|
+
isExpanded = false,
|
|
12
14
|
}) => {
|
|
13
15
|
const { content } = block;
|
|
14
16
|
|
|
@@ -26,7 +28,11 @@ export const ReasoningDisplay: React.FC<ReasoningDisplayProps> = ({
|
|
|
26
28
|
paddingLeft={1}
|
|
27
29
|
>
|
|
28
30
|
<Box flexDirection="column">
|
|
29
|
-
|
|
31
|
+
{isExpanded ? (
|
|
32
|
+
<Text color="white">{content}</Text>
|
|
33
|
+
) : (
|
|
34
|
+
<Markdown>{content}</Markdown>
|
|
35
|
+
)}
|
|
30
36
|
</Box>
|
|
31
37
|
</Box>
|
|
32
38
|
);
|