wave-code 0.13.6 → 0.14.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.
Files changed (80) hide show
  1. package/dist/components/BackgroundTaskManager.d.ts.map +1 -1
  2. package/dist/components/BackgroundTaskManager.js +36 -25
  3. package/dist/components/ChatInterface.d.ts.map +1 -1
  4. package/dist/components/ChatInterface.js +1 -1
  5. package/dist/components/ConfirmationSelector.d.ts +0 -1
  6. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  7. package/dist/components/ConfirmationSelector.js +111 -259
  8. package/dist/components/InputBox.d.ts.map +1 -1
  9. package/dist/components/InputBox.js +2 -2
  10. package/dist/components/MarketplaceAddForm.d.ts.map +1 -1
  11. package/dist/components/MarketplaceAddForm.js +48 -17
  12. package/dist/components/MarketplaceDetail.d.ts.map +1 -1
  13. package/dist/components/MarketplaceDetail.js +2 -1
  14. package/dist/components/MarketplaceList.d.ts.map +1 -1
  15. package/dist/components/MarketplaceList.js +2 -1
  16. package/dist/components/McpManager.d.ts.map +1 -1
  17. package/dist/components/McpManager.js +22 -27
  18. package/dist/components/MessageList.d.ts.map +1 -1
  19. package/dist/components/MessageList.js +4 -6
  20. package/dist/components/PluginDetail.d.ts.map +1 -1
  21. package/dist/components/PluginDetail.js +22 -22
  22. package/dist/components/PluginManagerShell.d.ts +1 -0
  23. package/dist/components/PluginManagerShell.d.ts.map +1 -1
  24. package/dist/components/PluginManagerShell.js +2 -2
  25. package/dist/components/PluginManagerTypes.d.ts +2 -2
  26. package/dist/components/PluginManagerTypes.d.ts.map +1 -1
  27. package/dist/components/TaskNotificationMessage.js +1 -1
  28. package/dist/contexts/useChat.d.ts +1 -0
  29. package/dist/contexts/useChat.d.ts.map +1 -1
  30. package/dist/contexts/useChat.js +128 -102
  31. package/dist/hooks/usePluginManager.d.ts +3 -1
  32. package/dist/hooks/usePluginManager.d.ts.map +1 -1
  33. package/dist/hooks/usePluginManager.js +16 -7
  34. package/dist/managers/inputHandlers.d.ts.map +1 -1
  35. package/dist/managers/inputHandlers.js +6 -9
  36. package/dist/managers/inputReducer.d.ts +6 -0
  37. package/dist/managers/inputReducer.d.ts.map +1 -1
  38. package/dist/managers/inputReducer.js +15 -0
  39. package/dist/reducers/backgroundTaskManagerReducer.d.ts +43 -0
  40. package/dist/reducers/backgroundTaskManagerReducer.d.ts.map +1 -0
  41. package/dist/reducers/backgroundTaskManagerReducer.js +23 -0
  42. package/dist/reducers/confirmationReducer.d.ts +26 -0
  43. package/dist/reducers/confirmationReducer.d.ts.map +1 -0
  44. package/dist/reducers/confirmationReducer.js +45 -0
  45. package/dist/reducers/marketplaceAddFormReducer.d.ts +24 -0
  46. package/dist/reducers/marketplaceAddFormReducer.d.ts.map +1 -0
  47. package/dist/reducers/marketplaceAddFormReducer.js +18 -0
  48. package/dist/reducers/mcpManagerReducer.d.ts +16 -0
  49. package/dist/reducers/mcpManagerReducer.d.ts.map +1 -0
  50. package/dist/reducers/mcpManagerReducer.js +15 -0
  51. package/dist/reducers/pluginDetailReducer.d.ts +25 -0
  52. package/dist/reducers/pluginDetailReducer.d.ts.map +1 -0
  53. package/dist/reducers/pluginDetailReducer.js +38 -0
  54. package/dist/reducers/questionReducer.d.ts +69 -0
  55. package/dist/reducers/questionReducer.d.ts.map +1 -0
  56. package/dist/reducers/questionReducer.js +195 -0
  57. package/package.json +2 -2
  58. package/src/components/BackgroundTaskManager.tsx +32 -34
  59. package/src/components/ChatInterface.tsx +0 -1
  60. package/src/components/ConfirmationSelector.tsx +122 -304
  61. package/src/components/InputBox.tsx +7 -1
  62. package/src/components/MarketplaceAddForm.tsx +76 -22
  63. package/src/components/MarketplaceDetail.tsx +4 -0
  64. package/src/components/MarketplaceList.tsx +4 -0
  65. package/src/components/McpManager.tsx +30 -34
  66. package/src/components/MessageList.tsx +10 -14
  67. package/src/components/PluginDetail.tsx +25 -33
  68. package/src/components/PluginManagerShell.tsx +3 -2
  69. package/src/components/PluginManagerTypes.ts +8 -2
  70. package/src/components/TaskNotificationMessage.tsx +1 -1
  71. package/src/contexts/useChat.tsx +57 -24
  72. package/src/hooks/usePluginManager.ts +18 -7
  73. package/src/managers/inputHandlers.ts +6 -8
  74. package/src/managers/inputReducer.ts +19 -0
  75. package/src/reducers/backgroundTaskManagerReducer.ts +60 -0
  76. package/src/reducers/confirmationReducer.ts +74 -0
  77. package/src/reducers/marketplaceAddFormReducer.ts +35 -0
  78. package/src/reducers/mcpManagerReducer.ts +31 -0
  79. package/src/reducers/pluginDetailReducer.ts +58 -0
  80. package/src/reducers/questionReducer.ts +251 -0
