wave-code 0.13.5 → 0.14.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 +1 -1
- package/dist/components/ConfirmationSelector.d.ts +0 -1
- package/dist/components/ConfirmationSelector.d.ts.map +1 -1
- package/dist/components/ConfirmationSelector.js +111 -259
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +4 -6
- package/dist/components/TaskNotificationMessage.js +1 -1
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +3 -4
- package/dist/managers/inputHandlers.d.ts.map +1 -1
- package/dist/managers/inputHandlers.js +6 -9
- package/dist/managers/inputReducer.d.ts +6 -0
- package/dist/managers/inputReducer.d.ts.map +1 -1
- package/dist/managers/inputReducer.js +15 -0
- package/dist/reducers/confirmationReducer.d.ts +26 -0
- package/dist/reducers/confirmationReducer.d.ts.map +1 -0
- package/dist/reducers/confirmationReducer.js +45 -0
- package/dist/reducers/questionReducer.d.ts +69 -0
- package/dist/reducers/questionReducer.d.ts.map +1 -0
- package/dist/reducers/questionReducer.js +195 -0
- package/package.json +2 -2
- package/src/components/ChatInterface.tsx +0 -1
- package/src/components/ConfirmationSelector.tsx +122 -304
- package/src/components/MessageList.tsx +10 -14
- package/src/components/TaskNotificationMessage.tsx +1 -1
- package/src/contexts/useChat.tsx +3 -4
- package/src/managers/inputHandlers.ts +6 -8
- package/src/managers/inputReducer.ts +19 -0
- package/src/reducers/confirmationReducer.ts +74 -0
- package/src/reducers/questionReducer.ts +251 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect,
|
|
1
|
+
import React, { useEffect, useReducer } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import type { PermissionDecision, AskUserQuestionInput } from "wave-agent-sdk";
|
|
4
4
|
import {
|
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
ENTER_PLAN_MODE_TOOL_NAME,
|
|
8
8
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
9
9
|
} from "wave-agent-sdk";
|
|
10
|
+
import {
|
|
11
|
+
confirmationReducer,
|
|
12
|
+
type ConfirmationState,
|
|
13
|
+
} from "../reducers/confirmationReducer.js";
|
|
14
|
+
import { questionReducer } from "../reducers/questionReducer.js";
|
|
10
15
|
|
|
11
16
|
const getHeaderColor = (header: string) => {
|
|
12
17
|
const colors = ["red", "green", "blue", "magenta", "cyan"] as const;
|
|
@@ -25,14 +30,6 @@ export interface ConfirmationSelectorProps {
|
|
|
25
30
|
isExpanded?: boolean;
|
|
26
31
|
onDecision: (decision: PermissionDecision) => void;
|
|
27
32
|
onCancel: () => void;
|
|
28
|
-
onAbort: () => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface ConfirmationState {
|
|
32
|
-
selectedOption: "clear" | "auto" | "allow" | "alternative";
|
|
33
|
-
alternativeText: string;
|
|
34
|
-
alternativeCursorPosition: number;
|
|
35
|
-
hasUserInput: boolean;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
@@ -43,45 +40,42 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
43
40
|
isExpanded = false,
|
|
44
41
|
onDecision,
|
|
45
42
|
onCancel,
|
|
46
|
-
onAbort,
|
|
47
43
|
}) => {
|
|
48
|
-
const [state,
|
|
44
|
+
const [state, dispatch] = useReducer(confirmationReducer, {
|
|
49
45
|
selectedOption: toolName === EXIT_PLAN_MODE_TOOL_NAME ? "clear" : "allow",
|
|
50
46
|
alternativeText: "",
|
|
51
47
|
alternativeCursorPosition: 0,
|
|
52
48
|
hasUserInput: false,
|
|
49
|
+
decision: null,
|
|
53
50
|
});
|
|
54
51
|
|
|
55
|
-
const
|
|
52
|
+
const questions =
|
|
53
|
+
(toolInput as unknown as AskUserQuestionInput)?.questions || [];
|
|
54
|
+
|
|
55
|
+
const [questionState, questionDispatch] = useReducer(questionReducer, {
|
|
56
56
|
currentQuestionIndex: 0,
|
|
57
57
|
selectedOptionIndex: 0,
|
|
58
58
|
selectedOptionIndices: new Set<number>(),
|
|
59
|
-
userAnswers: {}
|
|
59
|
+
userAnswers: {},
|
|
60
60
|
otherText: "",
|
|
61
61
|
otherCursorPosition: 0,
|
|
62
|
-
savedStates: {}
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
selectedOptionIndex: number;
|
|
66
|
-
selectedOptionIndices: Set<number>;
|
|
67
|
-
otherText: string;
|
|
68
|
-
otherCursorPosition: number;
|
|
69
|
-
}
|
|
70
|
-
>,
|
|
62
|
+
savedStates: {},
|
|
63
|
+
decision: null,
|
|
71
64
|
});
|
|
72
65
|
|
|
73
|
-
|
|
66
|
+
// Handle decisions from reducers
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (state.decision) {
|
|
69
|
+
onDecision(state.decision);
|
|
70
|
+
}
|
|
71
|
+
}, [state.decision, onDecision]);
|
|
74
72
|
|
|
75
73
|
useEffect(() => {
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
pendingDecisionRef.current = null;
|
|
79
|
-
onDecision(decision);
|
|
74
|
+
if (questionState.decision) {
|
|
75
|
+
onDecision(questionState.decision);
|
|
80
76
|
}
|
|
81
|
-
});
|
|
77
|
+
}, [questionState.decision, onDecision]);
|
|
82
78
|
|
|
83
|
-
const questions =
|
|
84
|
-
(toolInput as unknown as AskUserQuestionInput)?.questions || [];
|
|
85
79
|
const currentQuestion = questions[questionState.currentQuestionIndex];
|
|
86
80
|
|
|
87
81
|
const getAutoOptionText = () => {
|
|
@@ -107,7 +101,6 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
107
101
|
useInput((input, key) => {
|
|
108
102
|
if (key.escape) {
|
|
109
103
|
onCancel();
|
|
110
|
-
onAbort();
|
|
111
104
|
return;
|
|
112
105
|
}
|
|
113
106
|
|
|
@@ -117,296 +110,148 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
117
110
|
const isMultiSelect = currentQuestion.multiSelect;
|
|
118
111
|
|
|
119
112
|
if (key.return) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
.filter((i) => i < currentQuestion.options.length)
|
|
127
|
-
.map((i) => currentQuestion.options[i].label);
|
|
128
|
-
const isOtherChecked = prev.selectedOptionIndices.has(
|
|
129
|
-
options.length - 1,
|
|
130
|
-
);
|
|
131
|
-
if (isOtherChecked && prev.otherText.trim()) {
|
|
132
|
-
selectedLabels.push(prev.otherText.trim());
|
|
133
|
-
}
|
|
134
|
-
answer = selectedLabels.join(", ");
|
|
135
|
-
} else {
|
|
136
|
-
if (isOtherFocused) {
|
|
137
|
-
answer = prev.otherText.trim();
|
|
138
|
-
} else {
|
|
139
|
-
answer = options[prev.selectedOptionIndex].label;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!answer) return prev;
|
|
144
|
-
|
|
145
|
-
const newAnswers = {
|
|
146
|
-
...prev.userAnswers,
|
|
147
|
-
[currentQuestion.question]: answer,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
if (prev.currentQuestionIndex < questions.length - 1) {
|
|
151
|
-
const nextIndex = prev.currentQuestionIndex + 1;
|
|
152
|
-
const savedStates = {
|
|
153
|
-
...prev.savedStates,
|
|
154
|
-
[prev.currentQuestionIndex]: {
|
|
155
|
-
selectedOptionIndex: prev.selectedOptionIndex,
|
|
156
|
-
selectedOptionIndices: prev.selectedOptionIndices,
|
|
157
|
-
otherText: prev.otherText,
|
|
158
|
-
otherCursorPosition: prev.otherCursorPosition,
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const nextState = savedStates[nextIndex] || {
|
|
163
|
-
selectedOptionIndex: 0,
|
|
164
|
-
selectedOptionIndices: new Set<number>(),
|
|
165
|
-
otherText: "",
|
|
166
|
-
otherCursorPosition: 0,
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
...prev,
|
|
171
|
-
currentQuestionIndex: nextIndex,
|
|
172
|
-
...nextState,
|
|
173
|
-
userAnswers: newAnswers,
|
|
174
|
-
savedStates,
|
|
175
|
-
};
|
|
176
|
-
} else {
|
|
177
|
-
const finalAnswers = { ...newAnswers };
|
|
178
|
-
// Also collect from savedStates for any questions that were skipped via Tab
|
|
179
|
-
for (const [idxStr, s] of Object.entries(prev.savedStates)) {
|
|
180
|
-
const idx = parseInt(idxStr);
|
|
181
|
-
const q = questions[idx];
|
|
182
|
-
if (q && !finalAnswers[q.question]) {
|
|
183
|
-
const opts = [...q.options, { label: "Other" }];
|
|
184
|
-
let a = "";
|
|
185
|
-
if (q.multiSelect) {
|
|
186
|
-
const selectedLabels = Array.from(s.selectedOptionIndices)
|
|
187
|
-
.filter((i) => i < q.options.length)
|
|
188
|
-
.map((i) => q.options[i].label);
|
|
189
|
-
const isOtherChecked = s.selectedOptionIndices.has(
|
|
190
|
-
opts.length - 1,
|
|
191
|
-
);
|
|
192
|
-
if (isOtherChecked && s.otherText.trim()) {
|
|
193
|
-
selectedLabels.push(s.otherText.trim());
|
|
194
|
-
}
|
|
195
|
-
a = selectedLabels.join(", ");
|
|
196
|
-
} else {
|
|
197
|
-
if (s.selectedOptionIndex === opts.length - 1) {
|
|
198
|
-
a = s.otherText.trim();
|
|
199
|
-
} else {
|
|
200
|
-
a = opts[s.selectedOptionIndex].label;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (a) finalAnswers[q.question] = a;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Only submit if all questions have been answered
|
|
208
|
-
const allAnswered = questions.every(
|
|
209
|
-
(q) => finalAnswers[q.question],
|
|
210
|
-
);
|
|
211
|
-
if (!allAnswered) return prev;
|
|
212
|
-
|
|
213
|
-
pendingDecisionRef.current = {
|
|
214
|
-
behavior: "allow",
|
|
215
|
-
message: JSON.stringify(finalAnswers),
|
|
216
|
-
};
|
|
217
|
-
return {
|
|
218
|
-
...prev,
|
|
219
|
-
userAnswers: finalAnswers,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
113
|
+
questionDispatch({
|
|
114
|
+
type: "CONFIRM_ANSWER",
|
|
115
|
+
currentQuestion,
|
|
116
|
+
options,
|
|
117
|
+
isMultiSelect: !!isMultiSelect,
|
|
118
|
+
questions,
|
|
222
119
|
});
|
|
223
120
|
return;
|
|
224
121
|
}
|
|
225
122
|
|
|
226
123
|
if (input === " ") {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
return prev;
|
|
245
|
-
});
|
|
124
|
+
const isOtherFocused =
|
|
125
|
+
questionState.selectedOptionIndex === options.length - 1;
|
|
126
|
+
if (
|
|
127
|
+
isMultiSelect &&
|
|
128
|
+
(!isOtherFocused ||
|
|
129
|
+
!questionState.selectedOptionIndices.has(
|
|
130
|
+
questionState.selectedOptionIndex,
|
|
131
|
+
))
|
|
132
|
+
) {
|
|
133
|
+
questionDispatch({
|
|
134
|
+
type: "TOGGLE_MULTI_SELECT",
|
|
135
|
+
optionsLength: options.length,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
246
139
|
// If it's other and focused, we don't return here, allowing the input handler below to handle it
|
|
247
140
|
}
|
|
248
141
|
|
|
249
142
|
if (key.upArrow) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
})
|
|
143
|
+
questionDispatch({
|
|
144
|
+
type: "MOVE_OPTION_UP",
|
|
145
|
+
maxIndex: options.length - 1,
|
|
146
|
+
});
|
|
254
147
|
return;
|
|
255
148
|
}
|
|
256
149
|
if (key.downArrow) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
prev.selectedOptionIndex + 1,
|
|
262
|
-
),
|
|
263
|
-
}));
|
|
150
|
+
questionDispatch({
|
|
151
|
+
type: "MOVE_OPTION_DOWN",
|
|
152
|
+
maxIndex: options.length - 1,
|
|
153
|
+
});
|
|
264
154
|
return;
|
|
265
155
|
}
|
|
266
156
|
if (key.tab) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (nextIndex >= questions.length) nextIndex = 0;
|
|
272
|
-
|
|
273
|
-
if (nextIndex === prev.currentQuestionIndex) return prev;
|
|
274
|
-
|
|
275
|
-
const savedStates = {
|
|
276
|
-
...prev.savedStates,
|
|
277
|
-
[prev.currentQuestionIndex]: {
|
|
278
|
-
selectedOptionIndex: prev.selectedOptionIndex,
|
|
279
|
-
selectedOptionIndices: prev.selectedOptionIndices,
|
|
280
|
-
otherText: prev.otherText,
|
|
281
|
-
otherCursorPosition: prev.otherCursorPosition,
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const nextState = savedStates[nextIndex] || {
|
|
286
|
-
selectedOptionIndex: 0,
|
|
287
|
-
selectedOptionIndices: new Set<number>(),
|
|
288
|
-
otherText: "",
|
|
289
|
-
otherCursorPosition: 0,
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
...prev,
|
|
294
|
-
currentQuestionIndex: nextIndex,
|
|
295
|
-
...nextState,
|
|
296
|
-
savedStates,
|
|
297
|
-
};
|
|
157
|
+
questionDispatch({
|
|
158
|
+
type: "CYCLE_QUESTION",
|
|
159
|
+
shift: key.shift,
|
|
160
|
+
questionCount: questions.length,
|
|
298
161
|
});
|
|
299
162
|
return;
|
|
300
163
|
}
|
|
301
164
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
otherText:
|
|
335
|
-
prev.otherText.slice(0, prev.otherCursorPosition) +
|
|
336
|
-
input +
|
|
337
|
-
prev.otherText.slice(prev.otherCursorPosition),
|
|
338
|
-
otherCursorPosition: prev.otherCursorPosition + input.length,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return prev;
|
|
343
|
-
});
|
|
165
|
+
// Always dispatch Other actions unconditionally; the reducer checks
|
|
166
|
+
// isOtherFocused from the latest state, so this works correctly even
|
|
167
|
+
// when inputs are batched before React re-renders.
|
|
168
|
+
if (key.leftArrow) {
|
|
169
|
+
questionDispatch({
|
|
170
|
+
type: "MOVE_OTHER_LEFT",
|
|
171
|
+
optionsLength: options.length,
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (key.rightArrow) {
|
|
176
|
+
questionDispatch({
|
|
177
|
+
type: "MOVE_OTHER_RIGHT",
|
|
178
|
+
optionsLength: options.length,
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (key.backspace || key.delete) {
|
|
183
|
+
questionDispatch({
|
|
184
|
+
type: "DELETE_OTHER",
|
|
185
|
+
optionsLength: options.length,
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (input && !key.ctrl && !key.meta) {
|
|
190
|
+
questionDispatch({
|
|
191
|
+
type: "INSERT_OTHER",
|
|
192
|
+
text: input,
|
|
193
|
+
optionsLength: options.length,
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
344
197
|
return;
|
|
345
198
|
}
|
|
346
199
|
|
|
347
200
|
if (key.return) {
|
|
201
|
+
let decision: PermissionDecision | null = null;
|
|
348
202
|
if (state.selectedOption === "clear") {
|
|
349
|
-
|
|
203
|
+
decision = {
|
|
350
204
|
behavior: "allow",
|
|
351
205
|
newPermissionMode: "acceptEdits",
|
|
352
206
|
clearContext: true,
|
|
353
|
-
}
|
|
207
|
+
};
|
|
354
208
|
} else if (state.selectedOption === "allow") {
|
|
355
209
|
if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
|
|
356
|
-
|
|
210
|
+
decision = { behavior: "allow", newPermissionMode: "default" };
|
|
357
211
|
} else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
|
|
358
|
-
|
|
212
|
+
decision = { behavior: "allow", newPermissionMode: "plan" };
|
|
359
213
|
} else {
|
|
360
|
-
|
|
214
|
+
decision = { behavior: "allow" };
|
|
361
215
|
}
|
|
362
216
|
} else if (state.selectedOption === "auto") {
|
|
363
217
|
if (toolName === BASH_TOOL_NAME) {
|
|
364
218
|
const command = (toolInput?.command as string) || "";
|
|
365
219
|
if (command.trim().startsWith("mkdir")) {
|
|
366
|
-
|
|
220
|
+
decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
|
|
367
221
|
} else {
|
|
368
222
|
const rule = suggestedPrefix
|
|
369
223
|
? `Bash(${suggestedPrefix})`
|
|
370
224
|
: `Bash(${toolInput?.command})`;
|
|
371
|
-
|
|
225
|
+
decision = { behavior: "allow", newPermissionRule: rule };
|
|
372
226
|
}
|
|
373
227
|
} else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
|
|
374
|
-
|
|
228
|
+
decision = { behavior: "allow", newPermissionMode: "plan" };
|
|
375
229
|
} else if (toolName.startsWith("mcp__")) {
|
|
376
|
-
|
|
230
|
+
decision = { behavior: "allow", newPermissionRule: toolName };
|
|
377
231
|
} else {
|
|
378
|
-
|
|
232
|
+
decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
|
|
379
233
|
}
|
|
380
234
|
} else if (state.alternativeText.trim()) {
|
|
381
|
-
|
|
235
|
+
decision = { behavior: "deny", message: state.alternativeText.trim() };
|
|
382
236
|
} else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
|
|
383
|
-
|
|
237
|
+
decision = {
|
|
384
238
|
behavior: "deny",
|
|
385
239
|
message: "User chose not to enter plan mode",
|
|
386
|
-
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (decision) {
|
|
243
|
+
dispatch({ type: "CONFIRM", decision });
|
|
387
244
|
}
|
|
388
245
|
return;
|
|
389
246
|
}
|
|
390
247
|
|
|
391
248
|
if (state.selectedOption === "alternative") {
|
|
392
249
|
if (key.leftArrow) {
|
|
393
|
-
|
|
394
|
-
...prev,
|
|
395
|
-
alternativeCursorPosition: Math.max(
|
|
396
|
-
0,
|
|
397
|
-
prev.alternativeCursorPosition - 1,
|
|
398
|
-
),
|
|
399
|
-
}));
|
|
250
|
+
dispatch({ type: "MOVE_CURSOR_LEFT" });
|
|
400
251
|
return;
|
|
401
252
|
}
|
|
402
253
|
if (key.rightArrow) {
|
|
403
|
-
|
|
404
|
-
...prev,
|
|
405
|
-
alternativeCursorPosition: Math.min(
|
|
406
|
-
prev.alternativeText.length,
|
|
407
|
-
prev.alternativeCursorPosition + 1,
|
|
408
|
-
),
|
|
409
|
-
}));
|
|
254
|
+
dispatch({ type: "MOVE_CURSOR_RIGHT" });
|
|
410
255
|
return;
|
|
411
256
|
}
|
|
412
257
|
}
|
|
@@ -420,10 +265,10 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
420
265
|
if (key.upArrow) {
|
|
421
266
|
const currentIndex = availableOptions.indexOf(state.selectedOption);
|
|
422
267
|
if (currentIndex > 0) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
})
|
|
268
|
+
dispatch({
|
|
269
|
+
type: "SELECT_OPTION",
|
|
270
|
+
option: availableOptions[currentIndex - 1],
|
|
271
|
+
});
|
|
427
272
|
}
|
|
428
273
|
return;
|
|
429
274
|
}
|
|
@@ -431,10 +276,10 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
431
276
|
if (key.downArrow) {
|
|
432
277
|
const currentIndex = availableOptions.indexOf(state.selectedOption);
|
|
433
278
|
if (currentIndex < availableOptions.length - 1) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
})
|
|
279
|
+
dispatch({
|
|
280
|
+
type: "SELECT_OPTION",
|
|
281
|
+
option: availableOptions[currentIndex + 1],
|
|
282
|
+
});
|
|
438
283
|
}
|
|
439
284
|
return;
|
|
440
285
|
}
|
|
@@ -445,47 +290,20 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
445
290
|
let nextIndex = currentIndex + direction;
|
|
446
291
|
if (nextIndex < 0) nextIndex = availableOptions.length - 1;
|
|
447
292
|
if (nextIndex >= availableOptions.length) nextIndex = 0;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
})
|
|
293
|
+
dispatch({
|
|
294
|
+
type: "SELECT_OPTION",
|
|
295
|
+
option: availableOptions[nextIndex],
|
|
296
|
+
});
|
|
452
297
|
return;
|
|
453
298
|
}
|
|
454
299
|
|
|
455
300
|
if (input && !key.ctrl && !key.meta && !("alt" in key && key.alt)) {
|
|
456
|
-
|
|
457
|
-
const nextText =
|
|
458
|
-
prev.alternativeText.slice(0, prev.alternativeCursorPosition) +
|
|
459
|
-
input +
|
|
460
|
-
prev.alternativeText.slice(prev.alternativeCursorPosition);
|
|
461
|
-
return {
|
|
462
|
-
...prev,
|
|
463
|
-
selectedOption: "alternative",
|
|
464
|
-
alternativeText: nextText,
|
|
465
|
-
alternativeCursorPosition:
|
|
466
|
-
prev.alternativeCursorPosition + input.length,
|
|
467
|
-
hasUserInput: true,
|
|
468
|
-
};
|
|
469
|
-
});
|
|
301
|
+
dispatch({ type: "INSERT_TEXT", text: input });
|
|
470
302
|
return;
|
|
471
303
|
}
|
|
472
304
|
|
|
473
305
|
if (key.backspace || key.delete) {
|
|
474
|
-
|
|
475
|
-
if (prev.alternativeCursorPosition > 0) {
|
|
476
|
-
const nextText =
|
|
477
|
-
prev.alternativeText.slice(0, prev.alternativeCursorPosition - 1) +
|
|
478
|
-
prev.alternativeText.slice(prev.alternativeCursorPosition);
|
|
479
|
-
return {
|
|
480
|
-
...prev,
|
|
481
|
-
selectedOption: "alternative",
|
|
482
|
-
alternativeText: nextText,
|
|
483
|
-
alternativeCursorPosition: prev.alternativeCursorPosition - 1,
|
|
484
|
-
hasUserInput: nextText.length > 0,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
return prev;
|
|
488
|
-
});
|
|
306
|
+
dispatch({ type: "BACKSPACE" });
|
|
489
307
|
return;
|
|
490
308
|
}
|
|
491
309
|
});
|
|
@@ -43,23 +43,19 @@ export const MessageList = React.memo(
|
|
|
43
43
|
b.stage === "streaming" ||
|
|
44
44
|
b.stage === "start")) ||
|
|
45
45
|
(b.type === "bang" && b.stage === "running") ||
|
|
46
|
-
(b.type === "reasoning" && b.stage === "streaming")
|
|
46
|
+
(b.type === "reasoning" && b.stage === "streaming") ||
|
|
47
|
+
(b.type === "text" && b.stage === "streaming");
|
|
47
48
|
|
|
48
49
|
// Flatten messages into blocks with metadata
|
|
49
|
-
//
|
|
50
|
-
// but allow streaming reasoning blocks (rendered as truncated gray text)
|
|
50
|
+
// Include streaming text blocks (rendered as truncated gray text)
|
|
51
51
|
const allBlocks = visibleMessages.flatMap((message, messageIndex) => {
|
|
52
|
-
return message.blocks
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
messageIndex,
|
|
60
|
-
// Unique key for each block to help Static component
|
|
61
|
-
key: `${message.id}-${blockIndex}`,
|
|
62
|
-
}));
|
|
52
|
+
return message.blocks.map((block, blockIndex) => ({
|
|
53
|
+
block,
|
|
54
|
+
message,
|
|
55
|
+
messageIndex,
|
|
56
|
+
// Unique key for each block to help Static component
|
|
57
|
+
key: `${message.id}-${blockIndex}`,
|
|
58
|
+
}));
|
|
63
59
|
});
|
|
64
60
|
|
|
65
61
|
// Find message indices that have any running/streaming block
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -613,6 +613,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
613
613
|
if (currentConfirmation) {
|
|
614
614
|
currentConfirmation.reject();
|
|
615
615
|
}
|
|
616
|
+
agentRef.current?.abortMessage();
|
|
616
617
|
hideConfirmation();
|
|
617
618
|
}, [currentConfirmation, hideConfirmation]);
|
|
618
619
|
|
|
@@ -697,10 +698,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
697
698
|
}
|
|
698
699
|
|
|
699
700
|
// Handle ESC key to cancel confirmation
|
|
700
|
-
if (key.escape) {
|
|
701
|
-
|
|
702
|
-
handleConfirmationCancel();
|
|
703
|
-
}
|
|
701
|
+
if (key.escape && isConfirmationVisible) {
|
|
702
|
+
handleConfirmationCancel();
|
|
704
703
|
}
|
|
705
704
|
});
|
|
706
705
|
|
|
@@ -297,14 +297,12 @@ export const handlePasteInput = (
|
|
|
297
297
|
inputString.includes("\r");
|
|
298
298
|
|
|
299
299
|
if (isPasteOperation) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
dispatch({ type: "APPEND_PASTE_BUFFER", payload: inputString });
|
|
307
|
-
}
|
|
300
|
+
// Dispatch a single action type; the reducer determines start vs append
|
|
301
|
+
// by checking pasteBuffer, avoiding stale state issues
|
|
302
|
+
dispatch({
|
|
303
|
+
type: "APPEND_PASTE_CHUNK",
|
|
304
|
+
payload: { chunk: inputString, cursorPosition: state.cursorPosition },
|
|
305
|
+
});
|
|
308
306
|
} else {
|
|
309
307
|
let char = inputString;
|
|
310
308
|
if (char === "!" && state.cursorPosition === 0) {
|