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.
Files changed (31) hide show
  1. package/dist/components/ChatInterface.d.ts.map +1 -1
  2. package/dist/components/ChatInterface.js +1 -1
  3. package/dist/components/ConfirmationSelector.d.ts +0 -1
  4. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  5. package/dist/components/ConfirmationSelector.js +111 -259
  6. package/dist/components/MessageList.d.ts.map +1 -1
  7. package/dist/components/MessageList.js +4 -6
  8. package/dist/components/TaskNotificationMessage.js +1 -1
  9. package/dist/contexts/useChat.d.ts.map +1 -1
  10. package/dist/contexts/useChat.js +3 -4
  11. package/dist/managers/inputHandlers.d.ts.map +1 -1
  12. package/dist/managers/inputHandlers.js +6 -9
  13. package/dist/managers/inputReducer.d.ts +6 -0
  14. package/dist/managers/inputReducer.d.ts.map +1 -1
  15. package/dist/managers/inputReducer.js +15 -0
  16. package/dist/reducers/confirmationReducer.d.ts +26 -0
  17. package/dist/reducers/confirmationReducer.d.ts.map +1 -0
  18. package/dist/reducers/confirmationReducer.js +45 -0
  19. package/dist/reducers/questionReducer.d.ts +69 -0
  20. package/dist/reducers/questionReducer.d.ts.map +1 -0
  21. package/dist/reducers/questionReducer.js +195 -0
  22. package/package.json +2 -2
  23. package/src/components/ChatInterface.tsx +0 -1
  24. package/src/components/ConfirmationSelector.tsx +122 -304
  25. package/src/components/MessageList.tsx +10 -14
  26. package/src/components/TaskNotificationMessage.tsx +1 -1
  27. package/src/contexts/useChat.tsx +3 -4
  28. package/src/managers/inputHandlers.ts +6 -8
  29. package/src/managers/inputReducer.ts +19 -0
  30. package/src/reducers/confirmationReducer.ts +74 -0
  31. package/src/reducers/questionReducer.ts +251 -0
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef, useState } from "react";
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, setState] = useState<ConfirmationState>({
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 [questionState, setQuestionState] = useState({
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: {} as Record<string, string>,
59
+ userAnswers: {},
60
60
  otherText: "",
61
61
  otherCursorPosition: 0,
62
- savedStates: {} as Record<
63
- number,
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
- const pendingDecisionRef = useRef<PermissionDecision | null>(null);
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 (pendingDecisionRef.current) {
77
- const decision = pendingDecisionRef.current;
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
- setQuestionState((prev) => {
121
- const isOtherFocused =
122
- prev.selectedOptionIndex === options.length - 1;
123
- let answer = "";
124
- if (isMultiSelect) {
125
- const selectedLabels = Array.from(prev.selectedOptionIndices)
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
- setQuestionState((prev) => {
228
- const isOtherFocused =
229
- prev.selectedOptionIndex === options.length - 1;
230
- if (
231
- isMultiSelect &&
232
- (!isOtherFocused ||
233
- !prev.selectedOptionIndices.has(prev.selectedOptionIndex))
234
- ) {
235
- const nextIndices = new Set(prev.selectedOptionIndices);
236
- if (nextIndices.has(prev.selectedOptionIndex))
237
- nextIndices.delete(prev.selectedOptionIndex);
238
- else nextIndices.add(prev.selectedOptionIndex);
239
- return {
240
- ...prev,
241
- selectedOptionIndices: nextIndices,
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
- setQuestionState((prev) => ({
251
- ...prev,
252
- selectedOptionIndex: Math.max(0, prev.selectedOptionIndex - 1),
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
- setQuestionState((prev) => ({
258
- ...prev,
259
- selectedOptionIndex: Math.min(
260
- options.length - 1,
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
- setQuestionState((prev) => {
268
- const direction = key.shift ? -1 : 1;
269
- let nextIndex = prev.currentQuestionIndex + direction;
270
- if (nextIndex < 0) nextIndex = questions.length - 1;
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
- setQuestionState((prev) => {
303
- const isOtherFocused = prev.selectedOptionIndex === options.length - 1;
304
- if (isOtherFocused) {
305
- if (key.leftArrow) {
306
- return {
307
- ...prev,
308
- otherCursorPosition: Math.max(0, prev.otherCursorPosition - 1),
309
- };
310
- }
311
- if (key.rightArrow) {
312
- return {
313
- ...prev,
314
- otherCursorPosition: Math.min(
315
- prev.otherText.length,
316
- prev.otherCursorPosition + 1,
317
- ),
318
- };
319
- }
320
- if (key.backspace || key.delete) {
321
- if (prev.otherCursorPosition > 0) {
322
- return {
323
- ...prev,
324
- otherText:
325
- prev.otherText.slice(0, prev.otherCursorPosition - 1) +
326
- prev.otherText.slice(prev.otherCursorPosition),
327
- otherCursorPosition: prev.otherCursorPosition - 1,
328
- };
329
- }
330
- }
331
- if (input && !key.ctrl && !key.meta) {
332
- return {
333
- ...prev,
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
- onDecision({
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
- onDecision({ behavior: "allow", newPermissionMode: "default" });
210
+ decision = { behavior: "allow", newPermissionMode: "default" };
357
211
  } else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
358
- onDecision({ behavior: "allow", newPermissionMode: "plan" });
212
+ decision = { behavior: "allow", newPermissionMode: "plan" };
359
213
  } else {
360
- onDecision({ behavior: "allow" });
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
- onDecision({ behavior: "allow", newPermissionMode: "acceptEdits" });
220
+ decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
367
221
  } else {
368
222
  const rule = suggestedPrefix
369
223
  ? `Bash(${suggestedPrefix})`
370
224
  : `Bash(${toolInput?.command})`;
371
- onDecision({ behavior: "allow", newPermissionRule: rule });
225
+ decision = { behavior: "allow", newPermissionRule: rule };
372
226
  }
373
227
  } else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
374
- onDecision({ behavior: "allow", newPermissionMode: "plan" });
228
+ decision = { behavior: "allow", newPermissionMode: "plan" };
375
229
  } else if (toolName.startsWith("mcp__")) {
376
- onDecision({ behavior: "allow", newPermissionRule: toolName });
230
+ decision = { behavior: "allow", newPermissionRule: toolName };
377
231
  } else {
378
- onDecision({ behavior: "allow", newPermissionMode: "acceptEdits" });
232
+ decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
379
233
  }
380
234
  } else if (state.alternativeText.trim()) {
381
- onDecision({ behavior: "deny", message: state.alternativeText.trim() });
235
+ decision = { behavior: "deny", message: state.alternativeText.trim() };
382
236
  } else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
383
- onDecision({
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
- setState((prev) => ({
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
- setState((prev) => ({
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
- setState((prev) => ({
424
- ...prev,
425
- selectedOption: availableOptions[currentIndex - 1],
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
- setState((prev) => ({
435
- ...prev,
436
- selectedOption: availableOptions[currentIndex + 1],
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
- setState((prev) => ({
449
- ...prev,
450
- selectedOption: availableOptions[nextIndex],
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
- setState((prev) => {
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
- setState((prev) => {
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
- // Filter out streaming text blocks to avoid frequent height changes,
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
- .filter(
54
- (block) => !(block.type === "text" && block.stage === "streaming"),
55
- )
56
- .map((block, blockIndex) => ({
57
- block,
58
- message,
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
@@ -20,7 +20,7 @@ export const TaskNotificationMessage = ({
20
20
  return (
21
21
  <Box flexDirection="column">
22
22
  <Box>
23
- <Text color={color}>● </Text>
23
+ <Text color={color}>◆ </Text>
24
24
  <Text color="white">{block.summary}</Text>
25
25
  </Box>
26
26
  </Box>
@@ -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
- if (isConfirmationVisible) {
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
- if (!state.isPasting) {
301
- dispatch({
302
- type: "START_PASTE",
303
- payload: { buffer: inputString, cursorPosition: state.cursorPosition },
304
- });
305
- } else {
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) {