fix: add consistent CTRL-C and ESC handling to all dialogs (#426)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -5802,6 +5802,7 @@ Plan file path: ${planFilePath}`;
|
||||
<QuestionDialog
|
||||
questions={getQuestionsFromApproval(currentApproval)}
|
||||
onSubmit={handleQuestionSubmit}
|
||||
onCancel={handleCancelApprovals}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,200 +18,219 @@ interface Question {
|
||||
type Props = {
|
||||
questions: Question[];
|
||||
onSubmit: (answers: Record<string, string>) => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export const QuestionDialog = memo(({ questions, onSubmit }: Props) => {
|
||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||
const [answers, setAnswers] = useState<Record<string, string>>({});
|
||||
const [selectedOption, setSelectedOption] = useState(0);
|
||||
const [isOtherMode, setIsOtherMode] = useState(false);
|
||||
const [otherText, setOtherText] = useState("");
|
||||
const [selectedMulti, setSelectedMulti] = useState<Set<number>>(new Set());
|
||||
export const QuestionDialog = memo(
|
||||
({ questions, onSubmit, onCancel }: Props) => {
|
||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||
const [answers, setAnswers] = useState<Record<string, string>>({});
|
||||
const [selectedOption, setSelectedOption] = useState(0);
|
||||
const [isOtherMode, setIsOtherMode] = useState(false);
|
||||
const [otherText, setOtherText] = useState("");
|
||||
const [selectedMulti, setSelectedMulti] = useState<Set<number>>(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 (
|
||||
<Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text color={colors.approval.header}>
|
||||
<Text bold>[{currentQuestion.header}]</Text>{" "}
|
||||
{currentQuestion.question}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{questions.length > 1 && (
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text dimColor>
|
||||
Question {currentQuestionIndex + 1} of {questions.length}
|
||||
<Text color={colors.approval.header}>
|
||||
<Text bold>[{currentQuestion.header}]</Text>{" "}
|
||||
{currentQuestion.question}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isOtherMode ? (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>Type your response (Esc to cancel):</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text color={colors.approval.header}>> </Text>
|
||||
<PasteAwareTextInput
|
||||
value={otherText}
|
||||
onChange={setOtherText}
|
||||
onSubmit={handleOtherSubmit}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="column">
|
||||
{optionsWithOther.map((option, index) => {
|
||||
const isSelected = index === selectedOption;
|
||||
const isChecked = selectedMulti.has(index);
|
||||
const color = isSelected ? colors.approval.header : undefined;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={option.label}
|
||||
flexDirection="column"
|
||||
marginBottom={index < optionsWithOther.length - 1 ? 1 : 0}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
<Box width={2} flexShrink={0}>
|
||||
<Text color={color}>{isSelected ? ">" : " "}</Text>
|
||||
</Box>
|
||||
{currentQuestion.multiSelect &&
|
||||
index < optionsWithOther.length - 1 && (
|
||||
<Box width={4} flexShrink={0}>
|
||||
<Text color={color}>[{isChecked ? "x" : " "}]</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexGrow={1}>
|
||||
<Text color={color} bold={isSelected}>
|
||||
{index + 1}. {option.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{option.description && (
|
||||
<Box paddingLeft={currentQuestion.multiSelect ? 6 : 2}>
|
||||
<Text dimColor>{option.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<Box marginTop={1}>
|
||||
{questions.length > 1 && (
|
||||
<Box marginBottom={1}>
|
||||
<Text dimColor>
|
||||
{currentQuestion.multiSelect
|
||||
? "Space to toggle, Enter to confirm selection"
|
||||
: `Enter to select, or type 1-${optionsWithOther.length}`}
|
||||
Question {currentQuestionIndex + 1} of {questions.length}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
)}
|
||||
|
||||
{isOtherMode ? (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>Type your response (Esc to cancel):</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text color={colors.approval.header}>> </Text>
|
||||
<PasteAwareTextInput
|
||||
value={otherText}
|
||||
onChange={setOtherText}
|
||||
onSubmit={handleOtherSubmit}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="column">
|
||||
{optionsWithOther.map((option, index) => {
|
||||
const isSelected = index === selectedOption;
|
||||
const isChecked = selectedMulti.has(index);
|
||||
const color = isSelected ? colors.approval.header : undefined;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={option.label}
|
||||
flexDirection="column"
|
||||
marginBottom={index < optionsWithOther.length - 1 ? 1 : 0}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
<Box width={2} flexShrink={0}>
|
||||
<Text color={color}>{isSelected ? ">" : " "}</Text>
|
||||
</Box>
|
||||
{currentQuestion.multiSelect &&
|
||||
index < optionsWithOther.length - 1 && (
|
||||
<Box width={4} flexShrink={0}>
|
||||
<Text color={color}>[{isChecked ? "x" : " "}]</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexGrow={1}>
|
||||
<Text color={color} bold={isSelected}>
|
||||
{index + 1}. {option.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{option.description && (
|
||||
<Box paddingLeft={currentQuestion.multiSelect ? 6 : 2}>
|
||||
<Text dimColor>{option.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
{currentQuestion.multiSelect
|
||||
? "Space to toggle, Enter to confirm selection"
|
||||
: `Enter to select, or type 1-${optionsWithOther.length}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
QuestionDialog.displayName = "QuestionDialog";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user