mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-13 23:51:16 -07:00
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.
This commit is contained in:
@@ -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 && (
|
||||
<Box
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
@@ -204,7 +207,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
flexDirection="column"
|
||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||
>
|
||||
{!showLoadingIndicator && (
|
||||
{!isActivelyStreaming && (
|
||||
<StatusDisplay hideContextSummary={hideContextSummary} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -24,8 +24,8 @@ export const ContextUsageDisplay = ({
|
||||
|
||||
return (
|
||||
<Text color={theme.text.secondary}>
|
||||
({percentageLeft}
|
||||
{label})
|
||||
{percentageLeft}
|
||||
{label}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 && (
|
||||
<Text color={theme.text.secondary}>[{displayVimMode}] </Text>
|
||||
)}
|
||||
{!hideCWD &&
|
||||
(nightly ? (
|
||||
<ThemedGradient>
|
||||
{displayPath}
|
||||
{branchName && <Text> ({branchName}*)</Text>}
|
||||
</ThemedGradient>
|
||||
) : (
|
||||
<Text color={theme.text.link}>
|
||||
{displayPath}
|
||||
{branchName && (
|
||||
<Text color={theme.text.secondary}> ({branchName}*)</Text>
|
||||
)}
|
||||
</Text>
|
||||
))}
|
||||
{!hideCWD && (
|
||||
<Text color={theme.text.primary}>
|
||||
{displayPath}
|
||||
{branchName && (
|
||||
<Text color={theme.text.secondary}> ({branchName}*)</Text>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{debugMode && (
|
||||
<Text color={theme.status.error}>
|
||||
{' ' + (debugMessage || '--debug')}
|
||||
@@ -146,9 +137,9 @@ export const Footer: React.FC = () => {
|
||||
{!hideModelInfo && (
|
||||
<Box alignItems="center" justifyContent="flex-end">
|
||||
<Box alignItems="center">
|
||||
<Text color={theme.text.accent}>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text color={theme.text.secondary}>/model </Text>
|
||||
{getDisplayString(model)}
|
||||
<Text color={theme.text.secondary}> /model</Text>
|
||||
{!hideContextPercentage && (
|
||||
<>
|
||||
{' '}
|
||||
|
||||
@@ -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 ? (
|
||||
<Text>{SCREEN_READER_LOADING}</Text>
|
||||
) : (
|
||||
<Text color={theme.text.primary}>{nonRespondingDisplay}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -57,10 +65,39 @@ export const GeminiSpinner: React.FC<GeminiSpinnerProps> = ({
|
||||
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 ? (
|
||||
<Text>{altText}</Text>
|
||||
) : (
|
||||
<Text color={theme.text.primary}>
|
||||
<Text color={currentColor}>
|
||||
<CliSpinner type={spinnerType} />
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -37,41 +37,43 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
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 (
|
||||
<Box>
|
||||
<Box marginRight={1}>
|
||||
<GeminiRespondingSpinner
|
||||
nonRespondingDisplay={
|
||||
streamingState === StreamingState.WaitingForConfirmation
|
||||
? '⠏'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{streamingState !== StreamingState.Idle && (
|
||||
<Box marginRight={1}>
|
||||
<GeminiRespondingSpinner
|
||||
nonRespondingDisplay={
|
||||
streamingState === StreamingState.WaitingForConfirmation
|
||||
? '⠏'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{primaryText && (
|
||||
<Text color={theme.text.accent} wrap="truncate-end">
|
||||
<Text color={textColor} italic={italic} wrap="truncate-end">
|
||||
{primaryText}
|
||||
</Text>
|
||||
)}
|
||||
@@ -94,17 +96,19 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
>
|
||||
<Box>
|
||||
<Box marginRight={1}>
|
||||
<GeminiRespondingSpinner
|
||||
nonRespondingDisplay={
|
||||
streamingState === StreamingState.WaitingForConfirmation
|
||||
? '⠏'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{streamingState !== StreamingState.Idle && (
|
||||
<Box marginRight={1}>
|
||||
<GeminiRespondingSpinner
|
||||
nonRespondingDisplay={
|
||||
streamingState === StreamingState.WaitingForConfirmation
|
||||
? '⠏'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{primaryText && (
|
||||
<Text color={theme.text.accent} wrap="truncate-end">
|
||||
<Text color={textColor} italic={italic} wrap="truncate-end">
|
||||
{primaryText}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -110,7 +110,7 @@ export interface UIState {
|
||||
showEscapePrompt: boolean;
|
||||
shortcutsHelpVisible: boolean;
|
||||
elapsedTime: number;
|
||||
currentLoadingPhrase: string;
|
||||
currentLoadingPhrase: string | undefined;
|
||||
historyRemountKey: number;
|
||||
activeHooks: ActiveHook[];
|
||||
messageQueue: string[];
|
||||
|
||||
@@ -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<NodeJS.Timeout | null>(null);
|
||||
const hasShownFirstRequestTipRef = useRef(false);
|
||||
@@ -56,7 +56,7 @@ export const usePhraseCycler = (
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
setCurrentLoadingPhrase(loadingPhrases[0]);
|
||||
setCurrentLoadingPhrase(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user