@@ -1 +1 @@
1
- {"version":3,"file":"BackgroundTaskManager.d.ts","sourceRoot":"","sources":["../../src/components/BackgroundTaskManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAgBnD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAyUtE,CAAC"}
1
+ {"version":3,"file":"BackgroundTaskManager.d.ts","sourceRoot":"","sources":["../../src/components/BackgroundTaskManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAgC,MAAM,OAAO,CAAC;AASrD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CA8UtE,CAAC"}
@@ -1,34 +1,41 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useEffect } from "react";
2
+ import { useEffect, useReducer } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
4
  import { useChat } from "../contexts/useChat.js";
5
5
  import { getLastLines } from "wave-agent-sdk";
6
+ import { backgroundTaskManagerReducer, } from "../reducers/backgroundTaskManagerReducer.js";
6
7
  export const BackgroundTaskManager = ({ onCancel, }) => {
7
8
  const { backgroundTasks, getBackgroundTaskOutput, stopBackgroundTask } = useChat();
8
- const [tasks, setTasks] = useState([]);
9
- const [selectedIndex, setSelectedIndex] = useState(0);
10
9
  const MAX_VISIBLE_ITEMS = 3;
11
- const [viewMode, setViewMode] = useState("list");
12
- const [detailTaskId, setDetailTaskId] = useState(null);
13
- const [detailOutput, setDetailOutput] = useState(null);
10
+ const [state, dispatch] = useReducer(backgroundTaskManagerReducer, {
11
+ tasks: [],
12
+ selectedIndex: 0,
13
+ viewMode: "list",
14
+ detailTaskId: null,
15
+ detailOutput: null,
16
+ });
17
+ const { tasks, selectedIndex, viewMode, detailTaskId, detailOutput } = state;
14
18
  // Convert backgroundTasks to local Task format
15
19
  useEffect(() => {
16
- setTasks(backgroundTasks.map((task) => ({
17
- id: task.id,
18
- type: task.type,
19
- description: task.description,
20
- status: task.status,
21
- startTime: task.startTime,
22
- exitCode: task.exitCode,
23
- runtime: task.runtime,
24
- outputPath: task.outputPath,
25
- })));
20
+ dispatch({
21
+ type: "SET_TASKS",
22
+ tasks: backgroundTasks.map((task) => ({
23
+ id: task.id,
24
+ type: task.type,
25
+ description: task.description,
26
+ status: task.status,
27
+ startTime: task.startTime,
28
+ exitCode: task.exitCode,
29
+ runtime: task.runtime,
30
+ outputPath: task.outputPath,
31
+ })),
32
+ });
26
33
  }, [backgroundTasks]);
27
34
  // Load detail output for selected task
28
35
  useEffect(() => {
29
36
  if (viewMode === "detail" && detailTaskId) {
30
37
  const output = getBackgroundTaskOutput(detailTaskId);
31
- setDetailOutput(output);
38
+ dispatch({ type: "SET_DETAIL_OUTPUT", output });
32
39
  }
33
40
  }, [viewMode, detailTaskId, getBackgroundTaskOutput]);
34
41
  const formatDuration = (ms) => {
@@ -55,8 +62,8 @@ export const BackgroundTaskManager = ({ onCancel, }) => {
55
62
  if (key.return) {
56
63
  if (tasks.length > 0 && selectedIndex < tasks.length) {
57
64
  const selectedTask = tasks[selectedIndex];
58
- setDetailTaskId(selectedTask.id);
59
- setViewMode("detail");
65
+ dispatch({ type: "SET_DETAIL_TASK_ID", id: selectedTask.id });
66
+ dispatch({ type: "SET_VIEW_MODE", mode: "detail" });
60
67
  }
61
68
  return;
62
69
  }
@@ -65,11 +72,17 @@ export const BackgroundTaskManager = ({ onCancel, }) => {
65
72
  return;
66
73
  }
67
74
  if (key.upArrow) {
68
- setSelectedIndex(Math.max(0, selectedIndex - 1));
75
+ dispatch({
76
+ type: "SELECT_INDEX",
77
+ index: Math.max(0, selectedIndex - 1),
78
+ });
69
79
  return;
70
80
  }
71
81
  if (key.downArrow) {
72
- setSelectedIndex(Math.min(tasks.length - 1, selectedIndex + 1));
82
+ dispatch({
83
+ type: "SELECT_INDEX",
84
+ index: Math.min(tasks.length - 1, selectedIndex + 1),
85
+ });
73
86
  return;
74
87
  }
75
88
  if (input === "k" && tasks.length > 0 && selectedIndex < tasks.length) {
@@ -83,9 +96,7 @@ export const BackgroundTaskManager = ({ onCancel, }) => {
83
96
  else if (viewMode === "detail") {
84
97
  // Detail mode navigation
85
98
  if (key.escape) {
86
- setViewMode("list");
87
- setDetailTaskId(null);
88
- setDetailOutput(null);
99
+ dispatch({ type: "RESET_DETAIL" });
89
100
  return;
90
101
  }
91
102
  if (input === "k" && detailTaskId) {
@@ -100,7 +111,7 @@ export const BackgroundTaskManager = ({ onCancel, }) => {
100
111
  if (viewMode === "detail" && detailTaskId && detailOutput) {
101
112
  const task = tasks.find((t) => t.id === detailTaskId);
102
113
  if (!task) {
103
- setViewMode("list");
114
+ dispatch({ type: "RESET_DETAIL" });
104
115
  return null;
105
116
  }
106
117
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", borderBottom: false, borderLeft: false, borderRight: false, paddingTop: 1, gap: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: "cyan", bold: true, children: ["Background Task Details: ", task.id] }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "Type:" }), " ", task.type] }) }), task.description && (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "Description:" }), " ", task.description] }) })), _jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "Status:" }), " ", task.status, task.exitCode !== undefined && ` (exit code: ${task.exitCode})`] }) }), _jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "Started:" }), " ", formatTime(task.startTime), task.runtime !== undefined && (_jsxs(Text, { children: [" ", "| ", _jsx(Text, { color: "blue", children: "Runtime:" }), " ", formatDuration(task.runtime)] }))] }) }), task.outputPath && (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "Log File:" }), " ", task.outputPath] }) }))] }), detailOutput.stdout && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "green", bold: true, children: "OUTPUT (last 10 lines):" }), _jsx(Box, { borderStyle: "single", borderColor: "green", padding: 1, children: _jsx(Text, { children: getLastLines(detailOutput.stdout, 10) }) })] })), detailOutput.stderr && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "red", bold: true, children: "ERRORS:" }), _jsx(Box, { borderStyle: "single", borderColor: "red", padding: 1, children: _jsx(Text, { color: "red", children: getLastLines(detailOutput.stderr, 10) }) })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [task.status === "running" ? "k to stop · " : "", "Esc to go back"] }) })] }));
@@ -1 +1 @@
1
- {"version":3,"file":"ChatInterface.d.ts","sourceRoot":"","sources":["../../src/components/ChatInterface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAa3D,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAgIjC,CAAC"}
1
+ {"version":3,"file":"ChatInterface.d.ts","sourceRoot":"","sources":["../../src/components/ChatInterface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAa3D,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EA+HjC,CAAC"}
@@ -37,5 +37,5 @@ export const ChatInterface = () => {
37
37
  ]);
38
38
  if (!sessionId)
39
39
  return null;
40
- return (_jsxs(Box, { ref: chatInterfaceRef, flexDirection: "column", children: [_jsx(MessageList, { messages: displayMessages, isExpanded: isExpanded, version: version, workdir: workdir, forceStatic: forceStatic }, remountKey), !isConfirmationVisible && !isExpanded && (_jsxs(_Fragment, { children: [(isLoading || isCommandRunning || isCompressing) && (_jsx(LoadingIndicator, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompressing: isCompressing, latestTotalTokens: latestTotalTokens })), _jsx(TaskList, {}), _jsx(QueuedMessageList, {}), _jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, sendMessage: sendMessage, abortMessage: abortMessage, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand })] })), isConfirmationVisible && (_jsxs(_Fragment, { children: [forceStatic ? (_jsx(Static, { items: [{ key: "confirmation-details" }], children: () => (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded }, "confirmation-details")) })) : (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded })), _jsx(ConfirmationSelector, { toolName: confirmingTool.name, toolInput: confirmingTool.input, suggestedPrefix: confirmingTool.suggestedPrefix, hidePersistentOption: confirmingTool.hidePersistentOption, isExpanded: isExpanded, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel, onAbort: abortMessage })] }))] }));
40
+ return (_jsxs(Box, { ref: chatInterfaceRef, flexDirection: "column", children: [_jsx(MessageList, { messages: displayMessages, isExpanded: isExpanded, version: version, workdir: workdir, forceStatic: forceStatic }, remountKey), !isConfirmationVisible && !isExpanded && (_jsxs(_Fragment, { children: [(isLoading || isCommandRunning || isCompressing) && (_jsx(LoadingIndicator, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompressing: isCompressing, latestTotalTokens: latestTotalTokens })), _jsx(TaskList, {}), _jsx(QueuedMessageList, {}), _jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, sendMessage: sendMessage, abortMessage: abortMessage, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand })] })), isConfirmationVisible && (_jsxs(_Fragment, { children: [forceStatic ? (_jsx(Static, { items: [{ key: "confirmation-details" }], children: () => (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded }, "confirmation-details")) })) : (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded })), _jsx(ConfirmationSelector, { toolName: confirmingTool.name, toolInput: confirmingTool.input, suggestedPrefix: confirmingTool.suggestedPrefix, hidePersistentOption: confirmingTool.hidePersistentOption, isExpanded: isExpanded, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel })] }))] }));
41
41
  };
