fix(cli): handle tmux false positive background detection (#27572)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
amelidev
2026-06-16 18:34:53 +00:00
committed by GitHub
parent fbce3e51b6
commit 5624a3b01d
4 changed files with 179 additions and 4 deletions
@@ -117,6 +117,51 @@ describe('TerminalCapabilityManager', () => {
expect(manager.getTerminalBackgroundColor()).toBe('#00ff00');
});
it('should ignore #ffffff in tmux as it is a common false positive', async () => {
const manager = TerminalCapabilityManager.getInstance();
vi.spyOn(manager, 'isTmux').mockReturnValue(true);
const promise = manager.detectCapabilities();
// Simulate OSC 11 response for white
stdin.emit('data', Buffer.from('\x1b]11;rgb:ffff/ffff/ffff\x1b\\'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.getTerminalBackgroundColor()).toBeUndefined();
});
it('should not ignore #ffffff when NOT in tmux', async () => {
const manager = TerminalCapabilityManager.getInstance();
vi.spyOn(manager, 'isTmux').mockReturnValue(false);
const promise = manager.detectCapabilities();
// Simulate OSC 11 response for white
stdin.emit('data', Buffer.from('\x1b]11;rgb:ffff/ffff/ffff\x1b\\'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.getTerminalBackgroundColor()).toBe('#ffffff');
});
it('should NOT ignore other colors in tmux', async () => {
const manager = TerminalCapabilityManager.getInstance();
vi.stubEnv('TMUX', '1');
const promise = manager.detectCapabilities();
// Simulate OSC 11 response for grey
stdin.emit('data', Buffer.from('\x1b]11;rgb:8888/8888/8888\x1b\\'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.getTerminalBackgroundColor()).toBe('#888888');
});
it('should detect Terminal Name', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
@@ -161,9 +161,21 @@ export class TerminalCapabilityManager {
match[2],
match[3],
);
debugLogger.log(
`Detected terminal background color: ${this.terminalBackgroundColor}`,
);
// Heuristic: tmux 3.5+ may report #ffffff when it doesn't know the
// actual host terminal color (e.g. over mosh). We ignore this specific
// fallback value to prevent blinding the user with a light theme in a
// likely dark terminal.
if (this.terminalBackgroundColor === '#ffffff' && this.isTmux()) {
debugLogger.log(
'Ignored #ffffff background in tmux (common false positive over mosh).',
);
this.terminalBackgroundColor = undefined;
} else {
debugLogger.log(
`Detected terminal background color: ${this.terminalBackgroundColor}`,
);
}
}
}
@@ -0,0 +1,115 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { setupTerminalAndTheme } from './terminalTheme.js';
import { terminalCapabilityManager } from '../ui/utils/terminalCapabilityManager.js';
import { themeManager } from '../ui/themes/theme-manager.js';
import { coreEvents, type Config } from '@google/gemini-cli-core';
import type { LoadedSettings } from '../config/settings.js';
import type { Theme } from '../ui/themes/theme.js';
vi.mock('../ui/utils/terminalCapabilityManager.js', () => ({
terminalCapabilityManager: {
detectCapabilities: vi.fn(),
getTerminalBackgroundColor: vi.fn(),
},
}));
vi.mock('../ui/themes/theme-manager.js', () => ({
themeManager: {
loadCustomThemes: vi.fn(),
setActiveTheme: vi.fn(),
getActiveTheme: vi.fn(),
setTerminalBackground: vi.fn(),
isThemeCompatible: vi.fn(),
getAllThemes: vi.fn().mockReturnValue([]),
},
DEFAULT_THEME: { name: 'Default Dark' },
}));
vi.mock('@google/gemini-cli-core', () => ({
coreEvents: {
emitFeedback: vi.fn(),
},
debugLogger: {
warn: vi.fn(),
},
}));
describe('setupTerminalAndTheme', () => {
let mockConfig: Config;
let mockSettings: LoadedSettings;
const originalIsTTY = process.stdin.isTTY;
beforeEach(() => {
vi.resetAllMocks();
mockConfig = {
isInteractive: vi.fn().mockReturnValue(true),
setTerminalBackground: vi.fn(),
} as Partial<Config> as Config;
mockSettings = {
merged: {
ui: {
customThemes: {},
theme: 'Dracula',
autoThemeSwitching: true,
},
},
} as Partial<LoadedSettings> as LoadedSettings;
// Mock process.stdin.isTTY
Object.defineProperty(process.stdin, 'isTTY', {
value: true,
configurable: true,
});
});
afterEach(() => {
Object.defineProperty(process.stdin, 'isTTY', {
value: originalIsTTY,
configurable: true,
});
});
it('should emit warning when theme is incompatible and autoThemeSwitching is enabled', async () => {
vi.mocked(
terminalCapabilityManager.getTerminalBackgroundColor,
).mockReturnValue('#ffffff'); // Light
vi.mocked(themeManager.setActiveTheme).mockReturnValue(true);
vi.mocked(themeManager.getActiveTheme).mockReturnValue({
name: 'Dracula',
type: 'dark',
} as Theme);
vi.mocked(themeManager.isThemeCompatible).mockReturnValue(false);
await setupTerminalAndTheme(mockConfig, mockSettings);
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'warning',
expect.stringContaining(
"Theme 'Dracula' (dark) might look incorrect on your light terminal background",
),
);
});
it('should NOT emit warning when theme is incompatible but autoThemeSwitching is DISABLED', async () => {
mockSettings.merged.ui.autoThemeSwitching = false;
vi.mocked(
terminalCapabilityManager.getTerminalBackgroundColor,
).mockReturnValue('#ffffff'); // Light
vi.mocked(themeManager.setActiveTheme).mockReturnValue(true);
vi.mocked(themeManager.getActiveTheme).mockReturnValue({
name: 'Dracula',
type: 'dark',
} as Theme);
vi.mocked(themeManager.isThemeCompatible).mockReturnValue(false);
await setupTerminalAndTheme(mockConfig, mockSettings);
expect(coreEvents.emitFeedback).not.toHaveBeenCalled();
});
});
+4 -1
View File
@@ -56,7 +56,10 @@ export async function setupTerminalAndTheme(
config.setTerminalBackground(terminalBackground);
themeManager.setTerminalBackground(terminalBackground);
if (terminalBackground !== undefined) {
if (
terminalBackground !== undefined &&
(settings.merged.ui.autoThemeSwitching ?? true)
) {
const currentTheme = themeManager.getActiveTheme();
if (!themeManager.isThemeCompatible(currentTheme, terminalBackground)) {
const backgroundType =