diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 197b8dcb0..6ecf2f0cc 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -671,6 +671,7 @@ describe('startInteractiveUI', () => { vi.mock('./ui/utils/kittyProtocolDetector.js', () => ({ detectAndEnableKittyProtocol: vi.fn(() => Promise.resolve(true)), + disableKittyProtocol: vi.fn(), })); vi.mock('./ui/utils/updateCheck.js', () => ({ diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index e151d9dee..49c29532d 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -46,7 +46,10 @@ import { VimModeProvider } from './ui/contexts/VimModeContext.js'; import { AgentViewProvider } from './ui/contexts/AgentViewContext.js'; import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js'; import { themeManager, AUTO_THEME_NAME } from './ui/themes/theme-manager.js'; -import { detectAndEnableKittyProtocol } from './ui/utils/kittyProtocolDetector.js'; +import { + detectAndEnableKittyProtocol, + disableKittyProtocol, +} from './ui/utils/kittyProtocolDetector.js'; import { checkForUpdates } from './ui/utils/updateCheck.js'; import { cleanupCheckpoints, @@ -289,6 +292,10 @@ export async function startInteractiveUI( registerCleanup(async () => { remoteInputWatcher?.shutdown(); await dualOutputBridge?.shutdown(); + // Explicitly disable the Kitty keyboard protocol before unmounting Ink so + // that the disable escape sequence is written while stdout is still fully + // operational, preventing garbled terminal output after the app exits. + disableKittyProtocol(); instance.unmount(); restoreTerminalRedrawOptimizer(); }); diff --git a/packages/cli/src/ui/utils/kittyProtocolDetector.ts b/packages/cli/src/ui/utils/kittyProtocolDetector.ts index a46390603..2653d1aa8 100644 --- a/packages/cli/src/ui/utils/kittyProtocolDetector.ts +++ b/packages/cli/src/ui/utils/kittyProtocolDetector.ts @@ -86,9 +86,11 @@ export async function detectAndEnableKittyProtocol(): Promise { protocolSupported = true; protocolEnabled = true; - // Set up cleanup on exit + // Set up cleanup on exit (exit covers process.exit() calls, + // SIGTERM/SIGINT cover signal-based terminations). process.on('exit', disableProtocol); process.on('SIGTERM', disableProtocol); + process.on('SIGINT', disableProtocol); } detectionComplete = true; @@ -116,6 +118,15 @@ function disableProtocol() { } } +/** + * Explicitly disables the Kitty keyboard protocol. Should be called during + * application cleanup before process.exit() to ensure the terminal is restored + * even if the 'exit' event handler does not fire in time (e.g. on SIGKILL). + */ +export function disableKittyProtocol(): void { + disableProtocol(); +} + export function isKittyProtocolEnabled(): boolean { return protocolEnabled; }