Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions packages/cli/src/ui/utils/terminalCapabilityManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.spyOn(manager, 'isTmux').mockReturnValue(true);

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();
Expand Down
18 changes: 15 additions & 3 deletions packages/cli/src/ui/utils/terminalCapabilityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
);
}
}
}

Expand Down
114 changes: 114 additions & 0 deletions packages/cli/src/utils/terminalTheme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @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';

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(() => {
Comment thread
amelidev marked this conversation as resolved.
vi.resetAllMocks();
mockConfig = {
isInteractive: vi.fn().mockReturnValue(true),
setTerminalBackground: vi.fn(),
} as unknown as Config;
mockSettings = {
merged: {
ui: {
customThemes: {},
theme: 'Dracula',
autoThemeSwitching: true,
},
},
} as unknown as LoadedSettings;

// Mock process.stdin.isTTY
Object.defineProperty(process.stdin, 'isTTY', {
value: true,
configurable: true,
});
});

Comment thread
amelidev marked this conversation as resolved.
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 unknown as never);
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 unknown as never);
vi.mocked(themeManager.isThemeCompatible).mockReturnValue(false);

await setupTerminalAndTheme(mockConfig, mockSettings);

expect(coreEvents.emitFeedback).not.toHaveBeenCalled();
});
});
5 changes: 4 additions & 1 deletion packages/cli/src/utils/terminalTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
) {
Comment thread
amelidev marked this conversation as resolved.
const currentTheme = themeManager.getActiveTheme();
if (!themeManager.isThemeCompatible(currentTheme, terminalBackground)) {
const backgroundType =
Expand Down