mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
* 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
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
let detectionComplete = false;
|
|
let protocolSupported = false;
|
|
let protocolEnabled = false;
|
|
|
|
/**
|
|
* Detects Kitty keyboard protocol support.
|
|
* Definitive document about this protocol lives at https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
* This function should be called once at app startup.
|
|
*/
|
|
export async function detectAndEnableKittyProtocol(): Promise<boolean> {
|
|
if (detectionComplete) {
|
|
return protocolSupported;
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
detectionComplete = true;
|
|
resolve(false);
|
|
return;
|
|
}
|
|
|
|
const originalRawMode = process.stdin.isRaw;
|
|
if (!originalRawMode) {
|
|
process.stdin.setRawMode(true);
|
|
}
|
|
|
|
let responseBuffer = '';
|
|
let progressiveEnhancementReceived = false;
|
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
|
|
const onTimeout = () => {
|
|
timeoutId = undefined;
|
|
process.stdin.removeListener('data', handleData);
|
|
|
|
// Keep a drain handler briefly to consume any late-arriving terminal
|
|
// responses that would otherwise leak into the application input.
|
|
const drainHandler = () => {};
|
|
process.stdin.on('data', drainHandler);
|
|
|
|
setTimeout(() => {
|
|
process.stdin.removeListener('data', drainHandler);
|
|
if (!originalRawMode) {
|
|
process.stdin.setRawMode(false);
|
|
}
|
|
detectionComplete = true;
|
|
resolve(false);
|
|
}, 100);
|
|
};
|
|
|
|
const handleData = (data: Buffer) => {
|
|
if (timeoutId === undefined) {
|
|
// Race condition. We have already timed out.
|
|
return;
|
|
}
|
|
responseBuffer += data.toString();
|
|
|
|
// Check for progressive enhancement response (CSI ? <flags> u)
|
|
if (responseBuffer.includes('\x1b[?') && responseBuffer.includes('u')) {
|
|
progressiveEnhancementReceived = true;
|
|
// Give more time to get the full set of kitty responses if we have an
|
|
// indication the terminal probably supports kitty and we just need to
|
|
// wait a bit longer for a response.
|
|
clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(onTimeout, 1000);
|
|
}
|
|
|
|
// Check for device attributes response (CSI ? <attrs> c)
|
|
if (responseBuffer.includes('\x1b[?') && responseBuffer.includes('c')) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = undefined;
|
|
process.stdin.removeListener('data', handleData);
|
|
|
|
if (!originalRawMode) {
|
|
process.stdin.setRawMode(false);
|
|
}
|
|
|
|
if (progressiveEnhancementReceived) {
|
|
// Enable the protocol
|
|
process.stdout.write('\x1b[>1u');
|
|
protocolSupported = true;
|
|
protocolEnabled = true;
|
|
|
|
// 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;
|
|
resolve(protocolSupported);
|
|
}
|
|
};
|
|
|
|
process.stdin.on('data', handleData);
|
|
|
|
// Send queries
|
|
process.stdout.write('\x1b[?u'); // Query progressive enhancement
|
|
process.stdout.write('\x1b[c'); // Query device attributes
|
|
|
|
// Timeout after 200ms
|
|
// When a iterm2 terminal does not have focus this can take over 90s on a
|
|
// fast macbook so we need a somewhat longer threshold than would be ideal.
|
|
timeoutId = setTimeout(onTimeout, 200);
|
|
});
|
|
}
|
|
|
|
function disableProtocol() {
|
|
if (protocolEnabled) {
|
|
process.stdout.write('\x1b[<u');
|
|
protocolEnabled = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
export function isKittyProtocolSupported(): boolean {
|
|
return protocolSupported;
|
|
}
|