wave-code 0.0.6 → 0.0.8
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 +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/components/App.d.ts +1 -0
- package/dist/components/App.d.ts.map +1 -1
- package/dist/components/App.js +4 -4
- package/dist/components/BashHistorySelector.d.ts.map +1 -1
- package/dist/components/BashHistorySelector.js +17 -3
- package/dist/components/ChatInterface.d.ts.map +1 -1
- package/dist/components/ChatInterface.js +4 -2
- package/dist/components/Confirmation.d.ts +11 -0
- package/dist/components/Confirmation.d.ts.map +1 -0
- package/dist/components/Confirmation.js +148 -0
- package/dist/components/DiffDisplay.d.ts +8 -0
- package/dist/components/DiffDisplay.d.ts.map +1 -0
- package/dist/components/DiffDisplay.js +168 -0
- package/dist/components/FileSelector.d.ts +2 -4
- package/dist/components/FileSelector.d.ts.map +1 -1
- package/dist/components/InputBox.d.ts.map +1 -1
- package/dist/components/InputBox.js +10 -1
- package/dist/components/MemoryDisplay.js +1 -1
- package/dist/components/MessageItem.d.ts +1 -2
- package/dist/components/MessageItem.d.ts.map +1 -1
- package/dist/components/MessageItem.js +3 -3
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +2 -2
- package/dist/components/ReasoningDisplay.d.ts +8 -0
- package/dist/components/ReasoningDisplay.d.ts.map +1 -0
- package/dist/components/ReasoningDisplay.js +10 -0
- package/dist/components/ToolResultDisplay.d.ts.map +1 -1
- package/dist/components/ToolResultDisplay.js +2 -1
- package/dist/contexts/useChat.d.ts +13 -1
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +117 -15
- package/dist/hooks/useInputManager.d.ts +3 -0
- package/dist/hooks/useInputManager.d.ts.map +1 -1
- package/dist/hooks/useInputManager.js +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -4
- package/dist/managers/InputManager.d.ts +8 -0
- package/dist/managers/InputManager.d.ts.map +1 -1
- package/dist/managers/InputManager.js +33 -2
- package/dist/print-cli.d.ts +1 -0
- package/dist/print-cli.d.ts.map +1 -1
- package/dist/print-cli.js +36 -3
- package/dist/utils/toolParameterTransforms.d.ts +23 -0
- package/dist/utils/toolParameterTransforms.d.ts.map +1 -0
- package/dist/utils/toolParameterTransforms.js +77 -0
- package/package.json +6 -5
- package/src/cli.tsx +3 -1
- package/src/components/App.tsx +7 -3
- package/src/components/BashHistorySelector.tsx +26 -3
- package/src/components/ChatInterface.tsx +29 -15
- package/src/components/Confirmation.tsx +253 -0
- package/src/components/DiffDisplay.tsx +300 -0
- package/src/components/FileSelector.tsx +2 -4
- package/src/components/InputBox.tsx +37 -14
- package/src/components/MemoryDisplay.tsx +1 -1
- package/src/components/MessageItem.tsx +4 -12
- package/src/components/MessageList.tsx +0 -2
- package/src/components/ReasoningDisplay.tsx +33 -0
- package/src/components/ToolResultDisplay.tsx +4 -0
- package/src/contexts/useChat.tsx +178 -14
- package/src/hooks/useInputManager.ts +19 -0
- package/src/index.ts +34 -4
- package/src/managers/InputManager.ts +46 -2
- package/src/print-cli.ts +42 -2
- package/src/utils/toolParameterTransforms.ts +104 -0
- package/dist/components/DiffViewer.d.ts +0 -9
- package/dist/components/DiffViewer.d.ts.map +0 -1
- package/dist/components/DiffViewer.js +0 -221
- package/dist/utils/fileSearch.d.ts +0 -20
- package/dist/utils/fileSearch.d.ts.map +0 -1
- package/dist/utils/fileSearch.js +0 -102
- package/src/components/DiffViewer.tsx +0 -323
- package/src/utils/fileSearch.ts +0 -133
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import type { PermissionDecision } from "wave-agent-sdk";
|
|
4
|
+
|
|
5
|
+
// Helper function to generate descriptive action text
|
|
6
|
+
const getActionDescription = (
|
|
7
|
+
toolName: string,
|
|
8
|
+
toolInput?: Record<string, unknown>,
|
|
9
|
+
): string => {
|
|
10
|
+
if (!toolInput) {
|
|
11
|
+
return "Execute operation";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
switch (toolName) {
|
|
15
|
+
case "Bash":
|
|
16
|
+
return `Execute command: ${toolInput.command || "unknown command"}`;
|
|
17
|
+
case "Edit":
|
|
18
|
+
return `Edit file: ${toolInput.file_path || "unknown file"}`;
|
|
19
|
+
case "MultiEdit":
|
|
20
|
+
return `Edit multiple sections in: ${toolInput.file_path || "unknown file"}`;
|
|
21
|
+
case "Delete":
|
|
22
|
+
return `Delete file: ${toolInput.target_file || "unknown file"}`;
|
|
23
|
+
case "Write":
|
|
24
|
+
return `Write to file: ${toolInput.file_path || "unknown file"}`;
|
|
25
|
+
default:
|
|
26
|
+
return "Execute operation";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface ConfirmationProps {
|
|
31
|
+
toolName: string;
|
|
32
|
+
toolInput?: Record<string, unknown>;
|
|
33
|
+
onDecision: (decision: PermissionDecision) => void;
|
|
34
|
+
onCancel: () => void;
|
|
35
|
+
onAbort: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ConfirmationState {
|
|
39
|
+
selectedOption: "allow" | "auto" | "alternative";
|
|
40
|
+
alternativeText: string;
|
|
41
|
+
hasUserInput: boolean; // to hide placeholder
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Confirmation: React.FC<ConfirmationProps> = ({
|
|
45
|
+
toolName,
|
|
46
|
+
toolInput,
|
|
47
|
+
onDecision,
|
|
48
|
+
onCancel,
|
|
49
|
+
onAbort,
|
|
50
|
+
}) => {
|
|
51
|
+
const [state, setState] = useState<ConfirmationState>({
|
|
52
|
+
selectedOption: "allow",
|
|
53
|
+
alternativeText: "",
|
|
54
|
+
hasUserInput: false,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const getAutoOptionText = () => {
|
|
58
|
+
if (toolName === "Bash") {
|
|
59
|
+
return `Yes, and don't ask again for ${toolInput?.command || "this"} commands in this workdir`;
|
|
60
|
+
}
|
|
61
|
+
return "Yes, and auto-accept edits";
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
useInput((input, key) => {
|
|
65
|
+
// Handle ESC to cancel and abort
|
|
66
|
+
if (key.escape) {
|
|
67
|
+
onCancel();
|
|
68
|
+
onAbort();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle Enter to confirm selection
|
|
73
|
+
if (key.return) {
|
|
74
|
+
if (state.selectedOption === "allow") {
|
|
75
|
+
onDecision({ behavior: "allow" });
|
|
76
|
+
} else if (state.selectedOption === "auto") {
|
|
77
|
+
if (toolName === "Bash") {
|
|
78
|
+
onDecision({
|
|
79
|
+
behavior: "allow",
|
|
80
|
+
newPermissionRule: `Bash(${toolInput?.command})`,
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
onDecision({
|
|
84
|
+
behavior: "allow",
|
|
85
|
+
newPermissionMode: "acceptEdits",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
// For alternative option, require text input
|
|
90
|
+
if (state.alternativeText.trim()) {
|
|
91
|
+
onDecision({
|
|
92
|
+
behavior: "deny",
|
|
93
|
+
message: state.alternativeText.trim(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle numeric keys for quick selection (only if not typing in alternative)
|
|
101
|
+
if (state.selectedOption !== "alternative" || !state.hasUserInput) {
|
|
102
|
+
if (input === "1") {
|
|
103
|
+
onDecision({ behavior: "allow" });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (input === "2") {
|
|
107
|
+
if (toolName === "Bash") {
|
|
108
|
+
onDecision({
|
|
109
|
+
behavior: "allow",
|
|
110
|
+
newPermissionRule: `Bash(${toolInput?.command})`,
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
onDecision({
|
|
114
|
+
behavior: "allow",
|
|
115
|
+
newPermissionMode: "acceptEdits",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (input === "3") {
|
|
121
|
+
setState((prev) => ({ ...prev, selectedOption: "alternative" }));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle arrow keys for navigation
|
|
127
|
+
if (key.upArrow) {
|
|
128
|
+
setState((prev) => {
|
|
129
|
+
if (prev.selectedOption === "alternative")
|
|
130
|
+
return { ...prev, selectedOption: "auto" };
|
|
131
|
+
if (prev.selectedOption === "auto")
|
|
132
|
+
return { ...prev, selectedOption: "allow" };
|
|
133
|
+
return prev;
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (key.downArrow) {
|
|
139
|
+
setState((prev) => {
|
|
140
|
+
if (prev.selectedOption === "allow")
|
|
141
|
+
return { ...prev, selectedOption: "auto" };
|
|
142
|
+
if (prev.selectedOption === "auto")
|
|
143
|
+
return { ...prev, selectedOption: "alternative" };
|
|
144
|
+
return prev;
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Handle text input for alternative option
|
|
150
|
+
if (input && !key.ctrl && !key.meta && !("alt" in key && key.alt)) {
|
|
151
|
+
// Focus on alternative option when user starts typing
|
|
152
|
+
setState((prev) => ({
|
|
153
|
+
selectedOption: "alternative",
|
|
154
|
+
alternativeText: prev.alternativeText + input,
|
|
155
|
+
hasUserInput: true,
|
|
156
|
+
}));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle backspace and delete (same behavior - delete one character)
|
|
161
|
+
if (key.backspace || key.delete) {
|
|
162
|
+
setState((prev) => {
|
|
163
|
+
const newText = prev.alternativeText.slice(0, -1);
|
|
164
|
+
return {
|
|
165
|
+
...prev,
|
|
166
|
+
selectedOption: "alternative",
|
|
167
|
+
alternativeText: newText,
|
|
168
|
+
hasUserInput: newText.length > 0,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const placeholderText = "Type here to tell Wave what to do differently";
|
|
176
|
+
const showPlaceholder =
|
|
177
|
+
state.selectedOption === "alternative" && !state.hasUserInput;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Box
|
|
181
|
+
flexDirection="column"
|
|
182
|
+
borderStyle="single"
|
|
183
|
+
borderColor="yellow"
|
|
184
|
+
padding={1}
|
|
185
|
+
marginBottom={1}
|
|
186
|
+
>
|
|
187
|
+
<Text color="yellow" bold>
|
|
188
|
+
Tool: {toolName}
|
|
189
|
+
</Text>
|
|
190
|
+
<Text color="yellow">{getActionDescription(toolName, toolInput)}</Text>
|
|
191
|
+
|
|
192
|
+
<Box marginTop={1}>
|
|
193
|
+
<Text>Do you want to proceed?</Text>
|
|
194
|
+
</Box>
|
|
195
|
+
|
|
196
|
+
<Box marginTop={1} flexDirection="column">
|
|
197
|
+
{/* Option 1: Yes */}
|
|
198
|
+
<Box key="allow-option">
|
|
199
|
+
<Text
|
|
200
|
+
color={state.selectedOption === "allow" ? "black" : "white"}
|
|
201
|
+
backgroundColor={
|
|
202
|
+
state.selectedOption === "allow" ? "yellow" : undefined
|
|
203
|
+
}
|
|
204
|
+
bold={state.selectedOption === "allow"}
|
|
205
|
+
>
|
|
206
|
+
{state.selectedOption === "allow" ? "> " : " "}1. Yes
|
|
207
|
+
</Text>
|
|
208
|
+
</Box>
|
|
209
|
+
|
|
210
|
+
{/* Option 2: Auto-accept/Persistent */}
|
|
211
|
+
<Box key="auto-option">
|
|
212
|
+
<Text
|
|
213
|
+
color={state.selectedOption === "auto" ? "black" : "white"}
|
|
214
|
+
backgroundColor={
|
|
215
|
+
state.selectedOption === "auto" ? "yellow" : undefined
|
|
216
|
+
}
|
|
217
|
+
bold={state.selectedOption === "auto"}
|
|
218
|
+
>
|
|
219
|
+
{state.selectedOption === "auto" ? "> " : " "}2.{" "}
|
|
220
|
+
{getAutoOptionText()}
|
|
221
|
+
</Text>
|
|
222
|
+
</Box>
|
|
223
|
+
|
|
224
|
+
{/* Option 3: Alternative */}
|
|
225
|
+
<Box key="alternative-option">
|
|
226
|
+
<Text
|
|
227
|
+
color={state.selectedOption === "alternative" ? "black" : "white"}
|
|
228
|
+
backgroundColor={
|
|
229
|
+
state.selectedOption === "alternative" ? "yellow" : undefined
|
|
230
|
+
}
|
|
231
|
+
bold={state.selectedOption === "alternative"}
|
|
232
|
+
>
|
|
233
|
+
{state.selectedOption === "alternative" ? "> " : " "}3.{" "}
|
|
234
|
+
{showPlaceholder ? (
|
|
235
|
+
<Text color="gray" dimColor>
|
|
236
|
+
{placeholderText}
|
|
237
|
+
</Text>
|
|
238
|
+
) : (
|
|
239
|
+
<Text>
|
|
240
|
+
{state.alternativeText ||
|
|
241
|
+
"Type here to tell Wave what to do differently"}
|
|
242
|
+
</Text>
|
|
243
|
+
)}
|
|
244
|
+
</Text>
|
|
245
|
+
</Box>
|
|
246
|
+
</Box>
|
|
247
|
+
|
|
248
|
+
<Box marginTop={1}>
|
|
249
|
+
<Text dimColor>Use ↑↓ or 1-3 to navigate • ESC to cancel</Text>
|
|
250
|
+
</Box>
|
|
251
|
+
</Box>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { transformToolBlockToChanges } from "../utils/toolParameterTransforms.js";
|
|
4
|
+
import { diffLines, diffWords } from "diff";
|
|
5
|
+
import type { ToolBlock } from "wave-agent-sdk";
|
|
6
|
+
|
|
7
|
+
interface DiffDisplayProps {
|
|
8
|
+
toolBlock: ToolBlock;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DiffDisplay: React.FC<DiffDisplayProps> = ({ toolBlock }) => {
|
|
12
|
+
const showDiff =
|
|
13
|
+
["running", "end"].includes(toolBlock.stage) &&
|
|
14
|
+
toolBlock.name &&
|
|
15
|
+
["Write", "Edit", "MultiEdit"].includes(toolBlock.name);
|
|
16
|
+
|
|
17
|
+
// Diff detection and transformation using typed parameters
|
|
18
|
+
const changes = useMemo(() => {
|
|
19
|
+
if (!showDiff || !toolBlock.name || !toolBlock.parameters) return [];
|
|
20
|
+
try {
|
|
21
|
+
// Use local transformation with JSON parsing and type guards
|
|
22
|
+
return transformToolBlockToChanges(toolBlock.name, toolBlock.parameters);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn("Error transforming tool block to changes:", error);
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}, [toolBlock.name, toolBlock.parameters, showDiff]);
|
|
28
|
+
|
|
29
|
+
// Render word-level diff between two lines of text
|
|
30
|
+
const renderWordLevelDiff = (
|
|
31
|
+
oldLine: string,
|
|
32
|
+
newLine: string,
|
|
33
|
+
keyPrefix: string,
|
|
34
|
+
) => {
|
|
35
|
+
try {
|
|
36
|
+
const changes = diffWords(oldLine, newLine);
|
|
37
|
+
|
|
38
|
+
const removedParts: React.ReactNode[] = [];
|
|
39
|
+
const addedParts: React.ReactNode[] = [];
|
|
40
|
+
|
|
41
|
+
changes.forEach((part, index) => {
|
|
42
|
+
if (part.removed) {
|
|
43
|
+
removedParts.push(
|
|
44
|
+
<Text
|
|
45
|
+
key={`removed-${keyPrefix}-${index}`}
|
|
46
|
+
color="black"
|
|
47
|
+
backgroundColor="red"
|
|
48
|
+
>
|
|
49
|
+
{part.value}
|
|
50
|
+
</Text>,
|
|
51
|
+
);
|
|
52
|
+
} else if (part.added) {
|
|
53
|
+
addedParts.push(
|
|
54
|
+
<Text
|
|
55
|
+
key={`added-${keyPrefix}-${index}`}
|
|
56
|
+
color="black"
|
|
57
|
+
backgroundColor="green"
|
|
58
|
+
>
|
|
59
|
+
{part.value}
|
|
60
|
+
</Text>,
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
// Unchanged parts
|
|
64
|
+
removedParts.push(
|
|
65
|
+
<Text key={`removed-unchanged-${keyPrefix}-${index}`} color="red">
|
|
66
|
+
{part.value}
|
|
67
|
+
</Text>,
|
|
68
|
+
);
|
|
69
|
+
addedParts.push(
|
|
70
|
+
<Text key={`added-unchanged-${keyPrefix}-${index}`} color="green">
|
|
71
|
+
{part.value}
|
|
72
|
+
</Text>,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return { removedParts, addedParts };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn("Error rendering word-level diff:", error);
|
|
80
|
+
// Fallback to simple line display
|
|
81
|
+
return {
|
|
82
|
+
removedParts: [
|
|
83
|
+
<Text key={`fallback-removed-${keyPrefix}`} color="red">
|
|
84
|
+
{oldLine}
|
|
85
|
+
</Text>,
|
|
86
|
+
],
|
|
87
|
+
addedParts: [
|
|
88
|
+
<Text key={`fallback-added-${keyPrefix}`} color="green">
|
|
89
|
+
{newLine}
|
|
90
|
+
</Text>,
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Render expanded diff display using word-level diff for all changes
|
|
97
|
+
const renderExpandedDiff = () => {
|
|
98
|
+
try {
|
|
99
|
+
if (changes.length === 0) return null;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Box flexDirection="column">
|
|
103
|
+
{changes.map((change, changeIndex) => {
|
|
104
|
+
try {
|
|
105
|
+
// Get line-level diff to understand the structure
|
|
106
|
+
const lineDiffs = diffLines(
|
|
107
|
+
change.oldContent || "",
|
|
108
|
+
change.newContent || "",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const diffElements: React.ReactNode[] = [];
|
|
112
|
+
|
|
113
|
+
// Process line diffs and apply word-level diff to changed lines
|
|
114
|
+
lineDiffs.forEach((part, partIndex) => {
|
|
115
|
+
if (part.added) {
|
|
116
|
+
const lines = part.value
|
|
117
|
+
.split("\n")
|
|
118
|
+
.filter((line) => line !== "");
|
|
119
|
+
lines.forEach((line, lineIndex) => {
|
|
120
|
+
diffElements.push(
|
|
121
|
+
<Box
|
|
122
|
+
key={`add-${changeIndex}-${partIndex}-${lineIndex}`}
|
|
123
|
+
flexDirection="row"
|
|
124
|
+
>
|
|
125
|
+
<Text color="green">+</Text>
|
|
126
|
+
<Text color="green">{line}</Text>
|
|
127
|
+
</Box>,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
} else if (part.removed) {
|
|
131
|
+
const lines = part.value
|
|
132
|
+
.split("\n")
|
|
133
|
+
.filter((line) => line !== "");
|
|
134
|
+
lines.forEach((line, lineIndex) => {
|
|
135
|
+
diffElements.push(
|
|
136
|
+
<Box
|
|
137
|
+
key={`remove-${changeIndex}-${partIndex}-${lineIndex}`}
|
|
138
|
+
flexDirection="row"
|
|
139
|
+
>
|
|
140
|
+
<Text color="red">-</Text>
|
|
141
|
+
<Text color="red">{line}</Text>
|
|
142
|
+
</Box>,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
// Context lines - show unchanged content
|
|
147
|
+
const lines = part.value
|
|
148
|
+
.split("\n")
|
|
149
|
+
.filter((line) => line !== "");
|
|
150
|
+
lines.forEach((line, lineIndex) => {
|
|
151
|
+
diffElements.push(
|
|
152
|
+
<Box
|
|
153
|
+
key={`context-${changeIndex}-${partIndex}-${lineIndex}`}
|
|
154
|
+
flexDirection="row"
|
|
155
|
+
>
|
|
156
|
+
<Text color="white"> </Text>
|
|
157
|
+
<Text color="white">{line}</Text>
|
|
158
|
+
</Box>,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Now look for pairs of removed/added lines that can be word-diffed
|
|
165
|
+
const processedElements: React.ReactNode[] = [];
|
|
166
|
+
let i = 0;
|
|
167
|
+
|
|
168
|
+
while (i < diffElements.length) {
|
|
169
|
+
const current = diffElements[i];
|
|
170
|
+
const next =
|
|
171
|
+
i + 1 < diffElements.length ? diffElements[i + 1] : null;
|
|
172
|
+
|
|
173
|
+
// Check if we have a removed line followed by an added line
|
|
174
|
+
const currentKey = React.isValidElement(current)
|
|
175
|
+
? current.key
|
|
176
|
+
: "";
|
|
177
|
+
const nextKey = React.isValidElement(next) ? next.key : "";
|
|
178
|
+
|
|
179
|
+
const isCurrentRemoved =
|
|
180
|
+
typeof currentKey === "string" &&
|
|
181
|
+
currentKey.includes("remove-");
|
|
182
|
+
const isNextAdded =
|
|
183
|
+
typeof nextKey === "string" && nextKey.includes("add-");
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
isCurrentRemoved &&
|
|
187
|
+
isNextAdded &&
|
|
188
|
+
React.isValidElement(current) &&
|
|
189
|
+
React.isValidElement(next)
|
|
190
|
+
) {
|
|
191
|
+
// Extract the text content from the removed and added lines
|
|
192
|
+
const removedText = extractTextFromElement(current);
|
|
193
|
+
const addedText = extractTextFromElement(next);
|
|
194
|
+
|
|
195
|
+
if (removedText && addedText) {
|
|
196
|
+
// Apply word-level diff
|
|
197
|
+
const { removedParts, addedParts } = renderWordLevelDiff(
|
|
198
|
+
removedText,
|
|
199
|
+
addedText,
|
|
200
|
+
`word-${changeIndex}-${i}`,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
processedElements.push(
|
|
204
|
+
<Box
|
|
205
|
+
key={`word-diff-removed-${changeIndex}-${i}`}
|
|
206
|
+
flexDirection="row"
|
|
207
|
+
>
|
|
208
|
+
<Text color="red">-</Text>
|
|
209
|
+
{removedParts}
|
|
210
|
+
</Box>,
|
|
211
|
+
);
|
|
212
|
+
processedElements.push(
|
|
213
|
+
<Box
|
|
214
|
+
key={`word-diff-added-${changeIndex}-${i}`}
|
|
215
|
+
flexDirection="row"
|
|
216
|
+
>
|
|
217
|
+
<Text color="green">+</Text>
|
|
218
|
+
{addedParts}
|
|
219
|
+
</Box>,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
i += 2; // Skip the next element since we processed it
|
|
223
|
+
} else {
|
|
224
|
+
// Fallback to original elements
|
|
225
|
+
processedElements.push(current);
|
|
226
|
+
i += 1;
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
processedElements.push(current);
|
|
230
|
+
i += 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<Box key={changeIndex} flexDirection="column">
|
|
236
|
+
{processedElements}
|
|
237
|
+
</Box>
|
|
238
|
+
);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.warn(
|
|
241
|
+
`Error rendering diff for change ${changeIndex}:`,
|
|
242
|
+
error,
|
|
243
|
+
);
|
|
244
|
+
// Fallback to simple display
|
|
245
|
+
return (
|
|
246
|
+
<Box key={changeIndex} flexDirection="column">
|
|
247
|
+
<Text color="red">-{change.oldContent || ""}</Text>
|
|
248
|
+
<Text color="green">+{change.newContent || ""}</Text>
|
|
249
|
+
</Box>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
})}
|
|
253
|
+
</Box>
|
|
254
|
+
);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.warn("Error rendering expanded diff:", error);
|
|
257
|
+
return (
|
|
258
|
+
<Box>
|
|
259
|
+
<Text color="gray">Error rendering diff display</Text>
|
|
260
|
+
</Box>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Helper function to extract text content from a React element
|
|
266
|
+
const extractTextFromElement = (element: React.ReactNode): string | null => {
|
|
267
|
+
if (!React.isValidElement(element)) return null;
|
|
268
|
+
|
|
269
|
+
// Navigate through Box -> Text structure
|
|
270
|
+
const children = (
|
|
271
|
+
element.props as unknown as { children?: React.ReactNode[] }
|
|
272
|
+
).children;
|
|
273
|
+
if (Array.isArray(children) && children.length >= 2) {
|
|
274
|
+
const textElement = children[1]; // Second child should be the Text with content
|
|
275
|
+
if (
|
|
276
|
+
React.isValidElement(textElement) &&
|
|
277
|
+
(textElement.props as unknown as { children?: string }).children
|
|
278
|
+
) {
|
|
279
|
+
return (textElement.props as unknown as { children: string }).children;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Don't render anything if no diff should be shown
|
|
286
|
+
if (!showDiff) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<Box flexDirection="column">
|
|
292
|
+
<Box paddingLeft={2} borderLeft borderColor="cyan" flexDirection="column">
|
|
293
|
+
<Text color="cyan" bold>
|
|
294
|
+
Diff:
|
|
295
|
+
</Text>
|
|
296
|
+
{renderExpandedDiff()}
|
|
297
|
+
</Box>
|
|
298
|
+
</Box>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import type { FileItem } from "wave-agent-sdk";
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
path: string;
|
|
6
|
-
type: "file" | "directory";
|
|
7
|
-
}
|
|
5
|
+
export { type FileItem } from "wave-agent-sdk";
|
|
8
6
|
|
|
9
7
|
export interface FileSelectorProps {
|
|
10
8
|
files: FileItem[];
|
|
@@ -8,6 +8,7 @@ import { MemoryTypeSelector } from "./MemoryTypeSelector.js";
|
|
|
8
8
|
import { BashShellManager } from "./BashShellManager.js";
|
|
9
9
|
import { McpManager } from "./McpManager.js";
|
|
10
10
|
import { useInputManager } from "../hooks/useInputManager.js";
|
|
11
|
+
import { useChat } from "../contexts/useChat.js";
|
|
11
12
|
|
|
12
13
|
import type { McpServerStatus, SlashCommand } from "wave-agent-sdk";
|
|
13
14
|
|
|
@@ -56,6 +57,11 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
56
57
|
// Get current working directory - memoized to avoid repeated process.cwd() calls
|
|
57
58
|
const currentWorkdir = useMemo(() => workdir || process.cwd(), [workdir]);
|
|
58
59
|
|
|
60
|
+
const {
|
|
61
|
+
permissionMode: chatPermissionMode,
|
|
62
|
+
setPermissionMode: setChatPermissionMode,
|
|
63
|
+
} = useChat();
|
|
64
|
+
|
|
59
65
|
// Input manager with all input state and functionality (including images)
|
|
60
66
|
const {
|
|
61
67
|
inputText,
|
|
@@ -90,6 +96,9 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
90
96
|
showMcpManager,
|
|
91
97
|
setShowBashManager,
|
|
92
98
|
setShowMcpManager,
|
|
99
|
+
// Permission mode
|
|
100
|
+
permissionMode,
|
|
101
|
+
setPermissionMode,
|
|
93
102
|
// Input history
|
|
94
103
|
setUserInputHistory,
|
|
95
104
|
// Complex handlers combining multiple operations
|
|
@@ -103,8 +112,14 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
103
112
|
onHasSlashCommand: hasSlashCommand,
|
|
104
113
|
onSaveMemory: saveMemory,
|
|
105
114
|
onAbortMessage: abortMessage,
|
|
115
|
+
onPermissionModeChange: setChatPermissionMode,
|
|
106
116
|
});
|
|
107
117
|
|
|
118
|
+
// Sync permission mode from useChat to InputManager
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
setPermissionMode(chatPermissionMode);
|
|
121
|
+
}, [chatPermissionMode, setPermissionMode]);
|
|
122
|
+
|
|
108
123
|
// Set user input history when it changes
|
|
109
124
|
useEffect(() => {
|
|
110
125
|
setUserInputHistory(userInputHistory);
|
|
@@ -198,20 +213,28 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
198
213
|
/>
|
|
199
214
|
)}
|
|
200
215
|
{showBashManager || showMcpManager || (
|
|
201
|
-
<Box
|
|
202
|
-
<
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
<Box flexDirection="column">
|
|
217
|
+
<Box borderStyle="single" borderColor="gray" paddingX={1}>
|
|
218
|
+
<Text color={isPlaceholder ? "gray" : "white"}>
|
|
219
|
+
{shouldShowCursor ? (
|
|
220
|
+
<>
|
|
221
|
+
{beforeCursor}
|
|
222
|
+
<Text backgroundColor="white" color="black">
|
|
223
|
+
{atCursor}
|
|
224
|
+
</Text>
|
|
225
|
+
{afterCursor}
|
|
226
|
+
</>
|
|
227
|
+
) : (
|
|
228
|
+
displayText
|
|
229
|
+
)}
|
|
230
|
+
</Text>
|
|
231
|
+
</Box>
|
|
232
|
+
<Box paddingX={1}>
|
|
233
|
+
<Text color="gray">
|
|
234
|
+
Mode: <Text color="cyan">{permissionMode}</Text> (Shift+Tab to
|
|
235
|
+
cycle)
|
|
236
|
+
</Text>
|
|
237
|
+
</Box>
|
|
215
238
|
</Box>
|
|
216
239
|
)}
|
|
217
240
|
</Box>
|
|
@@ -25,7 +25,7 @@ export const MemoryDisplay: React.FC<MemoryDisplayProps> = ({ block }) => {
|
|
|
25
25
|
if (!isSuccess) return null;
|
|
26
26
|
|
|
27
27
|
if (memoryType === "user") {
|
|
28
|
-
return `Memory saved to ${storagePath || "
|
|
28
|
+
return `Memory saved to ${storagePath || "AGENTS.md"}`;
|
|
29
29
|
} else {
|
|
30
30
|
return `Memory saved to ${storagePath || "AGENTS.md"}`;
|
|
31
31
|
}
|