diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 7717b1e2f9..0d000fc79f 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -34,7 +34,6 @@ import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion. import clipboardy from 'clipboardy'; import * as clipboardUtils from '../utils/clipboardUtils.js'; import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js'; -import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import stripAnsi from 'strip-ansi'; import chalk from 'chalk'; @@ -125,10 +124,6 @@ describe('InputPrompt', () => { beforeEach(() => { vi.resetAllMocks(); - vi.spyOn( - terminalCapabilityManager, - 'isBracketedPasteEnabled', - ).mockReturnValue(true); mockCommandContext = createMockCommandContext(); diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index 2c0c10e502..d33130744a 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -35,7 +35,6 @@ import { type SettingDefinition, type SettingsSchemaType, } from '../../config/settingsSchema.js'; -import { terminalCapabilityManager } from '../../ui/utils/terminalCapabilityManager.js'; // Mock the VimModeContext const mockToggleVimEnabled = vi.fn(); @@ -254,10 +253,6 @@ const renderDialog = ( describe('SettingsDialog', () => { beforeEach(() => { - vi.spyOn( - terminalCapabilityManager, - 'isBracketedPasteEnabled', - ).mockReturnValue(true); mockToggleVimEnabled.mockResolvedValue(true); }); diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts index e10ca2593c..67f16e5db2 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts @@ -7,6 +7,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { TerminalCapabilityManager } from './terminalCapabilityManager.js'; import { EventEmitter } from 'node:events'; +import { + enableKittyKeyboardProtocol, + enableModifyOtherKeys, +} from '@google/gemini-cli-core'; // Mock fs vi.mock('node:fs', () => ({ @@ -190,7 +194,8 @@ describe('TerminalCapabilityManager', () => { stdin.emit('data', Buffer.from('\x1b[?62c')); await promise; - expect(manager.isModifyOtherKeysEnabled()).toBe(true); + + expect(enableModifyOtherKeys).toHaveBeenCalled(); }); it('should not enable modifyOtherKeys for level 0', async () => { @@ -203,7 +208,8 @@ describe('TerminalCapabilityManager', () => { stdin.emit('data', Buffer.from('\x1b[?62c')); await promise; - expect(manager.isModifyOtherKeysEnabled()).toBe(false); + + expect(enableModifyOtherKeys).not.toHaveBeenCalled(); }); it('should prefer Kitty over modifyOtherKeys', async () => { @@ -218,7 +224,9 @@ describe('TerminalCapabilityManager', () => { await promise; expect(manager.isKittyProtocolEnabled()).toBe(true); - expect(manager.isModifyOtherKeysEnabled()).toBe(false); + + expect(enableKittyKeyboardProtocol).toHaveBeenCalled(); + expect(enableModifyOtherKeys).not.toHaveBeenCalled(); }); it('should enable modifyOtherKeys when Kitty not supported', async () => { @@ -231,8 +239,9 @@ describe('TerminalCapabilityManager', () => { stdin.emit('data', Buffer.from('\x1b[?62c')); await promise; - expect(manager.isModifyOtherKeysEnabled()).toBe(true); + expect(manager.isKittyProtocolEnabled()).toBe(false); + expect(enableModifyOtherKeys).toHaveBeenCalled(); }); it('should handle split modifyOtherKeys response chunks', async () => { @@ -246,7 +255,8 @@ describe('TerminalCapabilityManager', () => { stdin.emit('data', Buffer.from('\x1b[?62c')); await promise; - expect(manager.isModifyOtherKeysEnabled()).toBe(true); + + expect(enableModifyOtherKeys).toHaveBeenCalled(); }); it('should detect modifyOtherKeys with other capabilities', async () => { @@ -263,7 +273,23 @@ describe('TerminalCapabilityManager', () => { expect(manager.getTerminalBackgroundColor()).toBe('#1a1a1a'); expect(manager.getTerminalName()).toBe('tmux'); - expect(manager.isModifyOtherKeysEnabled()).toBe(true); + + expect(enableModifyOtherKeys).toHaveBeenCalled(); + }); + + it('should infer modifyOtherKeys support from Device Attributes (DA1) alone', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + // Simulate only DA1 response (no specific MOK or Kitty response) + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + + expect(manager.isKittyProtocolEnabled()).toBe(false); + // It should fall back to modifyOtherKeys because DA1 proves it's an ANSI terminal + + expect(enableModifyOtherKeys).toHaveBeenCalled(); }); }); }); diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.ts index 7b09a33e4e..50a69ee707 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.ts @@ -43,14 +43,13 @@ export class TerminalCapabilityManager { // eslint-disable-next-line no-control-regex private static readonly MODIFY_OTHER_KEYS_REGEX = /\x1b\[>4;(\d+)m/; + private detectionComplete = false; private terminalBackgroundColor: TerminalBackgroundColor; private kittySupported = false; private kittyEnabled = false; - private detectionComplete = false; private terminalName: string | undefined; - private modifyOtherKeysSupported = false; - private modifyOtherKeysEnabled = false; - private bracketedPasteEnabled = false; + private modifyOtherKeysSupported?: boolean; + private deviceAttributesSupported = false; private constructor() {} @@ -187,6 +186,7 @@ export class TerminalCapabilityManager { ); if (match) { deviceAttributesReceived = true; + this.deviceAttributesSupported = true; cleanup(); } } @@ -215,13 +215,17 @@ export class TerminalCapabilityManager { if (this.kittySupported) { enableKittyKeyboardProtocol(); this.kittyEnabled = true; - } else if (this.modifyOtherKeysSupported) { + } else if ( + this.modifyOtherKeysSupported === true || + // If device attributes were received it's safe to try enabling + // anyways, since it will be ignored if unsupported + (this.modifyOtherKeysSupported === undefined && + this.deviceAttributesSupported) + ) { enableModifyOtherKeys(); - this.modifyOtherKeysEnabled = true; } // Always enable bracketed paste since it'll be ignored if unsupported. enableBracketedPasteMode(); - this.bracketedPasteEnabled = true; } catch (e) { debugLogger.warn('Failed to enable keyboard protocols:', e); } @@ -239,14 +243,6 @@ export class TerminalCapabilityManager { return this.kittyEnabled; } - isBracketedPasteEnabled(): boolean { - return this.bracketedPasteEnabled; - } - - isModifyOtherKeysEnabled(): boolean { - return this.modifyOtherKeysEnabled; - } - private parseColor(rHex: string, gHex: string, bHex: string): string { const parseComponent = (hex: string) => { const val = parseInt(hex, 16);