@@ -8,7 +8,6 @@ export interface ConfirmationSelectorProps {
8
8
  isExpanded?: boolean;
9
9
  onDecision: (decision: PermissionDecision) => void;
10
10
  onCancel: () => void;
11
- onAbort: () => void;
12
11
  }
13
12
  export declare const ConfirmationSelector: React.FC<ConfirmationSelectorProps>;
14
13
  //# sourceMappingURL=ConfirmationSelector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConfirmationSelector.d.ts","sourceRoot":"","sources":["../../src/components/ConfirmationSelector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAE3D,OAAO,KAAK,EAAE,kBAAkB,EAAwB,MAAM,gBAAgB,CAAC;AAiB/E,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACnD,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AASD,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CA6nBpE,CAAC"}
1
+ {"version":3,"file":"ConfirmationSelector.d.ts","sourceRoot":"","sources":["../../src/components/ConfirmationSelector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAErD,OAAO,KAAK,EAAE,kBAAkB,EAAwB,MAAM,gBAAgB,CAAC;AAsB/E,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACnD,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CA0cpE,CAAC"}
@@ -1,7 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
2
+ import { useEffect, useReducer } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
4
  import { BASH_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "wave-agent-sdk";
5
+ import { confirmationReducer, } from "../reducers/confirmationReducer.js";
6
+ import { questionReducer } from "../reducers/questionReducer.js";
5
7
  const getHeaderColor = (header) => {
6
8
  const colors = ["red", "green", "blue", "magenta", "cyan"];
7
9
  let hash = 0;
@@ -10,14 +12,16 @@ const getHeaderColor = (header) => {
10
12
  }
11
13
  return colors[Math.abs(hash) % colors.length];
12
14
  };
13
- export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hidePersistentOption, isExpanded = false, onDecision, onCancel, onAbort, }) => {
14
- const [state, setState] = useState({
15
+ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hidePersistentOption, isExpanded = false, onDecision, onCancel, }) => {
16
+ const [state, dispatch] = useReducer(confirmationReducer, {
15
17
  selectedOption: toolName === EXIT_PLAN_MODE_TOOL_NAME ? "clear" : "allow",
16
18
  alternativeText: "",
17
19
  alternativeCursorPosition: 0,
18
20
  hasUserInput: false,
21
+ decision: null,
19
22
  });
20
- const [questionState, setQuestionState] = useState({
23
+ const questions = toolInput?.questions || [];
24
+ const [questionState, questionDispatch] = useReducer(questionReducer, {
21
25
  currentQuestionIndex: 0,
22
26
  selectedOptionIndex: 0,
23
27
  selectedOptionIndices: new Set(),
@@ -25,16 +29,19 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
25
29
  otherText: "",
26
30
  otherCursorPosition: 0,
27
31
  savedStates: {},
32
+ decision: null,
28
33
  });
29
- const pendingDecisionRef = useRef(null);
34
+ // Handle decisions from reducers
30
35
  useEffect(() => {
31
- if (pendingDecisionRef.current) {
32
- const decision = pendingDecisionRef.current;
33
- pendingDecisionRef.current = null;
34
- onDecision(decision);
36
+ if (state.decision) {
37
+ onDecision(state.decision);
35
38
  }
36
- });
37
- const questions = toolInput?.questions || [];
39
+ }, [state.decision, onDecision]);
40
+ useEffect(() => {
41
+ if (questionState.decision) {
42
+ onDecision(questionState.decision);
43
+ }
44
+ }, [questionState.decision, onDecision]);
38
45
  const currentQuestion = questions[questionState.currentQuestionIndex];
39
46
  const getAutoOptionText = () => {
40
47
  if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
@@ -58,7 +65,6 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
58
65
  useInput((input, key) => {
59
66
  if (key.escape) {
60
67
  onCancel();
61
- onAbort();
62
68
  return;
63
69
  }
64
70
  if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
@@ -67,278 +73,148 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
67
73
  const options = [...currentQuestion.options, { label: "Other" }];
68
74
  const isMultiSelect = currentQuestion.multiSelect;
69
75
  if (key.return) {
70
- setQuestionState((prev) => {
71
- const isOtherFocused = prev.selectedOptionIndex === options.length - 1;
72
- let answer = "";
73
- if (isMultiSelect) {
74
- const selectedLabels = Array.from(prev.selectedOptionIndices)
75
- .filter((i) => i < currentQuestion.options.length)
76
- .map((i) => currentQuestion.options[i].label);
77
- const isOtherChecked = prev.selectedOptionIndices.has(options.length - 1);
78
- if (isOtherChecked && prev.otherText.trim()) {
79
- selectedLabels.push(prev.otherText.trim());
80
- }
81
- answer = selectedLabels.join(", ");
82
- }
83
- else {
84
- if (isOtherFocused) {
85
- answer = prev.otherText.trim();
86
- }
87
- else {
88
- answer = options[prev.selectedOptionIndex].label;
89
- }
90
- }
91
- if (!answer)
92
- return prev;
93
- const newAnswers = {
94
- ...prev.userAnswers,
95
- [currentQuestion.question]: answer,
96
- };
97
- if (prev.currentQuestionIndex < questions.length - 1) {
98
- const nextIndex = prev.currentQuestionIndex + 1;
99
- const savedStates = {
100
- ...prev.savedStates,
101
- [prev.currentQuestionIndex]: {
102
- selectedOptionIndex: prev.selectedOptionIndex,
103
- selectedOptionIndices: prev.selectedOptionIndices,
104
- otherText: prev.otherText,
105
- otherCursorPosition: prev.otherCursorPosition,
106
- },
107
- };
108
- const nextState = savedStates[nextIndex] || {
109
- selectedOptionIndex: 0,
110
- selectedOptionIndices: new Set(),
111
- otherText: "",
112
- otherCursorPosition: 0,
113
- };
114
- return {
115
- ...prev,
116
- currentQuestionIndex: nextIndex,
117
- ...nextState,
118
- userAnswers: newAnswers,
119
- savedStates,
120
- };
121
- }
122
- else {
123
- const finalAnswers = { ...newAnswers };
124
- // Also collect from savedStates for any questions that were skipped via Tab
125
- for (const [idxStr, s] of Object.entries(prev.savedStates)) {
126
- const idx = parseInt(idxStr);
127
- const q = questions[idx];
128
- if (q && !finalAnswers[q.question]) {
129
- const opts = [...q.options, { label: "Other" }];
130
- let a = "";
131
- if (q.multiSelect) {
132
- const selectedLabels = Array.from(s.selectedOptionIndices)
133
- .filter((i) => i < q.options.length)
134
- .map((i) => q.options[i].label);
135
- const isOtherChecked = s.selectedOptionIndices.has(opts.length - 1);
136
- if (isOtherChecked && s.otherText.trim()) {
137
- selectedLabels.push(s.otherText.trim());
138
- }
139
- a = selectedLabels.join(", ");
140
- }
141
- else {
142
- if (s.selectedOptionIndex === opts.length - 1) {
143
- a = s.otherText.trim();
144
- }
145
- else {
146
- a = opts[s.selectedOptionIndex].label;
147
- }
148
- }
149
- if (a)
150
- finalAnswers[q.question] = a;
151
- }
152
- }
153
- // Only submit if all questions have been answered
154
- const allAnswered = questions.every((q) => finalAnswers[q.question]);
155
- if (!allAnswered)
156
- return prev;
157
- pendingDecisionRef.current = {
158
- behavior: "allow",
159
- message: JSON.stringify(finalAnswers),
160
- };
161
- return {
162
- ...prev,
163
- userAnswers: finalAnswers,
164
- };
165
- }
76
+ questionDispatch({
77
+ type: "CONFIRM_ANSWER",
78
+ currentQuestion,
79
+ options,
80
+ isMultiSelect: !!isMultiSelect,
81
+ questions,
166
82
  });
167
83
  return;
168
84
  }
169
85
  if (input === " ") {
170
- setQuestionState((prev) => {
171
- const isOtherFocused = prev.selectedOptionIndex === options.length - 1;
172
- if (isMultiSelect &&
173
- (!isOtherFocused ||
174
- !prev.selectedOptionIndices.has(prev.selectedOptionIndex))) {
175
- const nextIndices = new Set(prev.selectedOptionIndices);
176
- if (nextIndices.has(prev.selectedOptionIndex))
177
- nextIndices.delete(prev.selectedOptionIndex);
178
- else
179
- nextIndices.add(prev.selectedOptionIndex);
180
- return {
181
- ...prev,
182
- selectedOptionIndices: nextIndices,
183
- };
184
- }
185
- return prev;
186
- });
86
+ const isOtherFocused = questionState.selectedOptionIndex === options.length - 1;
87
+ if (isMultiSelect &&
88
+ (!isOtherFocused ||
89
+ !questionState.selectedOptionIndices.has(questionState.selectedOptionIndex))) {
90
+ questionDispatch({
91
+ type: "TOGGLE_MULTI_SELECT",
92
+ optionsLength: options.length,
93
+ });
94
+ return;
95
+ }
187
96
  // If it's other and focused, we don't return here, allowing the input handler below to handle it
188
97
  }
189
98
  if (key.upArrow) {
190
- setQuestionState((prev) => ({
191
- ...prev,
192
- selectedOptionIndex: Math.max(0, prev.selectedOptionIndex - 1),
193
- }));
99
+ questionDispatch({
100
+ type: "MOVE_OPTION_UP",
101
+ maxIndex: options.length - 1,
102
+ });
194
103
  return;
195
104
  }
196
105
  if (key.downArrow) {
197
- setQuestionState((prev) => ({
198
- ...prev,
199
- selectedOptionIndex: Math.min(options.length - 1, prev.selectedOptionIndex + 1),
200
- }));
106
+ questionDispatch({
107
+ type: "MOVE_OPTION_DOWN",
108
+ maxIndex: options.length - 1,
109
+ });
201
110
  return;
202
111
  }
203
112
  if (key.tab) {
204
- setQuestionState((prev) => {
205
- const direction = key.shift ? -1 : 1;
206
- let nextIndex = prev.currentQuestionIndex + direction;
207
- if (nextIndex < 0)
208
- nextIndex = questions.length - 1;
209
- if (nextIndex >= questions.length)
210
- nextIndex = 0;
211
- if (nextIndex === prev.currentQuestionIndex)
212
- return prev;
213
- const savedStates = {
214
- ...prev.savedStates,
215
- [prev.currentQuestionIndex]: {
216
- selectedOptionIndex: prev.selectedOptionIndex,
217
- selectedOptionIndices: prev.selectedOptionIndices,
218
- otherText: prev.otherText,
219
- otherCursorPosition: prev.otherCursorPosition,
220
- },
221
- };
222
- const nextState = savedStates[nextIndex] || {
223
- selectedOptionIndex: 0,
224
- selectedOptionIndices: new Set(),
225
- otherText: "",
226
- otherCursorPosition: 0,
227
- };
228
- return {
229
- ...prev,
230
- currentQuestionIndex: nextIndex,
231
- ...nextState,
232
- savedStates,
233
- };
113
+ questionDispatch({
114
+ type: "CYCLE_QUESTION",
115
+ shift: key.shift,
116
+ questionCount: questions.length,
117
+ });
118
+ return;
119
+ }
120
+ // Always dispatch Other actions unconditionally; the reducer checks
121
+ // isOtherFocused from the latest state, so this works correctly even
122
+ // when inputs are batched before React re-renders.
123
+ if (key.leftArrow) {
124
+ questionDispatch({
125
+ type: "MOVE_OTHER_LEFT",
126
+ optionsLength: options.length,
127
+ });
128
+ return;
129
+ }
130
+ if (key.rightArrow) {
131
+ questionDispatch({
132
+ type: "MOVE_OTHER_RIGHT",
133
+ optionsLength: options.length,
134
+ });
135
+ return;
136
+ }
137
+ if (key.backspace || key.delete) {
138
+ questionDispatch({
139
+ type: "DELETE_OTHER",
140
+ optionsLength: options.length,
141
+ });
142
+ return;
143
+ }
144
+ if (input && !key.ctrl && !key.meta) {
145
+ questionDispatch({
146
+ type: "INSERT_OTHER",
147
+ text: input,
148
+ optionsLength: options.length,
234
149
  });
235
150
  return;
236
151
  }
237
- setQuestionState((prev) => {
238
- const isOtherFocused = prev.selectedOptionIndex === options.length - 1;
239
- if (isOtherFocused) {
240
- if (key.leftArrow) {
241
- return {
242
- ...prev,
243
- otherCursorPosition: Math.max(0, prev.otherCursorPosition - 1),
244
- };
245
- }
246
- if (key.rightArrow) {
247
- return {
248
- ...prev,
249
- otherCursorPosition: Math.min(prev.otherText.length, prev.otherCursorPosition + 1),
250
- };
251
- }
252
- if (key.backspace || key.delete) {
253
- if (prev.otherCursorPosition > 0) {
254
- return {
255
- ...prev,
256
- otherText: prev.otherText.slice(0, prev.otherCursorPosition - 1) +
257
- prev.otherText.slice(prev.otherCursorPosition),
258
- otherCursorPosition: prev.otherCursorPosition - 1,
259
- };
260
- }
261
- }
262
- if (input && !key.ctrl && !key.meta) {
263
- return {
264
- ...prev,
265
- otherText: prev.otherText.slice(0, prev.otherCursorPosition) +
266
- input +
267
- prev.otherText.slice(prev.otherCursorPosition),
268
- otherCursorPosition: prev.otherCursorPosition + input.length,
269
- };
270
- }
271
- }
272
- return prev;
273
- });
274
152
  return;
275
153
  }
276
154
  if (key.return) {
155
+ let decision = null;
277
156
  if (state.selectedOption === "clear") {
278
- onDecision({
157
+ decision = {
279
158
  behavior: "allow",
280
159
  newPermissionMode: "acceptEdits",
281
160
  clearContext: true,
282
- });
161
+ };
283
162
  }
284
163
  else if (state.selectedOption === "allow") {
285
164
  if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
286
- onDecision({ behavior: "allow", newPermissionMode: "default" });
165
+ decision = { behavior: "allow", newPermissionMode: "default" };
287
166
  }
288
167
  else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
289
- onDecision({ behavior: "allow", newPermissionMode: "plan" });
168
+ decision = { behavior: "allow", newPermissionMode: "plan" };
290
169
  }
291
170
  else {
292
- onDecision({ behavior: "allow" });
171
+ decision = { behavior: "allow" };
293
172
  }
294
173
  }
295
174
  else if (state.selectedOption === "auto") {
296
175
  if (toolName === BASH_TOOL_NAME) {
297
176
  const command = toolInput?.command || "";
298
177
  if (command.trim().startsWith("mkdir")) {
299
- onDecision({ behavior: "allow", newPermissionMode: "acceptEdits" });
178
+ decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
300
179
  }
301
180
  else {
302
181
  const rule = suggestedPrefix
303
182
  ? `Bash(${suggestedPrefix})`
304
183
  : `Bash(${toolInput?.command})`;
305
- onDecision({ behavior: "allow", newPermissionRule: rule });
184
+ decision = { behavior: "allow", newPermissionRule: rule };
306
185
  }
307
186
  }
308
187
  else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
309
- onDecision({ behavior: "allow", newPermissionMode: "plan" });
188
+ decision = { behavior: "allow", newPermissionMode: "plan" };
310
189
  }
311
190
  else if (toolName.startsWith("mcp__")) {
312
- onDecision({ behavior: "allow", newPermissionRule: toolName });
191
+ decision = { behavior: "allow", newPermissionRule: toolName };
313
192
  }
314
193
  else {
315
- onDecision({ behavior: "allow", newPermissionMode: "acceptEdits" });
194
+ decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
316
195
  }
317
196
  }
318
197
  else if (state.alternativeText.trim()) {
319
- onDecision({ behavior: "deny", message: state.alternativeText.trim() });
198
+ decision = { behavior: "deny", message: state.alternativeText.trim() };
320
199
  }
321
200
  else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
322
- onDecision({
201
+ decision = {
323
202
  behavior: "deny",
324
203
  message: "User chose not to enter plan mode",
325
- });
204
+ };
205
+ }
206
+ if (decision) {
207
+ dispatch({ type: "CONFIRM", decision });
326
208
  }
327
209
  return;
328
210
  }
329
211
  if (state.selectedOption === "alternative") {
330
212
  if (key.leftArrow) {
331
- setState((prev) => ({
332
- ...prev,
333
- alternativeCursorPosition: Math.max(0, prev.alternativeCursorPosition - 1),
334
- }));
213
+ dispatch({ type: "MOVE_CURSOR_LEFT" });
335
214
  return;
336
215
  }
337
216
  if (key.rightArrow) {
338
- setState((prev) => ({
339
- ...prev,
340
- alternativeCursorPosition: Math.min(prev.alternativeText.length, prev.alternativeCursorPosition + 1),
341
- }));
217
+ dispatch({ type: "MOVE_CURSOR_RIGHT" });
342
218
  return;
343
219
  }
344
220
  }
@@ -352,20 +228,20 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
352
228
  if (key.upArrow) {
353
229
  const currentIndex = availableOptions.indexOf(state.selectedOption);
354
230
  if (currentIndex > 0) {
355
- setState((prev) => ({
356
- ...prev,
357
- selectedOption: availableOptions[currentIndex - 1],
358
- }));
231
+ dispatch({
232
+ type: "SELECT_OPTION",
233
+ option: availableOptions[currentIndex - 1],
234
+ });
359
235
  }
360
236
  return;
361
237
  }
362
238
  if (key.downArrow) {
363
239
  const currentIndex = availableOptions.indexOf(state.selectedOption);
364
240
  if (currentIndex < availableOptions.length - 1) {
365
- setState((prev) => ({
366
- ...prev,
367
- selectedOption: availableOptions[currentIndex + 1],
368
- }));
241
+ dispatch({
242
+ type: "SELECT_OPTION",
243
+ option: availableOptions[currentIndex + 1],
244
+ });
369
245
  }
370
246
  return;
371
247
  }
@@ -377,42 +253,18 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
377
253
  nextIndex = availableOptions.length - 1;
378
254
  if (nextIndex >= availableOptions.length)
379
255
  nextIndex = 0;
380
- setState((prev) => ({
381
- ...prev,
382
- selectedOption: availableOptions[nextIndex],
383
- }));
256
+ dispatch({
257
+ type: "SELECT_OPTION",
258
+ option: availableOptions[nextIndex],
259
+ });
384
260
  return;
385
261
  }
386
262
  if (input && !key.ctrl && !key.meta && !("alt" in key && key.alt)) {
387
- setState((prev) => {
388
- const nextText = prev.alternativeText.slice(0, prev.alternativeCursorPosition) +
389
- input +
390
- prev.alternativeText.slice(prev.alternativeCursorPosition);
391
- return {
392
- ...prev,
393
- selectedOption: "alternative",
394
- alternativeText: nextText,
395
- alternativeCursorPosition: prev.alternativeCursorPosition + input.length,
396
- hasUserInput: true,
397
- };
398
- });
263
+ dispatch({ type: "INSERT_TEXT", text: input });
399
264
  return;
400
265
  }
401
266
  if (key.backspace || key.delete) {
402
- setState((prev) => {
403
- if (prev.alternativeCursorPosition > 0) {
404
- const nextText = prev.alternativeText.slice(0, prev.alternativeCursorPosition - 1) +
405
- prev.alternativeText.slice(prev.alternativeCursorPosition);
406
- return {
407
- ...prev,
408
- selectedOption: "alternative",
409
- alternativeText: nextText,
410
- alternativeCursorPosition: prev.alternativeCursorPosition - 1,
411
- hasUserInput: nextText.length > 0,
412
- };
413
- }
414
- return prev;
415
- });
267
+ dispatch({ type: "BACKSPACE" });
416
268
  return;
417
269
  }
418
270
  });