feat: Implement delayed Ctrl+C exit prompt

This change introduces a small delay after the first Ctrl+C press, prompting the user to press Ctrl+C again to exit. This helps prevent accidental termination of the application.

- Added `exitOnCtrlC={false}` to the Ink render options in `gemini.tsx` to enable custom Ctrl+C handling.
- Implemented logic in `App.tsx` to:
    - Display "Press Ctrl+C again to exit." for 2 seconds after the first Ctrl+C.
    - Exit the application if Ctrl+C is pressed again during this period.
    - Revert to normal operation if the second Ctrl+C is not pressed within the timeout.
- Defined a constant `CTRL_C_PROMPT_DURATION_MS` for the timeout duration.
This commit is contained in:
Daniel Young Lee 2025-05-30 19:36:52 -07:00 committed by N. Taylor Mullen
parent 7012c86336
commit 1468047081
2 changed files with 35 additions and 1 deletions

View File

@ -78,6 +78,7 @@ async function main() {
startupWarnings={startupWarnings}
/>
</React.StrictMode>,
{ exitOnCtrlC: false },
);
return;
}

View File

@ -44,6 +44,8 @@ import { useLogger } from './hooks/useLogger.js';
import { StreamingContext } from './contexts/StreamingContext.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
const CTRL_C_PROMPT_DURATION_MS = 1000;
interface AppProps {
config: Config;
settings: LoadedSettings;
@ -77,18 +79,43 @@ export const App = ({
const [corgiMode, setCorgiMode] = useState(false);
const [shellModeActive, setShellModeActive] = useState(false);
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
const ctrlCTimerRef = useRef<NodeJS.Timeout | null>(null);
const errorCount = useMemo(
() => consoleMessages.filter((msg) => msg.type === 'error').length,
[consoleMessages],
);
useInput((input: string, key: InkKeyType) => {
if (key.ctrl && input === 'o') {
setShowErrorDetails((prev) => !prev);
refreshStatic();
} else if (key.ctrl && (input === 'c' || input === 'C')) {
if (ctrlCPressedOnce) {
if (ctrlCTimerRef.current) {
clearTimeout(ctrlCTimerRef.current);
}
process.exit(0);
} else {
setCtrlCPressedOnce(true);
ctrlCTimerRef.current = setTimeout(() => {
setCtrlCPressedOnce(false);
ctrlCTimerRef.current = null;
}, CTRL_C_PROMPT_DURATION_MS);
}
}
});
useEffect(
() => () => {
if (ctrlCTimerRef.current) {
clearTimeout(ctrlCTimerRef.current);
}
},
[],
);
useConsolePatcher({
onNewMessage: handleNewMessage,
debugMode: config.getDebugMode(),
@ -365,11 +392,17 @@ export const App = ({
{process.env.GEMINI_SYSTEM_MD && (
<Text color={Colors.AccentRed}>|_| </Text>
)}
{geminiMdFileCount > 0 && (
{ctrlCPressedOnce ? (
<Text color={Colors.AccentYellow}>
Press Ctrl+C again to exit.
</Text>
) : geminiMdFileCount > 0 ? (
<Text color={Colors.SubtleComment}>
Using {geminiMdFileCount} GEMINI.md file
{geminiMdFileCount > 1 ? 's' : ''}
</Text>
) : (
<Text> </Text> // Render an empty space to reserve height
)}
</Box>
<Box>