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.
Files changed (41) hide show
  1. package/dist/components/ChatInterface.d.ts.map +1 -1
  2. package/dist/components/ChatInterface.js +26 -7
  3. package/dist/components/HelpView.d.ts.map +1 -1
  4. package/dist/components/HelpView.js +2 -0
  5. package/dist/components/InputBox.js +4 -3
  6. package/dist/components/MessageBlockItem.d.ts.map +1 -1
  7. package/dist/components/MessageBlockItem.js +1 -1
  8. package/dist/components/MessageList.d.ts +2 -1
  9. package/dist/components/MessageList.d.ts.map +1 -1
  10. package/dist/components/MessageList.js +14 -4
  11. package/dist/components/QueuedMessageList.d.ts +3 -0
  12. package/dist/components/QueuedMessageList.d.ts.map +1 -0
  13. package/dist/components/QueuedMessageList.js +17 -0
  14. package/dist/components/ReasoningDisplay.d.ts +1 -0
  15. package/dist/components/ReasoningDisplay.d.ts.map +1 -1
  16. package/dist/components/ReasoningDisplay.js +3 -3
  17. package/dist/components/RewindCommand.d.ts.map +1 -1
  18. package/dist/components/RewindCommand.js +10 -4
  19. package/dist/components/ToolDisplay.js +1 -1
  20. package/dist/contexts/useChat.d.ts +7 -0
  21. package/dist/contexts/useChat.d.ts.map +1 -1
  22. package/dist/contexts/useChat.js +17 -1
  23. package/dist/hooks/useInputManager.d.ts +3 -3
  24. package/dist/hooks/useInputManager.d.ts.map +1 -1
  25. package/dist/hooks/useInputManager.js +10 -9
  26. package/dist/managers/inputHandlers.d.ts +7 -4
  27. package/dist/managers/inputHandlers.d.ts.map +1 -1
  28. package/dist/managers/inputHandlers.js +165 -42
  29. package/package.json +2 -2
  30. package/src/components/ChatInterface.tsx +42 -15
  31. package/src/components/HelpView.tsx +2 -0
  32. package/src/components/InputBox.tsx +17 -17
  33. package/src/components/MessageBlockItem.tsx +8 -3
  34. package/src/components/MessageList.tsx +16 -3
  35. package/src/components/QueuedMessageList.tsx +31 -0
  36. package/src/components/ReasoningDisplay.tsx +8 -2
  37. package/src/components/RewindCommand.tsx +21 -5
  38. package/src/components/ToolDisplay.tsx +2 -2
  39. package/src/contexts/useChat.tsx +29 -1
  40. package/src/hooks/useInputManager.ts +9 -21
  41. 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, isLoading = false, isCommandRunning = false, attachedImagesOverride) => {
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 handleSpecialCharInput = (state, dispatch, char, cursorPosition, inputText) => {
86
- if (char === "@") {
87
- dispatch({ type: "ACTIVATE_FILE_SELECTOR", payload: cursorPosition - 1 });
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: "ACTIVATE_COMMAND_SELECTOR",
92
- payload: cursorPosition - 1,
136
+ type: trigger.type,
137
+ payload: newCursorPosition - 1,
93
138
  });
94
139
  }
95
140
  else {
96
- updateSearchQueriesForActiveSelectors(state, dispatch, inputText, cursorPosition);
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
- // Calculate new state for special char handling
123
- const newCursorPosition = state.cursorPosition + char.length;
124
- const beforeCursor = state.inputText.substring(0, state.cursorPosition);
125
- const afterCursor = state.inputText.substring(state.cursorPosition);
126
- const newInputText = beforeCursor + char + afterCursor;
127
- handleSpecialCharInput(state, dispatch, char, newCursorPosition, newInputText);
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 afterQuery = state.inputText.substring(state.cursorPosition);
134
- const newInput = beforeSlash + afterQuery;
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 afterQuery = state.inputText.substring(state.cursorPosition);
183
- const newInput = beforeAt + `${filePath} ` + afterQuery;
184
- const newCursorPosition = beforeAt.length + filePath.length + 1;
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
- // Calculate new state for special char handling
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, isLoading = false, isCommandRunning = false, clearImages) => {
338
+ export const handleNormalInput = async (state, dispatch, callbacks, input, key, clearImages) => {
250
339
  if (key.return) {
251
- await handleSubmit(state, dispatch, callbacks, isLoading, isCommandRunning);
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, isLoading = false, isCommandRunning = false, clearImages) => {
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 ((isLoading || isCommandRunning) &&
319
- !(state.showFileSelector ||
320
- state.showCommandSelector ||
321
- state.showHistorySearch ||
322
- state.showBackgroundTaskManager ||
323
- state.showMcpManager ||
324
- state.showRewindManager ||
325
- state.showHelp ||
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, isLoading, isCommandRunning, clearImages);
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.8.3",
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.8.3"
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
- }, [detailsHeight, selectorHeight, stdout?.rows]);
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
- <InputBox
141
- isLoading={isLoading}
142
- isCommandRunning={isCommandRunning}
143
- sendMessage={sendMessage}
144
- abortMessage={abortMessage}
145
- mcpServers={mcpServers}
146
- connectMcpServer={connectMcpServer}
147
- disconnectMcpServer={disconnectMcpServer}
148
- slashCommands={slashCommands}
149
- hasSlashCommand={hasSlashCommand}
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
- <Text color="gray">
258
- Mode:{" "}
259
- <Text color={permissionMode === "plan" ? "yellow" : "cyan"}>
260
- {permissionMode}
261
- </Text>{" "}
262
- (Shift+Tab to cycle)
263
- </Text>
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 backgroundColor="gray" color="white">
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" && <ReasoningDisplay block={block} />}
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
- <Markdown>{content}</Markdown>
31
+ {isExpanded ? (
32
+ <Text color="white">{content}</Text>
33
+ ) : (
34
+ <Markdown>{content}</Markdown>
35
+ )}
30
36
  </Box>
31
37
  </Box>
32
38
  );