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;
}