diff --git a/packages/cli/src/ui/hooks/useThemeCommand.test.ts b/packages/cli/src/ui/hooks/useThemeCommand.test.ts new file mode 100644 index 000000000..65d3650c8 --- /dev/null +++ b/packages/cli/src/ui/hooks/useThemeCommand.test.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { act } from 'react'; +import { renderHook } from '@testing-library/react'; +import type { LoadedSettings } from '../../config/settings.js'; +import { SettingScope } from '../../config/settings.js'; +import { useThemeCommand } from './useThemeCommand.js'; +import { themeManager } from '../themes/theme-manager.js'; + +describe('useThemeCommand', () => { + beforeEach(() => { + vi.restoreAllMocks(); + themeManager.setActiveTheme('Qwen Dark'); + }); + + it('restores previous theme on cancel (Esc)', () => { + const setValue = + vi.fn<(scope: SettingScope, key: string, value: unknown) => void>(); + const settings = { + merged: { ui: { theme: 'Qwen Dark' } }, + user: { settings: { ui: {} } }, + workspace: { settings: { ui: {} } }, + setValue, + } as unknown as LoadedSettings; + + const setThemeError = vi.fn<(error: string | null) => void>(); + const addItem = vi.fn(); + + const { result } = renderHook(() => + useThemeCommand(settings, setThemeError, addItem, null), + ); + + act(() => { + themeManager.setActiveTheme('Dracula'); + result.current.openThemeDialog(); + result.current.handleThemeHighlight('Default'); + }); + expect(themeManager.getActiveTheme().name).toBe('Default'); + + act(() => { + result.current.handleThemeSelect(undefined, SettingScope.User); + }); + + expect(themeManager.getActiveTheme().name).toBe('Dracula'); + expect(setValue).not.toHaveBeenCalled(); + expect(result.current.isThemeDialogOpen).toBe(false); + }); +}); diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts index 467ef313e..e0f0383cb 100644 --- a/packages/cli/src/ui/hooks/useThemeCommand.ts +++ b/packages/cli/src/ui/hooks/useThemeCommand.ts @@ -29,6 +29,9 @@ export const useThemeCommand = ( ): UseThemeCommandReturn => { const [isThemeDialogOpen, setIsThemeDialogOpen] = useState(!!initialThemeError); + const [themeBeforeDialogOpen, setThemeBeforeDialogOpen] = useState< + string | undefined + >(themeManager.getActiveTheme().name); const openThemeDialog = useCallback(() => { if (process.env['NO_COLOR']) { @@ -43,6 +46,9 @@ export const useThemeCommand = ( ); return; } + // The theme may temporarily change while navigating the list; keep the + // original value to restore it if user cancels with Esc/Ctrl+C. + setThemeBeforeDialogOpen(themeManager.getActiveTheme().name); setIsThemeDialogOpen(true); }, [addItem]); @@ -72,6 +78,14 @@ export const useThemeCommand = ( const handleThemeSelect = useCallback( (themeName: string | undefined, scope: SettingScope) => { + // Undefined means "cancel": close dialog and restore original theme. + if (themeName === undefined) { + applyTheme(themeBeforeDialogOpen); + setThemeError(null); + setIsThemeDialogOpen(false); + return; + } + try { // Merge user and workspace custom themes (workspace takes precedence) const mergedCustomThemes = { @@ -100,7 +114,7 @@ export const useThemeCommand = ( setIsThemeDialogOpen(false); // Close the dialog } }, - [applyTheme, loadedSettings, setThemeError], + [applyTheme, loadedSettings, setThemeError, themeBeforeDialogOpen], ); return {