From 397e52dac7a9c0f49ea454059675edf9b71adc7e Mon Sep 17 00:00:00 2001 From: Pyush Sinha Date: Mon, 20 Oct 2025 10:50:09 -0700 Subject: [PATCH] fix(ui): escaping theme dialog no longer resets theme to default (#11323) Co-authored-by: Jacob Richman --- packages/cli/src/ui/AppContainer.tsx | 3 +++ .../cli/src/ui/components/DialogManager.tsx | 1 + .../src/ui/components/ThemeDialog.test.tsx | 26 +++++++++++++++++++ .../cli/src/ui/components/ThemeDialog.tsx | 14 ++++++---- .../cli/src/ui/contexts/UIActionsContext.tsx | 6 ++--- packages/cli/src/ui/hooks/useThemeCommand.ts | 15 +++++++---- 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index caa0178347..03210c29d2 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -335,6 +335,7 @@ export const AppContainer = (props: AppContainerProps) => { const { isThemeDialogOpen, openThemeDialog, + closeThemeDialog, handleThemeSelect, handleThemeHighlight, } = useThemeCommand( @@ -1266,6 +1267,7 @@ Logging in with Google... Please restart Gemini CLI to continue. const uiActions: UIActions = useMemo( () => ({ handleThemeSelect, + closeThemeDialog, handleThemeHighlight, handleAuthSelect, setAuthState, @@ -1291,6 +1293,7 @@ Logging in with Google... Please restart Gemini CLI to continue. }), [ handleThemeSelect, + closeThemeDialog, handleThemeHighlight, handleAuthSelect, setAuthState, diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index 762de65794..0e5fc47eba 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -115,6 +115,7 @@ export const DialogManager = ({ )} { const baseProps = { onSelect: vi.fn(), + onCancel: vi.fn(), onHighlight: vi.fn(), availableTerminalHeight: 40, terminalWidth: 120, @@ -105,4 +107,28 @@ describe('ThemeDialog Snapshots', () => { expect(lastFrame()).toMatchSnapshot(); }); + + it('should call onCancel when ESC is pressed', async () => { + const mockOnCancel = vi.fn(); + const settings = createMockSettings(); + const { stdin } = render( + + + + + , + ); + + act(() => { + stdin.write('\x1b'); + }); + + await waitFor(() => { + expect(mockOnCancel).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx index 04ea2ad588..f6f35ed8f5 100644 --- a/packages/cli/src/ui/components/ThemeDialog.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.tsx @@ -20,7 +20,10 @@ import { ScopeSelector } from './shared/ScopeSelector.js'; interface ThemeDialogProps { /** Callback function when a theme is selected */ - onSelect: (themeName: string | undefined, scope: SettingScope) => void; + onSelect: (themeName: string, scope: SettingScope) => void; + + /** Callback function when the dialog is cancelled */ + onCancel: () => void; /** Callback function when a theme is highlighted */ onHighlight: (themeName: string | undefined) => void; @@ -32,6 +35,7 @@ interface ThemeDialogProps { export function ThemeDialog({ onSelect, + onCancel, onHighlight, settings, availableTerminalHeight, @@ -42,9 +46,9 @@ export function ThemeDialog({ ); // Track the currently highlighted theme name - const [highlightedThemeName, setHighlightedThemeName] = useState< - string | undefined - >(settings.merged.ui?.theme || DEFAULT_THEME.name); + const [highlightedThemeName, setHighlightedThemeName] = useState( + settings.merged.ui?.theme || DEFAULT_THEME.name, + ); // Generate theme items filtered by selected scope const customThemes = @@ -112,7 +116,7 @@ export function ThemeDialog({ setMode((prev) => (prev === 'theme' ? 'scope' : 'theme')); } if (key.name === 'escape') { - onSelect(undefined, selectedScope); + onCancel(); } }, { isActive: true }, diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index aa79365033..e055147c32 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -13,10 +13,8 @@ import { type SettingScope } from '../../config/settings.js'; import type { AuthState } from '../types.js'; export interface UIActions { - handleThemeSelect: ( - themeName: string | undefined, - scope: SettingScope, - ) => void; + handleThemeSelect: (themeName: string, scope: SettingScope) => void; + closeThemeDialog: () => void; handleThemeHighlight: (themeName: string | undefined) => void; handleAuthSelect: ( authType: AuthType | undefined, diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts index 9c534538c9..46cf0e5d85 100644 --- a/packages/cli/src/ui/hooks/useThemeCommand.ts +++ b/packages/cli/src/ui/hooks/useThemeCommand.ts @@ -13,10 +13,8 @@ import process from 'node:process'; interface UseThemeCommandReturn { isThemeDialogOpen: boolean; openThemeDialog: () => void; - handleThemeSelect: ( - themeName: string | undefined, - scope: SettingScope, - ) => void; // Added scope + closeThemeDialog: () => void; + handleThemeSelect: (themeName: string, scope: SettingScope) => void; handleThemeHighlight: (themeName: string | undefined) => void; } @@ -63,8 +61,14 @@ export const useThemeCommand = ( [applyTheme], ); + const closeThemeDialog = useCallback(() => { + // Re-apply the saved theme to revert any preview changes from highlighting + applyTheme(loadedSettings.merged.ui?.theme); + setIsThemeDialogOpen(false); + }, [applyTheme, loadedSettings]); + const handleThemeSelect = useCallback( - (themeName: string | undefined, scope: SettingScope) => { + (themeName: string, scope: SettingScope) => { try { // Merge user and workspace custom themes (workspace takes precedence) const mergedCustomThemes = { @@ -95,6 +99,7 @@ export const useThemeCommand = ( return { isThemeDialogOpen, openThemeDialog, + closeThemeDialog, handleThemeSelect, handleThemeHighlight, };