fix(cli): disable Kitty keyboard protocol on SIGINT to prevent garbled 9;5u output (#3544)

* fix(cli): disable Kitty keyboard protocol on SIGINT to prevent garbled 9;5u output

When a Kitty-capable terminal (iTerm2, Kitty, WezTerm) is used, the CLI
enables the Kitty keyboard protocol at startup via ESC[>1u. On exit, the
protocol must be disabled with ESC[<u to restore the terminal's default
key encoding. Failing to do so leaves the terminal in Kitty mode: any
subsequent Ctrl+C press is encoded as ESC[99;5u, and since the shell does
not understand this sequence, it echoes the trailing '9;5u' as garbled
text.

Root cause: kittyProtocolDetector registered cleanup handlers for 'exit'
and 'SIGTERM', but omitted SIGINT. A process terminated via SIGINT (e.g.
kill -INT <pid>, a parent process sending SIGINT, or certain process
managers) would exit without disabling the protocol.

Fix:
1. Add process.on('SIGINT', disableProtocol) alongside the existing
   'exit' and 'SIGTERM' handlers in kittyProtocolDetector.ts.
2. Export a new disableKittyProtocol() function for explicit call sites.
3. Call disableKittyProtocol() in the registerCleanup callback in
   gemini.tsx before instance.unmount(), so the disable sequence is
   written while stdout is fully operational regardless of exit path.

Fixes #3528

* fix(test): add disableKittyProtocol to kittyProtocolDetector mock
This commit is contained in:
顾盼 2026-04-24 15:27:55 +08:00 committed by GitHub
parent d75c13aae0
commit 2aad7c0617
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 21 additions and 2 deletions

View file

@ -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', () => ({

View file

@ -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();
});

View file

@ -86,9 +86,11 @@ export async function detectAndEnableKittyProtocol(): Promise<boolean> {
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;
}