From fe98ca7d24ceb13e7eef8598bd58681f899c8fdb Mon Sep 17 00:00:00 2001 From: ameliesther Date: Fri, 29 May 2026 21:42:33 +0000 Subject: [PATCH 1/5] fix(cli): handle tmux false positive background detection --- .../utils/terminalCapabilityManager.test.ts | 45 ++++++++ .../src/ui/utils/terminalCapabilityManager.ts | 18 ++- packages/cli/src/utils/terminalTheme.test.ts | 106 ++++++++++++++++++ packages/cli/src/utils/terminalTheme.ts | 5 +- 4 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/utils/terminalTheme.test.ts diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts index 261b0e6ca96..13e45403f82 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts @@ -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(); diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.ts index e0fc6c01b84..d3076430133 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.ts @@ -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}`, + ); + } } } diff --git a/packages/cli/src/utils/terminalTheme.test.ts b/packages/cli/src/utils/terminalTheme.test.ts new file mode 100644 index 00000000000..6650d6ca044 --- /dev/null +++ b/packages/cli/src/utils/terminalTheme.test.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach } 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; + + beforeEach(() => { + 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, + }); + }); + + 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(); + }); +}); diff --git a/packages/cli/src/utils/terminalTheme.ts b/packages/cli/src/utils/terminalTheme.ts index 72315d3fa5f..16f1e00ce29 100644 --- a/packages/cli/src/utils/terminalTheme.ts +++ b/packages/cli/src/utils/terminalTheme.ts @@ -56,7 +56,10 @@ export async function setupTerminalAndTheme( config.setTerminalBackground(terminalBackground); themeManager.setTerminalBackground(terminalBackground); - if (terminalBackground !== undefined) { + if ( + terminalBackground !== undefined && + settings.merged.ui.autoThemeSwitching !== false + ) { const currentTheme = themeManager.getActiveTheme(); if (!themeManager.isThemeCompatible(currentTheme, terminalBackground)) { const backgroundType = From cd4675b4a1a9cb15880d97a8ae33b330bc00fd19 Mon Sep 17 00:00:00 2001 From: amelidev Date: Fri, 29 May 2026 16:27:29 -0600 Subject: [PATCH 2/5] Update packages/cli/src/utils/terminalTheme.test.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/cli/src/utils/terminalTheme.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/utils/terminalTheme.test.ts b/packages/cli/src/utils/terminalTheme.test.ts index 6650d6ca044..016468a5c32 100644 --- a/packages/cli/src/utils/terminalTheme.test.ts +++ b/packages/cli/src/utils/terminalTheme.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +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'; From 3f2b9e67c3fec2e2f6a6a2c698b7b21feaeddc21 Mon Sep 17 00:00:00 2001 From: amelidev Date: Fri, 29 May 2026 16:27:46 -0600 Subject: [PATCH 3/5] Update packages/cli/src/utils/terminalTheme.test.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/cli/src/utils/terminalTheme.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/utils/terminalTheme.test.ts b/packages/cli/src/utils/terminalTheme.test.ts index 016468a5c32..025950a1e28 100644 --- a/packages/cli/src/utils/terminalTheme.test.ts +++ b/packages/cli/src/utils/terminalTheme.test.ts @@ -42,6 +42,7 @@ vi.mock('@google/gemini-cli-core', () => ({ describe('setupTerminalAndTheme', () => { let mockConfig: Config; let mockSettings: LoadedSettings; + const originalIsTTY = process.stdin.isTTY; beforeEach(() => { vi.resetAllMocks(); From 9055c4fabe75fddb8481f9ba8aefe2169e1c53a9 Mon Sep 17 00:00:00 2001 From: amelidev Date: Fri, 29 May 2026 16:29:04 -0600 Subject: [PATCH 4/5] Update packages/cli/src/utils/terminalTheme.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/cli/src/utils/terminalTheme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/utils/terminalTheme.ts b/packages/cli/src/utils/terminalTheme.ts index 16f1e00ce29..cf61a8fd2dc 100644 --- a/packages/cli/src/utils/terminalTheme.ts +++ b/packages/cli/src/utils/terminalTheme.ts @@ -58,7 +58,7 @@ export async function setupTerminalAndTheme( if ( terminalBackground !== undefined && - settings.merged.ui.autoThemeSwitching !== false + (settings.merged.ui?.autoThemeSwitching ?? true) ) { const currentTheme = themeManager.getActiveTheme(); if (!themeManager.isThemeCompatible(currentTheme, terminalBackground)) { From 0c8d07fbd7c99523c34230807c72aeb119cae952 Mon Sep 17 00:00:00 2001 From: ameliesther Date: Fri, 29 May 2026 22:36:50 +0000 Subject: [PATCH 5/5] test(cli): restore process.stdin.isTTY in afterEach --- packages/cli/src/utils/terminalTheme.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/cli/src/utils/terminalTheme.test.ts b/packages/cli/src/utils/terminalTheme.test.ts index 025950a1e28..bdde4ce1143 100644 --- a/packages/cli/src/utils/terminalTheme.test.ts +++ b/packages/cli/src/utils/terminalTheme.test.ts @@ -67,6 +67,13 @@ describe('setupTerminalAndTheme', () => { }); }); + 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,