feat(ui): use Tab to switch focus between shell and input (#14332)

This commit is contained in:
Jacob Richman
2026-01-12 15:30:12 -08:00
committed by GitHub
parent 2e8c6cfdbb
commit ca6786a28b
11 changed files with 180 additions and 114 deletions
+56 -18
View File
@@ -823,11 +823,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
embeddedShellFocused,
);
const lastOutputTimeRef = useRef(0);
useEffect(() => {
lastOutputTimeRef.current = lastOutputTime;
}, [lastOutputTime]);
// Auto-accept indicator
const showAutoAcceptIndicator = useAutoAcceptIndicator({
config,
addItem: historyManager.addItem,
onApprovalModeChange: handleApprovalModeChange,
isActive: !embeddedShellFocused,
});
const {
@@ -1053,19 +1059,20 @@ Logging in with Google... Restarting Gemini CLI to continue.
useIncludeDirsTrust(config, isTrustedFolder, historyManager, setCustomDialog);
const warningTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const tabFocusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleWarning = useCallback((message: string) => {
setWarningMessage(message);
if (warningTimeoutRef.current) {
clearTimeout(warningTimeoutRef.current);
}
warningTimeoutRef.current = setTimeout(() => {
setWarningMessage(null);
}, WARNING_PROMPT_DURATION_MS);
}, []);
useEffect(() => {
let timeoutId: NodeJS.Timeout;
const handleWarning = (message: string) => {
setWarningMessage(message);
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
setWarningMessage(null);
}, WARNING_PROMPT_DURATION_MS);
};
const handleSelectionWarning = () => {
handleWarning('Press Ctrl-S to enter selection mode to copy text.');
};
@@ -1077,11 +1084,14 @@ Logging in with Google... Restarting Gemini CLI to continue.
return () => {
appEvents.off(AppEvent.SelectionWarning, handleSelectionWarning);
appEvents.off(AppEvent.PasteTimeout, handlePasteTimeout);
if (timeoutId) {
clearTimeout(timeoutId);
if (warningTimeoutRef.current) {
clearTimeout(warningTimeoutRef.current);
}
if (tabFocusTimeoutRef.current) {
clearTimeout(tabFocusTimeoutRef.current);
}
};
}, []);
}, [handleWarning]);
useEffect(() => {
if (ideNeedsRestart) {
@@ -1269,10 +1279,37 @@ Logging in with Google... Restarting Gemini CLI to continue.
!enteringConstrainHeightMode
) {
setConstrainHeight(false);
} else if (keyMatchers[Command.TOGGLE_SHELL_INPUT_FOCUS](key)) {
if (activePtyId || embeddedShellFocused) {
setEmbeddedShellFocused((prev) => !prev);
} else if (
keyMatchers[Command.TOGGLE_SHELL_INPUT_FOCUS_OUT](key) &&
activePtyId &&
embeddedShellFocused
) {
if (key.name === 'tab' && key.shift) {
// Always change focus
setEmbeddedShellFocused(false);
return;
}
const now = Date.now();
// If the shell hasn't produced output in the last 100ms, it's considered idle.
const isIdle = now - lastOutputTimeRef.current >= 100;
if (isIdle) {
if (tabFocusTimeoutRef.current) {
clearTimeout(tabFocusTimeoutRef.current);
}
tabFocusTimeoutRef.current = setTimeout(() => {
tabFocusTimeoutRef.current = null;
// If the shell produced output since the tab press, we assume it handled the tab
// (e.g. autocomplete) so we should not toggle focus.
if (lastOutputTimeRef.current > now) {
handleWarning('Press Shift+Tab to focus out.');
return;
}
setEmbeddedShellFocused(false);
}, 100);
return;
}
handleWarning('Press Shift+Tab to focus out.');
}
},
[
@@ -1293,6 +1330,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
setCopyModeEnabled,
copyModeEnabled,
isAlternateBuffer,
handleWarning,
],
);