mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 06:31:01 -07:00
bug(ui) fix flicker refreshing background color (#19041)
This commit is contained in:
@@ -612,11 +612,10 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
setThemeError,
|
||||
historyManager.addItem,
|
||||
initializationResult.themeError,
|
||||
refreshStatic,
|
||||
);
|
||||
|
||||
// Poll for terminal background color changes to auto-switch theme
|
||||
useTerminalTheme(handleThemeSelect, config, refreshStatic);
|
||||
|
||||
const {
|
||||
authState,
|
||||
setAuthState,
|
||||
|
||||
@@ -79,14 +79,12 @@ describe('ThemeDialog Snapshots', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should call refreshStatic when a theme is selected', async () => {
|
||||
const mockRefreshStatic = vi.fn();
|
||||
it('should call onSelect when a theme is selected', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { stdin } = renderWithProviders(
|
||||
<ThemeDialog {...baseProps} settings={settings} />,
|
||||
{
|
||||
settings,
|
||||
uiActions: { refreshStatic: mockRefreshStatic },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -96,7 +94,6 @@ describe('ThemeDialog Snapshots', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefreshStatic).toHaveBeenCalled();
|
||||
expect(baseProps.onSelect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { ScopeSelector } from './shared/ScopeSelector.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
interface ThemeDialogProps {
|
||||
@@ -85,7 +84,6 @@ export function ThemeDialog({
|
||||
terminalWidth,
|
||||
}: ThemeDialogProps): React.JSX.Element {
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const { refreshStatic } = useUIActions();
|
||||
const { terminalBackgroundColor } = useUIState();
|
||||
const [selectedScope, setSelectedScope] = useState<LoadableSettingScope>(
|
||||
SettingScope.User,
|
||||
@@ -142,9 +140,8 @@ export function ThemeDialog({
|
||||
const handleThemeSelect = useCallback(
|
||||
async (themeName: string) => {
|
||||
await onSelect(themeName, selectedScope);
|
||||
refreshStatic();
|
||||
},
|
||||
[onSelect, selectedScope, refreshStatic],
|
||||
[onSelect, selectedScope],
|
||||
);
|
||||
|
||||
const handleThemeHighlight = (themeName: string) => {
|
||||
@@ -159,9 +156,8 @@ export function ThemeDialog({
|
||||
const handleScopeSelect = useCallback(
|
||||
async (scope: LoadableSettingScope) => {
|
||||
await onSelect(highlightedThemeName, scope);
|
||||
refreshStatic();
|
||||
},
|
||||
[onSelect, highlightedThemeName, refreshStatic],
|
||||
[onSelect, highlightedThemeName],
|
||||
);
|
||||
|
||||
const [mode, setMode] = useState<'theme' | 'scope'>('theme');
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useTerminalTheme } from './useTerminalTheme.js';
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
|
||||
import os from 'node:os';
|
||||
import { themeManager } from '../themes/theme-manager.js';
|
||||
|
||||
// Mocks
|
||||
const mockWrite = vi.fn();
|
||||
const mockSubscribe = vi.fn();
|
||||
const mockUnsubscribe = vi.fn();
|
||||
@@ -72,9 +72,7 @@ describe('useTerminalTheme', () => {
|
||||
config = makeFakeConfig({
|
||||
targetDir: os.tmpdir(),
|
||||
});
|
||||
// Set initial background to ensure the hook passes the startup check.
|
||||
config.setTerminalBackground('#000000');
|
||||
// Spy on future updates.
|
||||
vi.spyOn(config, 'setTerminalBackground');
|
||||
|
||||
mockWrite.mockClear();
|
||||
@@ -82,7 +80,7 @@ describe('useTerminalTheme', () => {
|
||||
mockUnsubscribe.mockClear();
|
||||
mockHandleThemeSelect.mockClear();
|
||||
mockQueryTerminalBackground.mockClear();
|
||||
// Reset any settings modifications
|
||||
vi.mocked(themeManager.setTerminalBackground).mockClear();
|
||||
mockSettings.merged.ui.autoThemeSwitching = true;
|
||||
mockSettings.merged.ui.theme = 'default';
|
||||
});
|
||||
@@ -108,7 +106,6 @@ describe('useTerminalTheme', () => {
|
||||
it('should poll for terminal background', () => {
|
||||
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
|
||||
|
||||
// Fast-forward time (1 minute)
|
||||
vi.advanceTimersByTime(60000);
|
||||
expect(mockQueryTerminalBackground).toHaveBeenCalled();
|
||||
});
|
||||
@@ -117,20 +114,23 @@ describe('useTerminalTheme', () => {
|
||||
config.getTerminalBackground = vi.fn().mockReturnValue(undefined);
|
||||
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
|
||||
|
||||
// Poll should not happen
|
||||
vi.advanceTimersByTime(60000);
|
||||
expect(mockQueryTerminalBackground).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should switch to light theme when background is light', () => {
|
||||
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
|
||||
it('should switch to light theme when background is light and not call refreshStatic directly', () => {
|
||||
const refreshStatic = vi.fn();
|
||||
renderHook(() =>
|
||||
useTerminalTheme(mockHandleThemeSelect, config, refreshStatic),
|
||||
);
|
||||
|
||||
const handler = mockSubscribe.mock.calls[0][0];
|
||||
|
||||
// Simulate light background response (white)
|
||||
handler('rgb:ffff/ffff/ffff');
|
||||
|
||||
expect(config.setTerminalBackground).toHaveBeenCalledWith('#ffffff');
|
||||
expect(themeManager.setTerminalBackground).toHaveBeenCalledWith('#ffffff');
|
||||
expect(refreshStatic).not.toHaveBeenCalled();
|
||||
expect(mockHandleThemeSelect).toHaveBeenCalledWith(
|
||||
'default-light',
|
||||
expect.anything(),
|
||||
@@ -138,31 +138,51 @@ describe('useTerminalTheme', () => {
|
||||
});
|
||||
|
||||
it('should switch to dark theme when background is dark', () => {
|
||||
// Start with light theme
|
||||
mockSettings.merged.ui.theme = 'default-light';
|
||||
|
||||
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
|
||||
config.setTerminalBackground('#ffffff');
|
||||
|
||||
const refreshStatic = vi.fn();
|
||||
renderHook(() =>
|
||||
useTerminalTheme(mockHandleThemeSelect, config, refreshStatic),
|
||||
);
|
||||
|
||||
const handler = mockSubscribe.mock.calls[0][0];
|
||||
|
||||
// Simulate dark background response (black)
|
||||
handler('rgb:0000/0000/0000');
|
||||
|
||||
expect(config.setTerminalBackground).toHaveBeenCalledWith('#000000');
|
||||
expect(themeManager.setTerminalBackground).toHaveBeenCalledWith('#000000');
|
||||
expect(refreshStatic).not.toHaveBeenCalled();
|
||||
expect(mockHandleThemeSelect).toHaveBeenCalledWith(
|
||||
'default',
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
// Reset theme
|
||||
mockSettings.merged.ui.theme = 'default';
|
||||
});
|
||||
|
||||
it('should not update config or call refreshStatic on repeated identical background reports', () => {
|
||||
const refreshStatic = vi.fn();
|
||||
renderHook(() =>
|
||||
useTerminalTheme(mockHandleThemeSelect, config, refreshStatic),
|
||||
);
|
||||
|
||||
const handler = mockSubscribe.mock.calls[0][0];
|
||||
|
||||
handler('rgb:0000/0000/0000');
|
||||
|
||||
expect(config.setTerminalBackground).not.toHaveBeenCalled();
|
||||
expect(themeManager.setTerminalBackground).not.toHaveBeenCalled();
|
||||
expect(refreshStatic).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockHandleThemeSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not switch theme if autoThemeSwitching is disabled', () => {
|
||||
mockSettings.merged.ui.autoThemeSwitching = false;
|
||||
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
|
||||
|
||||
// Poll should not happen
|
||||
vi.advanceTimersByTime(60000);
|
||||
expect(mockQueryTerminalBackground).not.toHaveBeenCalled();
|
||||
|
||||
|
||||
@@ -56,11 +56,18 @@ export function useTerminalTheme(
|
||||
if (!match) return;
|
||||
|
||||
const hexColor = parseColor(match[1], match[2], match[3]);
|
||||
const luminance = getLuminance(hexColor);
|
||||
if (!hexColor) return;
|
||||
|
||||
const previousColor = config.getTerminalBackground();
|
||||
|
||||
if (previousColor === hexColor) {
|
||||
return;
|
||||
}
|
||||
|
||||
config.setTerminalBackground(hexColor);
|
||||
themeManager.setTerminalBackground(hexColor);
|
||||
refreshStatic();
|
||||
|
||||
const luminance = getLuminance(hexColor);
|
||||
const currentThemeName = settings.merged.ui.theme;
|
||||
|
||||
const newTheme = shouldSwitchTheme(
|
||||
@@ -72,6 +79,11 @@ export function useTerminalTheme(
|
||||
|
||||
if (newTheme) {
|
||||
void handleThemeSelect(newTheme, SettingScope.User);
|
||||
} else {
|
||||
// The existing theme had its background changed so refresh because
|
||||
// there may be existing static UI rendered that relies on the old
|
||||
// background color.
|
||||
refreshStatic();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export const useThemeCommand = (
|
||||
setThemeError: (error: string | null) => void,
|
||||
addItem: UseHistoryManagerReturn['addItem'],
|
||||
initialThemeError: string | null,
|
||||
refreshStatic: () => void,
|
||||
): UseThemeCommandReturn => {
|
||||
const [isThemeDialogOpen, setIsThemeDialogOpen] =
|
||||
useState(!!initialThemeError);
|
||||
@@ -102,12 +103,13 @@ export const useThemeCommand = (
|
||||
themeManager.loadCustomThemes(loadedSettings.merged.ui.customThemes);
|
||||
}
|
||||
applyTheme(loadedSettings.merged.ui.theme); // Apply the current theme
|
||||
refreshStatic();
|
||||
setThemeError(null);
|
||||
} finally {
|
||||
setIsThemeDialogOpen(false); // Close the dialog
|
||||
}
|
||||
},
|
||||
[applyTheme, loadedSettings, setThemeError],
|
||||
[applyTheme, loadedSettings, refreshStatic, setThemeError],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user