Added modifyOtherKeys protocol support for tmux (#15524)

Co-authored-by: Ishaan Gupta <ishaankone@gmail.com>
This commit is contained in:
Vedant Mahajan
2025-12-26 00:39:38 +05:30
committed by GitHub
parent e6344a8c24
commit 546baf9934
3 changed files with 153 additions and 0 deletions

View File

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

View File

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

View File

@@ -26,6 +26,14 @@ export function disableKittyKeyboardProtocol() {
writeToStdout('\x1b[<u');
}
export function enableModifyOtherKeys() {
writeToStdout('\x1b[>4;2m');
}
export function disableModifyOtherKeys() {
writeToStdout('\x1b[>4;0m');
}
export function enableLineWrapping() {
writeToStdout('\x1b[?7h');
}