From 2df1f31af287a59f68b2fdc4e41ecbb351aa9f6a Mon Sep 17 00:00:00 2001 From: Keith Guerin Date: Sat, 7 Feb 2026 23:04:45 -0800 Subject: [PATCH 1/8] ui: update & subdue footer colors and animate progress indicator Update path, model, and thinking indicators to use primary theme colors instead of accents/gradients. Animate the progress indicator using interpolated Google brand colors, starting with purple for a calmer initial state. --- packages/cli/src/ui/components/Composer.tsx | 11 ++-- .../src/ui/components/ContextUsageDisplay.tsx | 4 +- packages/cli/src/ui/components/Footer.tsx | 29 +++------ .../ui/components/GeminiRespondingSpinner.tsx | 41 +++++++++++- .../src/ui/components/LoadingIndicator.tsx | 64 ++++++++++--------- .../cli/src/ui/contexts/UIStateContext.tsx | 2 +- packages/cli/src/ui/hooks/usePhraseCycler.ts | 8 +-- 7 files changed, 97 insertions(+), 62 deletions(-) diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index ee074c1c77c..3e443e2e6b3 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -63,14 +63,17 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { Boolean(uiState.proQuotaRequest) || Boolean(uiState.validationRequest) || Boolean(uiState.customDialog); + const isActivelyStreaming = + uiState.streamingState === StreamingState.Responding; const showLoadingIndicator = (!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) && - uiState.streamingState === StreamingState.Responding && !hasPendingActionRequired; const showApprovalIndicator = !uiState.shellModeActive; const showRawMarkdownIndicator = !uiState.renderMarkdown; const showEscToCancelHint = - showLoadingIndicator && + isActivelyStreaming && + !uiState.embeddedShellFocused && + !hasPendingActionRequired && uiState.streamingState !== StreamingState.WaitingForConfirmation; return ( @@ -158,7 +161,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { alignItems="center" flexGrow={1} > - {!showLoadingIndicator && ( + {!isActivelyStreaming && ( { flexDirection="column" alignItems={isNarrow ? 'flex-start' : 'flex-end'} > - {!showLoadingIndicator && ( + {!isActivelyStreaming && ( )} diff --git a/packages/cli/src/ui/components/ContextUsageDisplay.tsx b/packages/cli/src/ui/components/ContextUsageDisplay.tsx index 25dad9c7e3f..09cd4c3922f 100644 --- a/packages/cli/src/ui/components/ContextUsageDisplay.tsx +++ b/packages/cli/src/ui/components/ContextUsageDisplay.tsx @@ -24,8 +24,8 @@ export const ContextUsageDisplay = ({ return ( - ({percentageLeft} - {label}) + {percentageLeft} + {label} ); }; diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index 64ee355f56d..6b50a579095 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -14,7 +14,6 @@ import { } from '@google/gemini-cli-core'; import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js'; import process from 'node:process'; -import { ThemedGradient } from './ThemedGradient.js'; import { MemoryUsageDisplay } from './MemoryUsageDisplay.js'; import { ContextUsageDisplay } from './ContextUsageDisplay.js'; import { DebugProfiler } from './DebugProfiler.js'; @@ -40,7 +39,6 @@ export const Footer: React.FC = () => { errorCount, showErrorDetails, promptTokenCount, - nightly, isTrustedFolder, terminalWidth, } = { @@ -53,7 +51,6 @@ export const Footer: React.FC = () => { errorCount: uiState.errorCount, showErrorDetails: uiState.showErrorDetails, promptTokenCount: uiState.sessionStats.lastPromptTokenCount, - nightly: uiState.nightly, isTrustedFolder: uiState.isTrustedFolder, terminalWidth: uiState.terminalWidth, }; @@ -87,20 +84,14 @@ export const Footer: React.FC = () => { {displayVimMode && ( [{displayVimMode}] )} - {!hideCWD && - (nightly ? ( - - {displayPath} - {branchName && ({branchName}*)} - - ) : ( - - {displayPath} - {branchName && ( - ({branchName}*) - )} - - ))} + {!hideCWD && ( + + {displayPath} + {branchName && ( + ({branchName}*) + )} + + )} {debugMode && ( {' ' + (debugMessage || '--debug')} @@ -146,9 +137,9 @@ export const Footer: React.FC = () => { {!hideModelInfo && ( - + + /model {getDisplayString(model)} - /model {!hideContextPercentage && ( <> {' '} diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx index 8565ae5d3d4..da2fef686aa 100644 --- a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx +++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx @@ -5,6 +5,7 @@ */ import type React from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Text, useIsScreenReaderEnabled } from 'ink'; import { CliSpinner } from './CliSpinner.js'; import type { SpinnerName } from 'cli-spinners'; @@ -15,6 +16,10 @@ import { SCREEN_READER_RESPONDING, } from '../textConstants.js'; import { theme } from '../semantic-colors.js'; +import { Colors } from '../colors.js'; +import tinygradient from 'tinygradient'; + +const COLOR_CYCLE_DURATION_MS = 4000; interface GeminiRespondingSpinnerProps { /** @@ -37,13 +42,16 @@ export const GeminiRespondingSpinner: React.FC< altText={SCREEN_READER_RESPONDING} /> ); - } else if (nonRespondingDisplay) { + } + + if (nonRespondingDisplay) { return isScreenReaderEnabled ? ( {SCREEN_READER_LOADING} ) : ( {nonRespondingDisplay} ); } + return null; }; @@ -57,10 +65,39 @@ export const GeminiSpinner: React.FC = ({ altText, }) => { const isScreenReaderEnabled = useIsScreenReaderEnabled(); + const [time, setTime] = useState(0); + + const googleGradient = useMemo(() => { + const brandColors = [ + Colors.AccentPurple, + Colors.AccentBlue, + Colors.AccentCyan, + Colors.AccentGreen, + Colors.AccentYellow, + Colors.AccentRed, + ]; + return tinygradient([...brandColors, brandColors[0]]); + }, []); + + useEffect(() => { + if (isScreenReaderEnabled) { + return; + } + + const interval = setInterval(() => { + setTime((prevTime) => prevTime + 30); + }, 30); // ~33fps for smooth color transitions + + return () => clearInterval(interval); + }, [isScreenReaderEnabled]); + + const progress = (time % COLOR_CYCLE_DURATION_MS) / COLOR_CYCLE_DURATION_MS; + const currentColor = googleGradient.rgbAt(progress).toHexString(); + return isScreenReaderEnabled ? ( {altText} ) : ( - + ); diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx index 18e71b7a4ba..4425bc3a5c7 100644 --- a/packages/cli/src/ui/components/LoadingIndicator.tsx +++ b/packages/cli/src/ui/components/LoadingIndicator.tsx @@ -37,41 +37,43 @@ export const LoadingIndicator: React.FC = ({ const { columns: terminalWidth } = useTerminalSize(); const isNarrow = isNarrowWidth(terminalWidth); - if ( - streamingState === StreamingState.Idle && - !currentLoadingPhrase && - !thought - ) { - return null; - } - // Prioritize the interactive shell waiting phrase over the thought subject // because it conveys an actionable state for the user (waiting for input). const primaryText = currentLoadingPhrase === INTERACTIVE_SHELL_WAITING_PHRASE ? currentLoadingPhrase - : thought?.subject || currentLoadingPhrase; + : thought?.subject || currentLoadingPhrase || undefined; + + const textColor = + streamingState === StreamingState.Idle + ? theme.text.secondary + : theme.text.primary; + + const italic = streamingState === StreamingState.Responding; const cancelAndTimerContent = showCancelAndTimer && - streamingState !== StreamingState.WaitingForConfirmation + streamingState !== StreamingState.WaitingForConfirmation && + streamingState !== StreamingState.Idle ? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})` : null; if (inline) { return ( - - - + {streamingState !== StreamingState.Idle && ( + + + + )} {primaryText && ( - + {primaryText} )} @@ -94,17 +96,19 @@ export const LoadingIndicator: React.FC = ({ alignItems={isNarrow ? 'flex-start' : 'center'} > - - - + {streamingState !== StreamingState.Idle && ( + + + + )} {primaryText && ( - + {primaryText} )} diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 45111a29cce..063b9aad01b 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -110,7 +110,7 @@ export interface UIState { showEscapePrompt: boolean; shortcutsHelpVisible: boolean; elapsedTime: number; - currentLoadingPhrase: string; + currentLoadingPhrase: string | undefined; historyRemountKey: number; activeHooks: ActiveHook[]; messageQueue: string[]; diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.ts b/packages/cli/src/ui/hooks/usePhraseCycler.ts index 4c6e9e706dc..ffc469f02a7 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.ts +++ b/packages/cli/src/ui/hooks/usePhraseCycler.ts @@ -31,9 +31,9 @@ export const usePhraseCycler = ( ? customPhrases : WITTY_LOADING_PHRASES; - const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState( - loadingPhrases[0], - ); + const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState< + string | undefined + >(isActive ? loadingPhrases[0] : undefined); const phraseIntervalRef = useRef(null); const hasShownFirstRequestTipRef = useRef(false); @@ -56,7 +56,7 @@ export const usePhraseCycler = ( } if (!isActive) { - setCurrentLoadingPhrase(loadingPhrases[0]); + setCurrentLoadingPhrase(undefined); return; } From 1620128d74c57b65464d1fd647d99930ad792f7f Mon Sep 17 00:00:00 2001 From: Keith Guerin Date: Sat, 7 Feb 2026 23:17:53 -0800 Subject: [PATCH 2/8] ui: increase user background contrast to 16% --- packages/cli/src/ui/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/ui/constants.ts b/packages/cli/src/ui/constants.ts index 496217fe9e4..c0aed1c8fdd 100644 --- a/packages/cli/src/ui/constants.ts +++ b/packages/cli/src/ui/constants.ts @@ -34,7 +34,7 @@ export const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000; export const SHELL_ACTION_REQUIRED_TITLE_DELAY_MS = 30000; export const SHELL_SILENT_WORKING_TITLE_DELAY_MS = 120000; -export const DEFAULT_BACKGROUND_OPACITY = 0.08; +export const DEFAULT_BACKGROUND_OPACITY = 0.16; export const KEYBOARD_SHORTCUTS_URL = 'https://geminicli.com/docs/cli/keyboard-shortcuts/'; From 16bc4c5302f9d6a887f5091d54baf8e1137c9fce Mon Sep 17 00:00:00 2001 From: Keith Guerin Date: Sat, 7 Feb 2026 23:43:16 -0800 Subject: [PATCH 3/8] ui: refine user background contrast, borders, and thinking behavior --- packages/cli/src/ui/components/Composer.tsx | 2 ++ packages/cli/src/ui/components/InputPrompt.tsx | 15 +++++++++------ .../src/ui/components/messages/UserMessage.tsx | 2 +- .../ui/components/messages/UserShellMessage.tsx | 2 +- packages/cli/src/ui/constants.ts | 2 ++ packages/cli/src/ui/themes/theme.ts | 15 +++++++++++++-- 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 3e443e2e6b3..327da3c461f 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -67,6 +67,8 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { uiState.streamingState === StreamingState.Responding; const showLoadingIndicator = (!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) && + (uiState.streamingState === StreamingState.Responding || + uiState.streamingState === StreamingState.WaitingForConfirmation) && !hasPendingActionRequired; const showApprovalIndicator = !uiState.shellModeActive; const showRawMarkdownIndicator = !uiState.renderMarkdown; diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 49c609ec9b9..e25a1230f77 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -54,7 +54,10 @@ import { } from '../utils/commandUtils.js'; import * as path from 'node:path'; import { SCREEN_READER_USER_PREFIX } from '../textConstants.js'; -import { DEFAULT_BACKGROUND_OPACITY } from '../constants.js'; +import { + DEFAULT_BACKGROUND_OPACITY, + DEFAULT_INPUT_BACKGROUND_OPACITY, +} from '../constants.js'; import { getSafeLowColorBackground } from '../themes/color-utils.js'; import { isLowColorDepth } from '../utils/terminalUtils.js'; import { useShellFocusState } from '../contexts/ShellFocusContext.js'; @@ -1330,12 +1333,12 @@ export const InputPrompt: React.FC = ({ /> ) : null} = ({ text, width }) => { return ( diff --git a/packages/cli/src/ui/components/messages/UserShellMessage.tsx b/packages/cli/src/ui/components/messages/UserShellMessage.tsx index ca86e29b8c0..390977f2fef 100644 --- a/packages/cli/src/ui/components/messages/UserShellMessage.tsx +++ b/packages/cli/src/ui/components/messages/UserShellMessage.tsx @@ -28,7 +28,7 @@ export const UserShellMessage: React.FC = ({ return ( diff --git a/packages/cli/src/ui/constants.ts b/packages/cli/src/ui/constants.ts index c0aed1c8fdd..c08741177cd 100644 --- a/packages/cli/src/ui/constants.ts +++ b/packages/cli/src/ui/constants.ts @@ -35,6 +35,8 @@ export const SHELL_ACTION_REQUIRED_TITLE_DELAY_MS = 30000; export const SHELL_SILENT_WORKING_TITLE_DELAY_MS = 120000; export const DEFAULT_BACKGROUND_OPACITY = 0.16; +export const DEFAULT_INPUT_BACKGROUND_OPACITY = 0.24; +export const DEFAULT_BORDER_OPACITY = 0.2; export const KEYBOARD_SHORTCUTS_URL = 'https://geminicli.com/docs/cli/keyboard-shortcuts/'; diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts index e95799b8792..2e39b1b6c7f 100644 --- a/packages/cli/src/ui/themes/theme.ts +++ b/packages/cli/src/ui/themes/theme.ts @@ -15,6 +15,7 @@ import { } from './color-utils.js'; import type { CustomTheme } from '@google/gemini-cli-core'; +import { DEFAULT_BORDER_OPACITY } from '../constants.js'; export type { CustomTheme }; @@ -136,7 +137,11 @@ export class Theme { }, }, border: { - default: this.colors.Gray, + default: interpolateColor( + this.colors.Background, + this.colors.Gray, + DEFAULT_BORDER_OPACITY, + ), focused: this.colors.AccentBlue, }, ui: { @@ -401,7 +406,13 @@ export function createCustomTheme(customTheme: CustomTheme): Theme { }, }, border: { - default: customTheme.border?.default ?? colors.Gray, + default: + customTheme.border?.default ?? + interpolateColor( + colors.Background, + colors.Gray, + DEFAULT_BORDER_OPACITY, + ), focused: customTheme.border?.focused ?? colors.AccentBlue, }, ui: { From 4a0963373c855b37e6c6d65bba0a8f84e9467df2 Mon Sep 17 00:00:00 2001 From: Keith Guerin Date: Sat, 7 Feb 2026 23:43:22 -0800 Subject: [PATCH 4/8] test: update snapshots and unit tests for UI refinements --- packages/cli/src/ui/components/Composer.test.tsx | 6 +++--- packages/cli/src/ui/components/Footer.test.tsx | 10 ++++------ .../cli/src/ui/components/LoadingIndicator.test.tsx | 8 ++++---- .../ui/components/__snapshots__/Footer.test.tsx.snap | 4 ++-- packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx | 8 ++------ packages/cli/src/ui/hooks/usePhraseCycler.test.tsx | 10 +++++----- 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index 73765dcf045..956130fffed 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -308,19 +308,19 @@ describe('Composer', () => { expect(output).not.toContain('Should not show'); }); - it('does not render LoadingIndicator when waiting for confirmation', () => { + it('renders LoadingIndicator when waiting for confirmation', () => { const uiState = createMockUIState({ streamingState: StreamingState.WaitingForConfirmation, thought: { subject: 'Confirmation', - description: 'Should not show during confirmation', + description: 'Should show during confirmation', }, }); const { lastFrame } = renderComposer(uiState); const output = lastFrame(); - expect(output).not.toContain('LoadingIndicator'); + expect(output).toContain('LoadingIndicator'); }); it('does not render LoadingIndicator when a tool confirmation is pending', () => { diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index 4113060081a..cd28462bcde 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -128,7 +128,7 @@ describe('