Merge pull request #1824 from QwenLM/mingholy/fix/esc-interrupt

refactor(cli): unify Escape key handling in AppContainer
This commit is contained in:
Mingholy 2026-02-13 21:39:11 +08:00 committed by GitHub
commit ffa2d89ecd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 74 additions and 51 deletions

View file

@ -692,7 +692,6 @@ export const AppContainer = (props: AppContainerProps) => {
terminalWidth,
terminalHeight,
handleVisionSwitchRequired, // onVisionSwitchRequired
embeddedShellFocused,
);
// Track whether suggestions are visible for Tab key handling
@ -900,6 +899,8 @@ export const AppContainer = (props: AppContainerProps) => {
const ctrlCTimerRef = useRef<NodeJS.Timeout | null>(null);
const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false);
const ctrlDTimerRef = useRef<NodeJS.Timeout | null>(null);
const [escapePressedOnce, setEscapePressedOnce] = useState(false);
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
const [ideContextState, setIdeContextState] = useState<
IdeContext | undefined
@ -1176,6 +1177,47 @@ export const AppContainer = (props: AppContainerProps) => {
}
handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef);
return;
} else if (keyMatchers[Command.ESCAPE](key)) {
// Escape key handling
// Skip if shell is focused (to allow shell's own escape handling)
if (embeddedShellFocused) {
return;
}
// If input has content, use double-press to clear
if (buffer.text.length > 0) {
if (escapePressedOnce) {
// Second press: clear input, keep the flag to allow immediate cancel
buffer.setText('');
return;
}
// First press: set flag and show prompt
setEscapePressedOnce(true);
escapeTimerRef.current = setTimeout(() => {
setEscapePressedOnce(false);
escapeTimerRef.current = null;
}, CTRL_EXIT_PROMPT_DURATION_MS);
return;
}
// Input is empty, cancel request immediately (no double-press needed)
if (streamingState === StreamingState.Responding) {
if (escapeTimerRef.current) {
clearTimeout(escapeTimerRef.current);
escapeTimerRef.current = null;
}
cancelOngoingRequest?.();
setEscapePressedOnce(false);
return;
}
// No action available, reset the flag
if (escapeTimerRef.current) {
clearTimeout(escapeTimerRef.current);
escapeTimerRef.current = null;
}
setEscapePressedOnce(false);
return;
}
let enteringConstrainHeightMode = false;
@ -1220,10 +1262,15 @@ export const AppContainer = (props: AppContainerProps) => {
ctrlCPressedOnce,
setCtrlCPressedOnce,
ctrlCTimerRef,
buffer.text.length,
ctrlDPressedOnce,
setCtrlDPressedOnce,
ctrlDTimerRef,
escapePressedOnce,
setEscapePressedOnce,
escapeTimerRef,
streamingState,
cancelOngoingRequest,
buffer,
handleSlashCommand,
activePtyId,
embeddedShellFocused,