From 08067acc71da9b6738569249c5adfc7538bf3c58 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Wed, 3 Dec 2025 09:08:32 -0800 Subject: [PATCH] Avoid triggering refreshStatic unless there really is a banner to display. (#14328) --- packages/cli/src/ui/AppContainer.test.tsx | 32 ++++++++++++++++++++++ packages/cli/src/ui/AppContainer.tsx | 33 ++++++++++++++++++----- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index df6e09ebd8..e9684434ba 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -73,6 +73,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { disableMouseEvents: vi.fn(), }; }); +import ansiEscapes from 'ansi-escapes'; import type { LoadedSettings } from '../config/settings.js'; import type { InitializationResult } from '../core/initializer.js'; import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js'; @@ -1915,4 +1916,35 @@ describe('AppContainer State Management', () => { unmount(); }); }); + + describe('Regression Tests', () => { + it('does not refresh static on startup if banner text is empty', async () => { + // Mock banner text to be empty strings + vi.spyOn(mockConfig, 'getBannerTextNoCapacityIssues').mockResolvedValue( + '', + ); + vi.spyOn(mockConfig, 'getBannerTextCapacityIssues').mockResolvedValue(''); + + // Clear previous calls + mocks.mockStdout.write.mockClear(); + + const { unmount } = renderAppContainer(); + + // Allow async effects to run + await waitFor(() => expect(capturedUIState).toBeTruthy()); + + // Wait for fetchBannerTexts to complete + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + // Check that clearTerminal was NOT written to stdout + const clearTerminalCalls = mocks.mockStdout.write.mock.calls.filter( + (call: unknown[]) => call[0] === ansiEscapes.clearTerminal, + ); + + expect(clearTerminalCalls).toHaveLength(0); + unmount(); + }); + }); }); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 3e4cb4f123..56c198dd48 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -125,6 +125,7 @@ import { useSettings } from './contexts/SettingsContext.js'; import { enableSupportedProtocol } from './utils/kittyProtocolDetector.js'; import { useInputHistoryStore } from './hooks/useInputHistoryStore.js'; import { enableBracketedPaste } from './utils/bracketedPaste.js'; +import { useBanner } from './hooks/useBanner.js'; const WARNING_PROMPT_DURATION_MS = 1000; const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000; @@ -203,6 +204,16 @@ export const AppContainer = (props: AppContainerProps) => { const [warningBannerText, setWarningBannerText] = useState(''); const [bannerVisible, setBannerVisible] = useState(true); + const bannerData = useMemo( + () => ({ + defaultText: defaultBannerText, + warningText: warningBannerText, + }), + [defaultBannerText, warningBannerText], + ); + + const { bannerText } = useBanner(bannerData, config); + const extensionManager = config.getExtensionLoader() as ExtensionManager; // We are in the interactive CLI, update how we request consent and settings. extensionManager.setRequestConsent((description) => @@ -380,6 +391,7 @@ export const AppContainer = (props: AppContainerProps) => { } setHistoryRemountKey((prev) => prev + 1); }, [setHistoryRemountKey, isAlternateBuffer, stdout]); + const handleEditorClose = useCallback(() => { if ( shouldEnterAlternateScreen(isAlternateBuffer, config.getScreenReader()) @@ -403,6 +415,18 @@ export const AppContainer = (props: AppContainerProps) => { }; }, [handleEditorClose]); + useEffect(() => { + if ( + !(settings.merged.ui?.hideBanner || config.getScreenReader()) && + bannerVisible && + bannerText + ) { + // The header should show a banner but the Header is rendered in static + // so we must trigger a static refresh for it to be visible. + refreshStatic(); + } + }, [bannerVisible, bannerText, settings, config, refreshStatic]); + const { isThemeDialogOpen, openThemeDialog, @@ -1388,7 +1412,6 @@ Logging in with Google... Restarting Gemini CLI to continue. setDefaultBannerText(defaultBanner); setWarningBannerText(warningBanner); setBannerVisible(true); - refreshStatic(); const authType = config.getContentGeneratorConfig()?.authType; if ( authType === AuthType.USE_GEMINI || @@ -1497,10 +1520,7 @@ Logging in with Google... Restarting Gemini CLI to continue. customDialog, copyModeEnabled, warningMessage, - bannerData: { - defaultText: defaultBannerText, - warningText: warningBannerText, - }, + bannerData, bannerVisible, }), [ @@ -1591,8 +1611,7 @@ Logging in with Google... Restarting Gemini CLI to continue. authState, copyModeEnabled, warningMessage, - defaultBannerText, - warningBannerText, + bannerData, bannerVisible, ], );