wave-code 0.8.4 → 0.9.1
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/FileSelector.d.ts +1 -0
- package/dist/components/FileSelector.d.ts.map +1 -1
- package/dist/components/FileSelector.js +3 -3
- package/dist/components/HelpView.d.ts.map +1 -1
- package/dist/components/HelpView.js +1 -0
- package/dist/components/HistorySearch.d.ts +2 -1
- package/dist/components/HistorySearch.d.ts.map +1 -1
- package/dist/components/HistorySearch.js +1 -1
- package/dist/components/InputBox.d.ts.map +1 -1
- package/dist/components/InputBox.js +8 -6
- 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/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 +6 -5
- package/dist/hooks/useInputManager.d.ts.map +1 -1
- package/dist/hooks/useInputManager.js +18 -16
- package/dist/managers/inputHandlers.d.ts +7 -4
- package/dist/managers/inputHandlers.d.ts.map +1 -1
- package/dist/managers/inputHandlers.js +184 -46
- package/dist/managers/inputReducer.d.ts +18 -2
- package/dist/managers/inputReducer.d.ts.map +1 -1
- package/dist/managers/inputReducer.js +92 -3
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +13 -1
- package/package.json +2 -2
- package/src/components/ChatInterface.tsx +42 -15
- package/src/components/FileSelector.tsx +13 -3
- package/src/components/HelpView.tsx +1 -0
- package/src/components/HistorySearch.tsx +2 -2
- package/src/components/InputBox.tsx +21 -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/ToolDisplay.tsx +2 -2
- package/src/contexts/useChat.tsx +29 -1
- package/src/hooks/useInputManager.ts +22 -31
- package/src/managers/inputHandlers.ts +223 -60
- package/src/managers/inputReducer.ts +104 -6
- package/src/utils/logger.ts +15 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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.1"
|
|
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
|
);
|
|
@@ -7,6 +7,7 @@ export { type FileItem } from "wave-agent-sdk";
|
|
|
7
7
|
export interface FileSelectorProps {
|
|
8
8
|
files: FileItem[];
|
|
9
9
|
searchQuery: string;
|
|
10
|
+
isLoading?: boolean;
|
|
10
11
|
onSelect: (filePath: string) => void;
|
|
11
12
|
onCancel: () => void;
|
|
12
13
|
}
|
|
@@ -14,6 +15,7 @@ export interface FileSelectorProps {
|
|
|
14
15
|
export const FileSelector: React.FC<FileSelectorProps> = ({
|
|
15
16
|
files,
|
|
16
17
|
searchQuery,
|
|
18
|
+
isLoading = false,
|
|
17
19
|
onSelect,
|
|
18
20
|
onCancel,
|
|
19
21
|
}) => {
|
|
@@ -48,13 +50,21 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
48
50
|
<Box
|
|
49
51
|
flexDirection="column"
|
|
50
52
|
borderStyle="single"
|
|
51
|
-
borderColor="yellow"
|
|
53
|
+
borderColor={isLoading ? "cyan" : "yellow"}
|
|
52
54
|
borderBottom={false}
|
|
53
55
|
borderLeft={false}
|
|
54
56
|
borderRight={false}
|
|
55
57
|
>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
{isLoading ? (
|
|
59
|
+
<Text color="cyan" bold>
|
|
60
|
+
Select File/Directory...
|
|
61
|
+
</Text>
|
|
62
|
+
) : (
|
|
63
|
+
<>
|
|
64
|
+
<Text color="yellow">No files found for "{searchQuery}"</Text>
|
|
65
|
+
<Text dimColor>Press Escape to cancel</Text>
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
58
68
|
</Box>
|
|
59
69
|
);
|
|
60
70
|
}
|
|
@@ -64,6 +64,7 @@ export const HelpView: React.FC<HelpViewProps> = ({
|
|
|
64
64
|
{ key: "Ctrl+T", description: "Toggle task list" },
|
|
65
65
|
{ key: "Ctrl+B", description: "Background current task" },
|
|
66
66
|
{ key: "Ctrl+V", description: "Paste image" },
|
|
67
|
+
{ key: "Ctrl+J", description: "Newline" },
|
|
67
68
|
{ key: "Shift+Tab", description: "Cycle permission mode" },
|
|
68
69
|
{
|
|
69
70
|
key: "Esc",
|
|
@@ -4,7 +4,7 @@ import { PromptHistoryManager, type PromptEntry } from "wave-agent-sdk";
|
|
|
4
4
|
|
|
5
5
|
export interface HistorySearchProps {
|
|
6
6
|
searchQuery: string;
|
|
7
|
-
onSelect: (
|
|
7
|
+
onSelect: (entry: PromptEntry) => void;
|
|
8
8
|
onCancel: () => void;
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -44,7 +44,7 @@ export const HistorySearch: React.FC<HistorySearchProps> = ({
|
|
|
44
44
|
entriesRef.current.length > 0 &&
|
|
45
45
|
selectedIndexRef.current < entriesRef.current.length
|
|
46
46
|
) {
|
|
47
|
-
onSelect(entriesRef.current[selectedIndexRef.current]
|
|
47
|
+
onSelect(entriesRef.current[selectedIndexRef.current]);
|
|
48
48
|
}
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
@@ -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 = [],
|
|
@@ -59,6 +57,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
59
57
|
messages,
|
|
60
58
|
getFullMessageThread,
|
|
61
59
|
clearMessages,
|
|
60
|
+
sessionId,
|
|
62
61
|
} = useChat();
|
|
63
62
|
|
|
64
63
|
// Input manager with all input state and functionality (including images)
|
|
@@ -72,6 +71,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
72
71
|
showFileSelector,
|
|
73
72
|
filteredFiles,
|
|
74
73
|
fileSearchQuery: searchQuery,
|
|
74
|
+
isFileSearching,
|
|
75
75
|
handleFileSelect,
|
|
76
76
|
handleCancelFileSelect,
|
|
77
77
|
// Command selector
|
|
@@ -110,6 +110,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
110
110
|
onBackgroundCurrentTask: backgroundCurrentTask,
|
|
111
111
|
onPermissionModeChange: setChatPermissionMode,
|
|
112
112
|
onClearMessages: clearMessages,
|
|
113
|
+
sessionId,
|
|
113
114
|
});
|
|
114
115
|
|
|
115
116
|
// Sync permission mode from useChat to InputManager
|
|
@@ -119,14 +120,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
119
120
|
|
|
120
121
|
// Use the InputManager's unified input handler
|
|
121
122
|
useInput(async (input, key) => {
|
|
122
|
-
await handleInput(
|
|
123
|
-
input,
|
|
124
|
-
key,
|
|
125
|
-
attachedImages,
|
|
126
|
-
isLoading,
|
|
127
|
-
isCommandRunning,
|
|
128
|
-
clearImages,
|
|
129
|
-
);
|
|
123
|
+
await handleInput(input, key, attachedImages, clearImages);
|
|
130
124
|
});
|
|
131
125
|
|
|
132
126
|
const handleRewindCancel = () => {
|
|
@@ -138,6 +132,9 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
138
132
|
const isPlaceholder = !inputText;
|
|
139
133
|
const placeholderText = INPUT_PLACEHOLDER_TEXT;
|
|
140
134
|
|
|
135
|
+
const isShellCommand =
|
|
136
|
+
inputText?.startsWith("!") && !inputText.includes("\n");
|
|
137
|
+
|
|
141
138
|
// handleCommandSelectorInsert is already memoized in useInputManager, no need to wrap again
|
|
142
139
|
|
|
143
140
|
// Split text into three parts: before cursor, cursor position, after cursor
|
|
@@ -189,6 +186,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
189
186
|
<FileSelector
|
|
190
187
|
files={filteredFiles}
|
|
191
188
|
searchQuery={searchQuery}
|
|
189
|
+
isLoading={isFileSearching}
|
|
192
190
|
onSelect={handleFileSelect}
|
|
193
191
|
onCancel={handleCancelFileSelect}
|
|
194
192
|
/>
|
|
@@ -254,13 +252,19 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
254
252
|
</Text>
|
|
255
253
|
</Box>
|
|
256
254
|
<Box paddingRight={1} justifyContent="space-between" width="100%">
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
255
|
+
{isShellCommand ? (
|
|
256
|
+
<Text color="gray">
|
|
257
|
+
Shell: <Text color="yellow">Run shell command</Text>
|
|
258
|
+
</Text>
|
|
259
|
+
) : (
|
|
260
|
+
<Text color="gray">
|
|
261
|
+
Mode:{" "}
|
|
262
|
+
<Text color={permissionMode === "plan" ? "yellow" : "cyan"}>
|
|
263
|
+
{permissionMode}
|
|
264
|
+
</Text>{" "}
|
|
265
|
+
(Shift+Tab to cycle)
|
|
266
|
+
</Text>
|
|
267
|
+
)}
|
|
264
268
|
</Box>
|
|
265
269
|
</Box>
|
|
266
270
|
)}
|
|
@@ -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
|
);
|
|
@@ -129,8 +129,8 @@ export const ToolDisplay: React.FC<ToolDisplayProps> = ({
|
|
|
129
129
|
</Box>
|
|
130
130
|
)}
|
|
131
131
|
|
|
132
|
-
{/* Diff display - only show after tool execution completes and was successful */}
|
|
133
|
-
{stage === "end" && success && (
|
|
132
|
+
{/* Diff display - only show after tool execution completes and was successful, and NOT in expanded mode */}
|
|
133
|
+
{!isExpanded && stage === "end" && success && (
|
|
134
134
|
<DiffDisplay
|
|
135
135
|
toolName={name}
|
|
136
136
|
parameters={parameters}
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -37,6 +37,10 @@ export interface ChatContextType {
|
|
|
37
37
|
isExpanded: boolean;
|
|
38
38
|
isTaskListVisible: boolean;
|
|
39
39
|
setIsTaskListVisible: (visible: boolean) => void;
|
|
40
|
+
queuedMessages: Array<{
|
|
41
|
+
content: string;
|
|
42
|
+
images?: Array<{ path: string; mimeType: string }>;
|
|
43
|
+
}>;
|
|
40
44
|
// AI functionality
|
|
41
45
|
sessionId: string;
|
|
42
46
|
sendMessage: (
|
|
@@ -139,6 +143,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
139
143
|
|
|
140
144
|
const [isTaskListVisible, setIsTaskListVisible] = useState(true);
|
|
141
145
|
|
|
146
|
+
const [queuedMessages, setQueuedMessages] = useState<
|
|
147
|
+
Array<{
|
|
148
|
+
content: string;
|
|
149
|
+
images?: Array<{ path: string; mimeType: string }>;
|
|
150
|
+
}>
|
|
151
|
+
>([]);
|
|
152
|
+
|
|
142
153
|
// AI State
|
|
143
154
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
144
155
|
|
|
@@ -393,6 +404,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
393
404
|
|
|
394
405
|
if (!hasTextContent && !hasImageAttachments) return;
|
|
395
406
|
|
|
407
|
+
if (isLoading || isCommandRunning) {
|
|
408
|
+
setQueuedMessages((prev) => [...prev, { content, images }]);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
396
412
|
try {
|
|
397
413
|
// Handle bash mode - check if it's a bash command (starts with ! and only one line)
|
|
398
414
|
if (content.startsWith("!") && !content.includes("\n")) {
|
|
@@ -432,15 +448,26 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
432
448
|
// Loading state will be automatically updated by the useEffect that watches messages
|
|
433
449
|
}
|
|
434
450
|
},
|
|
435
|
-
[],
|
|
451
|
+
[isLoading, isCommandRunning],
|
|
436
452
|
);
|
|
437
453
|
|
|
454
|
+
// Process queued messages when idle
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (!isLoading && !isCommandRunning && queuedMessages.length > 0) {
|
|
457
|
+
const nextMessage = queuedMessages[0];
|
|
458
|
+
setQueuedMessages((prev) => prev.slice(1));
|
|
459
|
+
sendMessage(nextMessage.content, nextMessage.images);
|
|
460
|
+
}
|
|
461
|
+
}, [isLoading, isCommandRunning, queuedMessages, sendMessage]);
|
|
462
|
+
|
|
438
463
|
// Unified interrupt method, interrupt both AI messages and command execution
|
|
439
464
|
const abortMessage = useCallback(() => {
|
|
465
|
+
setQueuedMessages([]);
|
|
440
466
|
agentRef.current?.abortMessage();
|
|
441
467
|
}, []);
|
|
442
468
|
|
|
443
469
|
const clearMessages = useCallback(() => {
|
|
470
|
+
setQueuedMessages([]);
|
|
444
471
|
agentRef.current?.clearMessages();
|
|
445
472
|
}, []);
|
|
446
473
|
|
|
@@ -604,6 +631,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
604
631
|
isExpanded,
|
|
605
632
|
isTaskListVisible,
|
|
606
633
|
setIsTaskListVisible,
|
|
634
|
+
queuedMessages,
|
|
607
635
|
sessionId,
|
|
608
636
|
sendMessage,
|
|
609
637
|
abortMessage,
|
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
initialState,
|
|
6
6
|
InputManagerCallbacks,
|
|
7
7
|
} from "../managers/inputReducer.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
searchFiles as searchFilesUtil,
|
|
10
|
+
PermissionMode,
|
|
11
|
+
PromptEntry,
|
|
12
|
+
} from "wave-agent-sdk";
|
|
9
13
|
import * as handlers from "../managers/inputHandlers.js";
|
|
10
14
|
|
|
11
15
|
export const useInputManager = (
|
|
@@ -37,10 +41,10 @@ export const useInputManager = (
|
|
|
37
41
|
// Handle debounced file search
|
|
38
42
|
useEffect(() => {
|
|
39
43
|
if (state.showFileSelector) {
|
|
40
|
-
const debounceDelay =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const debounceDelay =
|
|
45
|
+
state.fileSearchQuery === ""
|
|
46
|
+
? 0
|
|
47
|
+
: parseInt(process.env.FILE_SELECTOR_DEBOUNCE_MS || "300", 10);
|
|
44
48
|
const timer = setTimeout(async () => {
|
|
45
49
|
try {
|
|
46
50
|
const fileItems = await searchFilesUtil(state.fileSearchQuery);
|
|
@@ -68,7 +72,7 @@ export const useInputManager = (
|
|
|
68
72
|
);
|
|
69
73
|
dispatch({ type: "COMPRESS_AND_INSERT_TEXT", payload: processedInput });
|
|
70
74
|
dispatch({ type: "END_PASTE" });
|
|
71
|
-
|
|
75
|
+
dispatch({ type: "RESET_HISTORY_NAVIGATION" });
|
|
72
76
|
}, pasteDebounceDelay);
|
|
73
77
|
return () => clearTimeout(timer);
|
|
74
78
|
}
|
|
@@ -204,14 +208,16 @@ export const useInputManager = (
|
|
|
204
208
|
const handleCommandInsert = useCallback((command: string) => {
|
|
205
209
|
const currentState = stateRef.current;
|
|
206
210
|
if (currentState.slashPosition >= 0) {
|
|
211
|
+
const wordEnd = handlers.getWordEnd(
|
|
212
|
+
currentState.inputText,
|
|
213
|
+
currentState.slashPosition,
|
|
214
|
+
);
|
|
207
215
|
const beforeSlash = currentState.inputText.substring(
|
|
208
216
|
0,
|
|
209
217
|
currentState.slashPosition,
|
|
210
218
|
);
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
);
|
|
214
|
-
const newInput = beforeSlash + `/${command} ` + afterQuery;
|
|
219
|
+
const afterWord = currentState.inputText.substring(wordEnd);
|
|
220
|
+
const newInput = beforeSlash + `/${command} ` + afterWord;
|
|
215
221
|
const newCursorPosition = beforeSlash.length + command.length + 2;
|
|
216
222
|
|
|
217
223
|
dispatch({ type: "SET_INPUT_TEXT", payload: newInput });
|
|
@@ -245,24 +251,16 @@ export const useInputManager = (
|
|
|
245
251
|
);
|
|
246
252
|
}, []);
|
|
247
253
|
|
|
248
|
-
const handleHistorySearchSelect = useCallback((
|
|
249
|
-
dispatch({ type: "
|
|
250
|
-
dispatch({ type: "SET_CURSOR_POSITION", payload: prompt.length });
|
|
251
|
-
dispatch({ type: "CANCEL_HISTORY_SEARCH" });
|
|
254
|
+
const handleHistorySearchSelect = useCallback((entry: PromptEntry) => {
|
|
255
|
+
dispatch({ type: "SELECT_HISTORY_ENTRY", payload: entry });
|
|
252
256
|
}, []);
|
|
253
257
|
|
|
254
258
|
const handleCancelHistorySearch = useCallback(() => {
|
|
255
259
|
dispatch({ type: "CANCEL_HISTORY_SEARCH" });
|
|
256
260
|
}, []);
|
|
257
261
|
|
|
258
|
-
const
|
|
259
|
-
handlers.
|
|
260
|
-
stateRef.current,
|
|
261
|
-
dispatch,
|
|
262
|
-
char,
|
|
263
|
-
stateRef.current.cursorPosition,
|
|
264
|
-
stateRef.current.inputText,
|
|
265
|
-
);
|
|
262
|
+
const processSelectorInput = useCallback((char: string) => {
|
|
263
|
+
handlers.processSelectorInput(stateRef.current, dispatch, char);
|
|
266
264
|
}, []);
|
|
267
265
|
|
|
268
266
|
const setInputText = useCallback((text: string) => {
|
|
@@ -326,15 +324,11 @@ export const useInputManager = (
|
|
|
326
324
|
const handleSubmit = useCallback(
|
|
327
325
|
async (
|
|
328
326
|
attachedImages: Array<{ id: number; path: string; mimeType: string }>,
|
|
329
|
-
isLoading: boolean = false,
|
|
330
|
-
isCommandRunning: boolean = false,
|
|
331
327
|
) => {
|
|
332
328
|
await handlers.handleSubmit(
|
|
333
329
|
stateRef.current,
|
|
334
330
|
dispatch,
|
|
335
331
|
callbacksRef.current,
|
|
336
|
-
isLoading,
|
|
337
|
-
isCommandRunning,
|
|
338
332
|
attachedImages,
|
|
339
333
|
);
|
|
340
334
|
},
|
|
@@ -357,8 +351,6 @@ export const useInputManager = (
|
|
|
357
351
|
input: string,
|
|
358
352
|
key: Key,
|
|
359
353
|
attachedImages: Array<{ id: number; path: string; mimeType: string }>,
|
|
360
|
-
isLoading: boolean = false,
|
|
361
|
-
isCommandRunning: boolean = false,
|
|
362
354
|
clearImages?: () => void,
|
|
363
355
|
) => {
|
|
364
356
|
return await handlers.handleInput(
|
|
@@ -367,8 +359,6 @@ export const useInputManager = (
|
|
|
367
359
|
callbacksRef.current,
|
|
368
360
|
input,
|
|
369
361
|
key,
|
|
370
|
-
isLoading,
|
|
371
|
-
isCommandRunning,
|
|
372
362
|
clearImages,
|
|
373
363
|
);
|
|
374
364
|
},
|
|
@@ -382,6 +372,7 @@ export const useInputManager = (
|
|
|
382
372
|
showFileSelector: state.showFileSelector,
|
|
383
373
|
filteredFiles: state.filteredFiles,
|
|
384
374
|
fileSearchQuery: state.fileSearchQuery,
|
|
375
|
+
isFileSearching: state.isFileSearching,
|
|
385
376
|
atPosition: state.atPosition,
|
|
386
377
|
showCommandSelector: state.showCommandSelector,
|
|
387
378
|
commandSearchQuery: state.commandSearchQuery,
|
|
@@ -424,7 +415,7 @@ export const useInputManager = (
|
|
|
424
415
|
handleCancelHistorySearch,
|
|
425
416
|
|
|
426
417
|
// Special handling
|
|
427
|
-
|
|
418
|
+
processSelectorInput,
|
|
428
419
|
|
|
429
420
|
// Bash/MCP Manager
|
|
430
421
|
setShowBackgroundTaskManager,
|