diff --git a/src/cli/App.tsx b/src/cli/App.tsx
index aa531e5..d91d1d3 100644
--- a/src/cli/App.tsx
+++ b/src/cli/App.tsx
@@ -5802,6 +5802,7 @@ Plan file path: ${planFilePath}`;
)}
diff --git a/src/cli/components/AgentSelector.tsx b/src/cli/components/AgentSelector.tsx
index 1f064ab..31742e4 100644
--- a/src/cli/components/AgentSelector.tsx
+++ b/src/cli/components/AgentSelector.tsx
@@ -63,6 +63,12 @@ export function AgentSelector({
}, []);
useInput((input, key) => {
+ // CTRL-C: immediately cancel (works even during loading/error)
+ if (key.ctrl && input === "c") {
+ onCancel();
+ return;
+ }
+
if (loading || error) return;
if (key.upArrow) {
diff --git a/src/cli/components/EnterPlanModeDialog.tsx b/src/cli/components/EnterPlanModeDialog.tsx
index e704c00..a64cb6e 100644
--- a/src/cli/components/EnterPlanModeDialog.tsx
+++ b/src/cli/components/EnterPlanModeDialog.tsx
@@ -16,6 +16,18 @@ export const EnterPlanModeDialog = memo(({ onApprove, onReject }: Props) => {
];
useInput((input, key) => {
+ // CTRL-C: immediately reject (cancel)
+ if (key.ctrl && input === "c") {
+ onReject();
+ return;
+ }
+
+ // ESC: reject (cancel) - was missing!
+ if (key.escape) {
+ onReject();
+ return;
+ }
+
if (key.upArrow) {
setSelectedOption((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {
diff --git a/src/cli/components/FeedbackDialog.tsx b/src/cli/components/FeedbackDialog.tsx
index 047a1d3..6df4c38 100644
--- a/src/cli/components/FeedbackDialog.tsx
+++ b/src/cli/components/FeedbackDialog.tsx
@@ -18,6 +18,12 @@ export function FeedbackDialog({
const [error, setError] = useState("");
useInput((_input, key) => {
+ // CTRL-C: immediately cancel
+ if (key.ctrl && _input === "c") {
+ onCancel();
+ return;
+ }
+
if (key.escape) {
onCancel();
}
diff --git a/src/cli/components/HelpDialog.tsx b/src/cli/components/HelpDialog.tsx
index ef47124..d00b7c5 100644
--- a/src/cli/components/HelpDialog.tsx
+++ b/src/cli/components/HelpDialog.tsx
@@ -100,6 +100,12 @@ export function HelpDialog({ onClose }: HelpDialogProps) {
useInput(
useCallback(
(input, key) => {
+ // CTRL-C: immediately close
+ if (key.ctrl && input === "c") {
+ onClose();
+ return;
+ }
+
if (key.escape) {
onClose();
} else if (key.tab) {
diff --git a/src/cli/components/McpSelector.tsx b/src/cli/components/McpSelector.tsx
index d790b73..4d027dd 100644
--- a/src/cli/components/McpSelector.tsx
+++ b/src/cli/components/McpSelector.tsx
@@ -292,6 +292,12 @@ export const McpSelector = memo(function McpSelector({
const selectedServer = pageServers[selectedIndex];
useInput((input, key) => {
+ // CTRL-C: immediately cancel (works even during loading)
+ if (key.ctrl && input === "c") {
+ onCancel();
+ return;
+ }
+
if (loading) return;
// Handle delete confirmation mode
diff --git a/src/cli/components/MemoryViewer.tsx b/src/cli/components/MemoryViewer.tsx
index 0890344..ab2d4a0 100644
--- a/src/cli/components/MemoryViewer.tsx
+++ b/src/cli/components/MemoryViewer.tsx
@@ -82,6 +82,12 @@ export function MemoryViewer({
);
useInput((input, key) => {
+ // CTRL-C: immediately close the entire viewer
+ if (key.ctrl && input === "c") {
+ onClose();
+ return;
+ }
+
// ESC: exit detail view or close entirely
if (key.escape) {
if (detailBlockIndex !== null) {
diff --git a/src/cli/components/MessageSearch.tsx b/src/cli/components/MessageSearch.tsx
index 326b3c2..035a2a6 100644
--- a/src/cli/components/MessageSearch.tsx
+++ b/src/cli/components/MessageSearch.tsx
@@ -194,6 +194,12 @@ export function MessageSearch({ onClose }: MessageSearchProps) {
const pageResults = results.slice(startIndex, startIndex + DISPLAY_PAGE_SIZE);
useInput((input, key) => {
+ // CTRL-C: immediately close (bypasses search clearing)
+ if (key.ctrl && input === "c") {
+ onClose();
+ return;
+ }
+
if (key.escape) {
if (searchInput || activeQuery) {
clearSearch();
diff --git a/src/cli/components/ModelSelector.tsx b/src/cli/components/ModelSelector.tsx
index 700f68e..7af7475 100644
--- a/src/cli/components/ModelSelector.tsx
+++ b/src/cli/components/ModelSelector.tsx
@@ -185,6 +185,12 @@ export function ModelSelector({
useInput(
(input, key) => {
+ // CTRL-C: immediately cancel (bypasses search clearing)
+ if (key.ctrl && input === "c") {
+ onCancel();
+ return;
+ }
+
// Handle ESC: clear search first if active, otherwise cancel
if (key.escape) {
if (searchQuery) {
diff --git a/src/cli/components/NewAgentDialog.tsx b/src/cli/components/NewAgentDialog.tsx
index 6cfff3b..504838f 100644
--- a/src/cli/components/NewAgentDialog.tsx
+++ b/src/cli/components/NewAgentDialog.tsx
@@ -14,7 +14,13 @@ export function NewAgentDialog({ onSubmit, onCancel }: NewAgentDialogProps) {
const [nameInput, setNameInput] = useState("");
const [error, setError] = useState("");
- useInput((_, key) => {
+ useInput((input, key) => {
+ // CTRL-C: immediately cancel
+ if (key.ctrl && input === "c") {
+ onCancel();
+ return;
+ }
+
if (key.escape) {
onCancel();
}
diff --git a/src/cli/components/OAuthCodeDialog.tsx b/src/cli/components/OAuthCodeDialog.tsx
index 7a7d638..ca2bba4 100644
--- a/src/cli/components/OAuthCodeDialog.tsx
+++ b/src/cli/components/OAuthCodeDialog.tsx
@@ -114,7 +114,16 @@ export const OAuthCodeDialog = memo(
}, [onComplete]);
// Handle keyboard input
- useInput((_input, key) => {
+ useInput((input, key) => {
+ // CTRL-C: cancel at any cancelable state
+ if (key.ctrl && input === "c") {
+ if (flowState === "waiting_for_code" || flowState === "select_model") {
+ settingsManager.clearOAuthState();
+ onCancel();
+ }
+ return;
+ }
+
if (key.escape && flowState === "waiting_for_code") {
settingsManager.clearOAuthState();
onCancel();
diff --git a/src/cli/components/PinDialog.tsx b/src/cli/components/PinDialog.tsx
index f1562c5..421f1cb 100644
--- a/src/cli/components/PinDialog.tsx
+++ b/src/cli/components/PinDialog.tsx
@@ -63,6 +63,12 @@ export function PinDialog({
const scopeText = local ? "to this project" : "globally";
useInput((input, key) => {
+ // CTRL-C: immediately cancel (bypasses mode transitions)
+ if (key.ctrl && input === "c") {
+ onCancel();
+ return;
+ }
+
if (key.escape) {
if (mode === "input" && !isDefault) {
// Go back to choose mode
diff --git a/src/cli/components/PlanModeDialog.tsx b/src/cli/components/PlanModeDialog.tsx
index 178c9db..5bdb737 100644
--- a/src/cli/components/PlanModeDialog.tsx
+++ b/src/cli/components/PlanModeDialog.tsx
@@ -53,6 +53,12 @@ export const PlanModeDialog = memo(
];
useInput((_input, key) => {
+ // CTRL-C: immediately exit plan approval (closest to cancel)
+ if (key.ctrl && _input === "c") {
+ onKeepPlanning("User pressed CTRL-C to cancel");
+ return;
+ }
+
if (isEnteringReason) {
// When entering reason, only handle enter/escape
if (key.return) {
diff --git a/src/cli/components/ProfileSelector.tsx b/src/cli/components/ProfileSelector.tsx
index cdcaaff..a3c503a 100644
--- a/src/cli/components/ProfileSelector.tsx
+++ b/src/cli/components/ProfileSelector.tsx
@@ -147,6 +147,12 @@ export const ProfileSelector = memo(function ProfileSelector({
const selectedProfile = pageProfiles[selectedIndex];
useInput((input, key) => {
+ // CTRL-C: immediately cancel (works even during loading)
+ if (key.ctrl && input === "c") {
+ onCancel();
+ return;
+ }
+
if (loading) return;
// Handle delete confirmation mode
diff --git a/src/cli/components/QuestionDialog.tsx b/src/cli/components/QuestionDialog.tsx
index eab770d..d26a798 100644
--- a/src/cli/components/QuestionDialog.tsx
+++ b/src/cli/components/QuestionDialog.tsx
@@ -18,200 +18,219 @@ interface Question {
type Props = {
questions: Question[];
onSubmit: (answers: Record) => void;
+ onCancel?: () => void;
};
-export const QuestionDialog = memo(({ questions, onSubmit }: Props) => {
- const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
- const [answers, setAnswers] = useState>({});
- const [selectedOption, setSelectedOption] = useState(0);
- const [isOtherMode, setIsOtherMode] = useState(false);
- const [otherText, setOtherText] = useState("");
- const [selectedMulti, setSelectedMulti] = useState>(new Set());
+export const QuestionDialog = memo(
+ ({ questions, onSubmit, onCancel }: Props) => {
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
+ const [answers, setAnswers] = useState>({});
+ const [selectedOption, setSelectedOption] = useState(0);
+ const [isOtherMode, setIsOtherMode] = useState(false);
+ const [otherText, setOtherText] = useState("");
+ const [selectedMulti, setSelectedMulti] = useState>(new Set());
- const currentQuestion = questions[currentQuestionIndex];
- const optionsWithOther = currentQuestion
- ? [
- ...currentQuestion.options,
- { label: "Other", description: "Provide a custom response" },
- ]
- : [];
+ const currentQuestion = questions[currentQuestionIndex];
+ const optionsWithOther = currentQuestion
+ ? [
+ ...currentQuestion.options,
+ { label: "Other", description: "Provide a custom response" },
+ ]
+ : [];
- const handleSubmitAnswer = (answer: string) => {
- if (!currentQuestion) return;
- const newAnswers = {
- ...answers,
- [currentQuestion.question]: answer,
- };
- setAnswers(newAnswers);
+ const handleSubmitAnswer = (answer: string) => {
+ if (!currentQuestion) return;
+ const newAnswers = {
+ ...answers,
+ [currentQuestion.question]: answer,
+ };
+ setAnswers(newAnswers);
- if (currentQuestionIndex < questions.length - 1) {
- setCurrentQuestionIndex(currentQuestionIndex + 1);
- setSelectedOption(0);
- setIsOtherMode(false);
- setOtherText("");
- setSelectedMulti(new Set());
- } else {
- onSubmit(newAnswers);
- }
- };
-
- useInput((input, key) => {
- if (!currentQuestion) return;
-
- if (isOtherMode) {
- if (key.escape) {
+ if (currentQuestionIndex < questions.length - 1) {
+ setCurrentQuestionIndex(currentQuestionIndex + 1);
+ setSelectedOption(0);
setIsOtherMode(false);
setOtherText("");
- }
- return;
- }
-
- if (key.upArrow) {
- setSelectedOption((prev) => Math.max(0, prev - 1));
- } else if (key.downArrow) {
- setSelectedOption((prev) =>
- Math.min(optionsWithOther.length - 1, prev + 1),
- );
- } else if (key.return) {
- if (currentQuestion.multiSelect) {
- if (selectedOption === optionsWithOther.length - 1) {
- setIsOtherMode(true);
- } else if (selectedMulti.size > 0) {
- const selectedLabels = Array.from(selectedMulti)
- .map((i) => optionsWithOther[i]?.label)
- .filter(Boolean)
- .join(", ");
- handleSubmitAnswer(selectedLabels);
- }
+ setSelectedMulti(new Set());
} else {
- if (selectedOption === optionsWithOther.length - 1) {
- setIsOtherMode(true);
- } else {
- handleSubmitAnswer(optionsWithOther[selectedOption]?.label || "");
+ onSubmit(newAnswers);
+ }
+ };
+
+ useInput((input, key) => {
+ if (!currentQuestion) return;
+
+ // CTRL-C: immediately cancel (works in any mode)
+ if (key.ctrl && input === "c") {
+ if (onCancel) {
+ onCancel();
}
+ return;
}
- } else if (input === " " && currentQuestion.multiSelect) {
- if (selectedOption < optionsWithOther.length - 1) {
- setSelectedMulti((prev) => {
- const newSet = new Set(prev);
- if (newSet.has(selectedOption)) {
- newSet.delete(selectedOption);
- } else {
- newSet.add(selectedOption);
- }
- return newSet;
- });
+
+ if (isOtherMode) {
+ if (key.escape) {
+ setIsOtherMode(false);
+ setOtherText("");
+ }
+ return;
}
- } else if (input >= "1" && input <= "9") {
- const optionIndex = Number.parseInt(input, 10) - 1;
- if (optionIndex < optionsWithOther.length) {
+
+ // ESC in main selection mode: cancel the dialog
+ if (key.escape) {
+ if (onCancel) {
+ onCancel();
+ }
+ return;
+ }
+
+ if (key.upArrow) {
+ setSelectedOption((prev) => Math.max(0, prev - 1));
+ } else if (key.downArrow) {
+ setSelectedOption((prev) =>
+ Math.min(optionsWithOther.length - 1, prev + 1),
+ );
+ } else if (key.return) {
if (currentQuestion.multiSelect) {
- if (optionIndex < optionsWithOther.length - 1) {
- setSelectedMulti((prev) => {
- const newSet = new Set(prev);
- if (newSet.has(optionIndex)) {
- newSet.delete(optionIndex);
- } else {
- newSet.add(optionIndex);
- }
- return newSet;
- });
+ if (selectedOption === optionsWithOther.length - 1) {
+ setIsOtherMode(true);
+ } else if (selectedMulti.size > 0) {
+ const selectedLabels = Array.from(selectedMulti)
+ .map((i) => optionsWithOther[i]?.label)
+ .filter(Boolean)
+ .join(", ");
+ handleSubmitAnswer(selectedLabels);
}
} else {
- if (optionIndex === optionsWithOther.length - 1) {
+ if (selectedOption === optionsWithOther.length - 1) {
setIsOtherMode(true);
} else {
- handleSubmitAnswer(optionsWithOther[optionIndex]?.label || "");
+ handleSubmitAnswer(optionsWithOther[selectedOption]?.label || "");
+ }
+ }
+ } else if (input === " " && currentQuestion.multiSelect) {
+ if (selectedOption < optionsWithOther.length - 1) {
+ setSelectedMulti((prev) => {
+ const newSet = new Set(prev);
+ if (newSet.has(selectedOption)) {
+ newSet.delete(selectedOption);
+ } else {
+ newSet.add(selectedOption);
+ }
+ return newSet;
+ });
+ }
+ } else if (input >= "1" && input <= "9") {
+ const optionIndex = Number.parseInt(input, 10) - 1;
+ if (optionIndex < optionsWithOther.length) {
+ if (currentQuestion.multiSelect) {
+ if (optionIndex < optionsWithOther.length - 1) {
+ setSelectedMulti((prev) => {
+ const newSet = new Set(prev);
+ if (newSet.has(optionIndex)) {
+ newSet.delete(optionIndex);
+ } else {
+ newSet.add(optionIndex);
+ }
+ return newSet;
+ });
+ }
+ } else {
+ if (optionIndex === optionsWithOther.length - 1) {
+ setIsOtherMode(true);
+ } else {
+ handleSubmitAnswer(optionsWithOther[optionIndex]?.label || "");
+ }
}
}
}
- }
- });
+ });
- const handleOtherSubmit = (text: string) => {
- handleSubmitAnswer(text);
- };
+ const handleOtherSubmit = (text: string) => {
+ handleSubmitAnswer(text);
+ };
- if (!currentQuestion) return null;
+ if (!currentQuestion) return null;
- return (
-
-
-
- [{currentQuestion.header}]{" "}
- {currentQuestion.question}
-
-
-
- {questions.length > 1 && (
+ return (
+
-
- Question {currentQuestionIndex + 1} of {questions.length}
+
+ [{currentQuestion.header}]{" "}
+ {currentQuestion.question}
- )}
- {isOtherMode ? (
-
- Type your response (Esc to cancel):
-
- >
-
-
-
- ) : (
-
- {optionsWithOther.map((option, index) => {
- const isSelected = index === selectedOption;
- const isChecked = selectedMulti.has(index);
- const color = isSelected ? colors.approval.header : undefined;
-
- return (
-
-
-
- {isSelected ? ">" : " "}
-
- {currentQuestion.multiSelect &&
- index < optionsWithOther.length - 1 && (
-
- [{isChecked ? "x" : " "}]
-
- )}
-
-
- {index + 1}. {option.label}
-
-
-
- {option.description && (
-
- {option.description}
-
- )}
-
- );
- })}
-
-
+ {questions.length > 1 && (
+
- {currentQuestion.multiSelect
- ? "Space to toggle, Enter to confirm selection"
- : `Enter to select, or type 1-${optionsWithOther.length}`}
+ Question {currentQuestionIndex + 1} of {questions.length}
-
- )}
-
- );
-});
+ )}
+
+ {isOtherMode ? (
+
+ Type your response (Esc to cancel):
+
+ >
+
+
+
+ ) : (
+
+ {optionsWithOther.map((option, index) => {
+ const isSelected = index === selectedOption;
+ const isChecked = selectedMulti.has(index);
+ const color = isSelected ? colors.approval.header : undefined;
+
+ return (
+
+
+
+ {isSelected ? ">" : " "}
+
+ {currentQuestion.multiSelect &&
+ index < optionsWithOther.length - 1 && (
+
+ [{isChecked ? "x" : " "}]
+
+ )}
+
+
+ {index + 1}. {option.label}
+
+
+
+ {option.description && (
+
+ {option.description}
+
+ )}
+
+ );
+ })}
+
+
+
+ {currentQuestion.multiSelect
+ ? "Space to toggle, Enter to confirm selection"
+ : `Enter to select, or type 1-${optionsWithOther.length}`}
+
+
+
+ )}
+
+ );
+ },
+);
QuestionDialog.displayName = "QuestionDialog";
diff --git a/src/cli/components/ResumeSelector.tsx b/src/cli/components/ResumeSelector.tsx
index ee067d9..c8a58ee 100644
--- a/src/cli/components/ResumeSelector.tsx
+++ b/src/cli/components/ResumeSelector.tsx
@@ -415,6 +415,12 @@ export function ResumeSelector({
}, [activeQuery]);
useInput((input, key) => {
+ // CTRL-C: immediately cancel
+ if (key.ctrl && input === "c") {
+ onCancel();
+ return;
+ }
+
// Tab key cycles through tabs
if (key.tab) {
const currentIndex = TABS.findIndex((t) => t.id === activeTab);
diff --git a/src/cli/components/SubagentManager.tsx b/src/cli/components/SubagentManager.tsx
index 1082b39..f4f855c 100644
--- a/src/cli/components/SubagentManager.tsx
+++ b/src/cli/components/SubagentManager.tsx
@@ -63,7 +63,13 @@ export function SubagentManager({ onClose }: SubagentManagerProps) {
loadSubagents();
}, []);
- useInput((_input, key) => {
+ useInput((input, key) => {
+ // CTRL-C: immediately close
+ if (key.ctrl && input === "c") {
+ onClose();
+ return;
+ }
+
if (key.escape || key.return) {
onClose();
}
diff --git a/src/cli/components/SystemPromptSelector.tsx b/src/cli/components/SystemPromptSelector.tsx
index 52df21e..774efe8 100644
--- a/src/cli/components/SystemPromptSelector.tsx
+++ b/src/cli/components/SystemPromptSelector.tsx
@@ -34,7 +34,13 @@ export function SystemPromptSelector({
const totalItems = visiblePrompts.length + (hasShowAllOption ? 1 : 0);
- useInput((_input, key) => {
+ useInput((input, key) => {
+ // CTRL-C: immediately cancel
+ if (key.ctrl && input === "c") {
+ onCancel();
+ return;
+ }
+
if (key.upArrow) {
setSelectedIndex((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {
diff --git a/src/cli/components/ToolsetSelector.tsx b/src/cli/components/ToolsetSelector.tsx
index d788374..84f51aa 100644
--- a/src/cli/components/ToolsetSelector.tsx
+++ b/src/cli/components/ToolsetSelector.tsx
@@ -139,7 +139,13 @@ export function ToolsetSelector({
const totalItems = visibleToolsets.length + (hasShowAllOption ? 1 : 0);
- useInput((_input, key) => {
+ useInput((input, key) => {
+ // CTRL-C: immediately cancel
+ if (key.ctrl && input === "c") {
+ onCancel();
+ return;
+ }
+
if (key.upArrow) {
setSelectedIndex((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {