diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts index 8d28f632c3..42f59f95a1 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts @@ -21,6 +21,8 @@ vi.mock('@google/gemini-cli-core', () => ({ }, enableKittyKeyboardProtocol: vi.fn(), disableKittyKeyboardProtocol: vi.fn(), + enableModifyOtherKeys: vi.fn(), + disableModifyOtherKeys: vi.fn(), })); describe('TerminalCapabilityManager', () => { @@ -174,4 +176,92 @@ describe('TerminalCapabilityManager', () => { await promise; expect(manager.isKittyProtocolEnabled()).toBe(true); }); + + describe('modifyOtherKeys detection', () => { + it('should detect modifyOtherKeys support (level 2)', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + // Simulate modifyOtherKeys level 2 response: \x1b[>4;2m + stdin.emit('data', Buffer.from('\x1b[>4;2m')); + // Complete detection with DA1 + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + expect(manager.isModifyOtherKeysEnabled()).toBe(true); + }); + + it('should not enable modifyOtherKeys for level 0', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + // Simulate modifyOtherKeys level 0 response: \x1b[>4;0m + stdin.emit('data', Buffer.from('\x1b[>4;0m')); + // Complete detection with DA1 + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + expect(manager.isModifyOtherKeysEnabled()).toBe(false); + }); + + it('should prefer Kitty over modifyOtherKeys', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + // Simulate both Kitty and modifyOtherKeys responses + stdin.emit('data', Buffer.from('\x1b[?1u')); + stdin.emit('data', Buffer.from('\x1b[>4;2m')); + // Complete detection with DA1 + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + expect(manager.isKittyProtocolEnabled()).toBe(true); + expect(manager.isModifyOtherKeysEnabled()).toBe(false); + }); + + it('should enable modifyOtherKeys when Kitty not supported', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + // Simulate only modifyOtherKeys response (no Kitty) + stdin.emit('data', Buffer.from('\x1b[>4;2m')); + // Complete detection with DA1 + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + expect(manager.isModifyOtherKeysEnabled()).toBe(true); + expect(manager.isKittyProtocolEnabled()).toBe(false); + }); + + it('should handle split modifyOtherKeys response chunks', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + // Split response: \x1b[>4;2m + stdin.emit('data', Buffer.from('\x1b[>4;')); + stdin.emit('data', Buffer.from('2m')); + // Complete detection with DA1 + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + expect(manager.isModifyOtherKeysEnabled()).toBe(true); + }); + + it('should detect modifyOtherKeys with other capabilities', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + stdin.emit('data', Buffer.from('\x1b]11;rgb:1a1a/1a1a/1a1a\x1b\\')); // background color + stdin.emit('data', Buffer.from('\x1bP>|tmux\x1b\\')); // Terminal name + stdin.emit('data', Buffer.from('\x1b[>4;2m')); // modifyOtherKeys + // Complete detection with DA1 + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + + expect(manager.getTerminalBackgroundColor()).toBe('#1a1a1a'); + expect(manager.getTerminalName()).toBe('tmux'); + expect(manager.isModifyOtherKeysEnabled()).toBe(true); + }); + }); }); diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.ts index e7782883ca..c893c46043 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.ts @@ -9,6 +9,8 @@ import { debugLogger, enableKittyKeyboardProtocol, disableKittyKeyboardProtocol, + enableModifyOtherKeys, + disableModifyOtherKeys, } from '@google/gemini-cli-core'; export type TerminalBackgroundColor = string | undefined; @@ -20,6 +22,7 @@ export class TerminalCapabilityManager { private static readonly OSC_11_QUERY = '\x1b]11;?\x1b\\'; private static readonly TERMINAL_NAME_QUERY = '\x1b[>q'; private static readonly DEVICE_ATTRIBUTES_QUERY = '\x1b[c'; + private static readonly MODIFY_OTHER_KEYS_QUERY = '\x1b[>4;?m'; // Kitty keyboard flags: CSI ? flags u // eslint-disable-next-line no-control-regex @@ -34,12 +37,17 @@ export class TerminalCapabilityManager { private static readonly OSC_11_REGEX = // eslint-disable-next-line no-control-regex /\x1b\]11;rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})(\x1b\\|\x07)?/; + // modifyOtherKeys response: CSI > 4 ; level m + // eslint-disable-next-line no-control-regex + private static readonly MODIFY_OTHER_KEYS_REGEX = /\x1b\[>4;(\d+)m/; private terminalBackgroundColor: TerminalBackgroundColor; private kittySupported = false; private kittyEnabled = false; private detectionComplete = false; private terminalName: string | undefined; + private modifyOtherKeysSupported = false; + private modifyOtherKeysEnabled = false; private constructor() {} @@ -78,6 +86,7 @@ export class TerminalCapabilityManager { let terminalNameReceived = false; let deviceAttributesReceived = false; let bgReceived = false; + let modifyOtherKeysReceived = false; // eslint-disable-next-line prefer-const let timeoutId: NodeJS.Timeout; @@ -96,6 +105,10 @@ export class TerminalCapabilityManager { this.enableKittyProtocol(); process.on('exit', () => this.disableKittyProtocol()); process.on('SIGTERM', () => this.disableKittyProtocol()); + } else if (this.modifyOtherKeysSupported) { + this.enableModifyOtherKeys(); + process.on('exit', () => this.disableModifyOtherKeys()); + process.on('SIGTERM', () => this.disableModifyOtherKeys()); } resolve(); @@ -161,6 +174,21 @@ export class TerminalCapabilityManager { cleanup(); } } + + // check for modifyOtherKeys support + if (!modifyOtherKeysReceived) { + const match = buffer.match( + TerminalCapabilityManager.MODIFY_OTHER_KEYS_REGEX, + ); + if (match) { + modifyOtherKeysReceived = true; + const level = parseInt(match[1], 10); + this.modifyOtherKeysSupported = level >= 2; + debugLogger.log( + `Detected modifyOtherKeys support: ${this.modifyOtherKeysSupported} (level ${level})`, + ); + } + } }; process.stdin.on('data', onData); @@ -171,6 +199,7 @@ export class TerminalCapabilityManager { TerminalCapabilityManager.KITTY_QUERY + TerminalCapabilityManager.OSC_11_QUERY + TerminalCapabilityManager.TERMINAL_NAME_QUERY + + TerminalCapabilityManager.MODIFY_OTHER_KEYS_QUERY + TerminalCapabilityManager.DEVICE_ATTRIBUTES_QUERY, ); } catch (e) { @@ -214,6 +243,32 @@ export class TerminalCapabilityManager { } } + enableModifyOtherKeys(): void { + try { + if (this.modifyOtherKeysSupported) { + enableModifyOtherKeys(); + this.modifyOtherKeysEnabled = true; + } + } catch (e) { + debugLogger.warn('Failed to enable modifyOtherKeys protocol:', e); + } + } + + disableModifyOtherKeys(): void { + try { + if (this.modifyOtherKeysEnabled) { + disableModifyOtherKeys(); + this.modifyOtherKeysEnabled = false; + } + } catch (e) { + debugLogger.warn('Failed to disable modifyOtherKeys protocol:', e); + } + } + + isModifyOtherKeysEnabled(): boolean { + return this.modifyOtherKeysEnabled; + } + private parseColor(rHex: string, gHex: string, bHex: string): string { const parseComponent = (hex: string) => { const val = parseInt(hex, 16); diff --git a/packages/core/src/utils/terminal.ts b/packages/core/src/utils/terminal.ts index 008919ea49..f8070a4a37 100644 --- a/packages/core/src/utils/terminal.ts +++ b/packages/core/src/utils/terminal.ts @@ -26,6 +26,14 @@ export function disableKittyKeyboardProtocol() { writeToStdout('\x1b[4;2m'); +} + +export function disableModifyOtherKeys() { + writeToStdout('\x1b[>4;0m'); +} + export function enableLineWrapping() { writeToStdout('\x1b[?7h'); }