diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index ee074c1c77..3e443e2e6b 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 25dad9c7e3..09cd4c3922 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 64ee355f56..6b50a57909 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 8565ae5d3d..da2fef686a 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 18e71b7a4b..4425bc3a5c 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 45111a29cc..063b9aad01 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 4c6e9e706d..ffc469f02a 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; }