mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 22:14:52 -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.proQuotaRequest) ||
|
||||||
Boolean(uiState.validationRequest) ||
|
Boolean(uiState.validationRequest) ||
|
||||||
Boolean(uiState.customDialog);
|
Boolean(uiState.customDialog);
|
||||||
|
const isActivelyStreaming =
|
||||||
|
uiState.streamingState === StreamingState.Responding;
|
||||||
const showLoadingIndicator =
|
const showLoadingIndicator =
|
||||||
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
|
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
|
||||||
uiState.streamingState === StreamingState.Responding &&
|
|
||||||
!hasPendingActionRequired;
|
!hasPendingActionRequired;
|
||||||
const showApprovalIndicator = !uiState.shellModeActive;
|
const showApprovalIndicator = !uiState.shellModeActive;
|
||||||
const showRawMarkdownIndicator = !uiState.renderMarkdown;
|
const showRawMarkdownIndicator = !uiState.renderMarkdown;
|
||||||
const showEscToCancelHint =
|
const showEscToCancelHint =
|
||||||
showLoadingIndicator &&
|
isActivelyStreaming &&
|
||||||
|
!uiState.embeddedShellFocused &&
|
||||||
|
!hasPendingActionRequired &&
|
||||||
uiState.streamingState !== StreamingState.WaitingForConfirmation;
|
uiState.streamingState !== StreamingState.WaitingForConfirmation;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -158,7 +161,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
>
|
>
|
||||||
{!showLoadingIndicator && (
|
{!isActivelyStreaming && (
|
||||||
<Box
|
<Box
|
||||||
flexDirection={isNarrow ? 'column' : 'row'}
|
flexDirection={isNarrow ? 'column' : 'row'}
|
||||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||||
@@ -204,7 +207,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||||
>
|
>
|
||||||
{!showLoadingIndicator && (
|
{!isActivelyStreaming && (
|
||||||
<StatusDisplay hideContextSummary={hideContextSummary} />
|
<StatusDisplay hideContextSummary={hideContextSummary} />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export const ContextUsageDisplay = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Text color={theme.text.secondary}>
|
<Text color={theme.text.secondary}>
|
||||||
({percentageLeft}
|
{percentageLeft}
|
||||||
{label})
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { ThemedGradient } from './ThemedGradient.js';
|
|
||||||
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
||||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||||
import { DebugProfiler } from './DebugProfiler.js';
|
import { DebugProfiler } from './DebugProfiler.js';
|
||||||
@@ -40,7 +39,6 @@ export const Footer: React.FC = () => {
|
|||||||
errorCount,
|
errorCount,
|
||||||
showErrorDetails,
|
showErrorDetails,
|
||||||
promptTokenCount,
|
promptTokenCount,
|
||||||
nightly,
|
|
||||||
isTrustedFolder,
|
isTrustedFolder,
|
||||||
terminalWidth,
|
terminalWidth,
|
||||||
} = {
|
} = {
|
||||||
@@ -53,7 +51,6 @@ export const Footer: React.FC = () => {
|
|||||||
errorCount: uiState.errorCount,
|
errorCount: uiState.errorCount,
|
||||||
showErrorDetails: uiState.showErrorDetails,
|
showErrorDetails: uiState.showErrorDetails,
|
||||||
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
||||||
nightly: uiState.nightly,
|
|
||||||
isTrustedFolder: uiState.isTrustedFolder,
|
isTrustedFolder: uiState.isTrustedFolder,
|
||||||
terminalWidth: uiState.terminalWidth,
|
terminalWidth: uiState.terminalWidth,
|
||||||
};
|
};
|
||||||
@@ -87,20 +84,14 @@ export const Footer: React.FC = () => {
|
|||||||
{displayVimMode && (
|
{displayVimMode && (
|
||||||
<Text color={theme.text.secondary}>[{displayVimMode}] </Text>
|
<Text color={theme.text.secondary}>[{displayVimMode}] </Text>
|
||||||
)}
|
)}
|
||||||
{!hideCWD &&
|
{!hideCWD && (
|
||||||
(nightly ? (
|
<Text color={theme.text.primary}>
|
||||||
<ThemedGradient>
|
{displayPath}
|
||||||
{displayPath}
|
{branchName && (
|
||||||
{branchName && <Text> ({branchName}*)</Text>}
|
<Text color={theme.text.secondary}> ({branchName}*)</Text>
|
||||||
</ThemedGradient>
|
)}
|
||||||
) : (
|
</Text>
|
||||||
<Text color={theme.text.link}>
|
)}
|
||||||
{displayPath}
|
|
||||||
{branchName && (
|
|
||||||
<Text color={theme.text.secondary}> ({branchName}*)</Text>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
{debugMode && (
|
{debugMode && (
|
||||||
<Text color={theme.status.error}>
|
<Text color={theme.status.error}>
|
||||||
{' ' + (debugMessage || '--debug')}
|
{' ' + (debugMessage || '--debug')}
|
||||||
@@ -146,9 +137,9 @@ export const Footer: React.FC = () => {
|
|||||||
{!hideModelInfo && (
|
{!hideModelInfo && (
|
||||||
<Box alignItems="center" justifyContent="flex-end">
|
<Box alignItems="center" justifyContent="flex-end">
|
||||||
<Box alignItems="center">
|
<Box alignItems="center">
|
||||||
<Text color={theme.text.accent}>
|
<Text color={theme.text.primary}>
|
||||||
|
<Text color={theme.text.secondary}>/model </Text>
|
||||||
{getDisplayString(model)}
|
{getDisplayString(model)}
|
||||||
<Text color={theme.text.secondary}> /model</Text>
|
|
||||||
{!hideContextPercentage && (
|
{!hideContextPercentage && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { Text, useIsScreenReaderEnabled } from 'ink';
|
import { Text, useIsScreenReaderEnabled } from 'ink';
|
||||||
import { CliSpinner } from './CliSpinner.js';
|
import { CliSpinner } from './CliSpinner.js';
|
||||||
import type { SpinnerName } from 'cli-spinners';
|
import type { SpinnerName } from 'cli-spinners';
|
||||||
@@ -15,6 +16,10 @@ import {
|
|||||||
SCREEN_READER_RESPONDING,
|
SCREEN_READER_RESPONDING,
|
||||||
} from '../textConstants.js';
|
} from '../textConstants.js';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
|
import { Colors } from '../colors.js';
|
||||||
|
import tinygradient from 'tinygradient';
|
||||||
|
|
||||||
|
const COLOR_CYCLE_DURATION_MS = 4000;
|
||||||
|
|
||||||
interface GeminiRespondingSpinnerProps {
|
interface GeminiRespondingSpinnerProps {
|
||||||
/**
|
/**
|
||||||
@@ -37,13 +42,16 @@ export const GeminiRespondingSpinner: React.FC<
|
|||||||
altText={SCREEN_READER_RESPONDING}
|
altText={SCREEN_READER_RESPONDING}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (nonRespondingDisplay) {
|
}
|
||||||
|
|
||||||
|
if (nonRespondingDisplay) {
|
||||||
return isScreenReaderEnabled ? (
|
return isScreenReaderEnabled ? (
|
||||||
<Text>{SCREEN_READER_LOADING}</Text>
|
<Text>{SCREEN_READER_LOADING}</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text color={theme.text.primary}>{nonRespondingDisplay}</Text>
|
<Text color={theme.text.primary}>{nonRespondingDisplay}</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,10 +65,39 @@ export const GeminiSpinner: React.FC<GeminiSpinnerProps> = ({
|
|||||||
altText,
|
altText,
|
||||||
}) => {
|
}) => {
|
||||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
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 ? (
|
return isScreenReaderEnabled ? (
|
||||||
<Text>{altText}</Text>
|
<Text>{altText}</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text color={theme.text.primary}>
|
<Text color={currentColor}>
|
||||||
<CliSpinner type={spinnerType} />
|
<CliSpinner type={spinnerType} />
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,41 +37,43 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
|||||||
const { columns: terminalWidth } = useTerminalSize();
|
const { columns: terminalWidth } = useTerminalSize();
|
||||||
const isNarrow = isNarrowWidth(terminalWidth);
|
const isNarrow = isNarrowWidth(terminalWidth);
|
||||||
|
|
||||||
if (
|
|
||||||
streamingState === StreamingState.Idle &&
|
|
||||||
!currentLoadingPhrase &&
|
|
||||||
!thought
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prioritize the interactive shell waiting phrase over the thought subject
|
// Prioritize the interactive shell waiting phrase over the thought subject
|
||||||
// because it conveys an actionable state for the user (waiting for input).
|
// because it conveys an actionable state for the user (waiting for input).
|
||||||
const primaryText =
|
const primaryText =
|
||||||
currentLoadingPhrase === INTERACTIVE_SHELL_WAITING_PHRASE
|
currentLoadingPhrase === INTERACTIVE_SHELL_WAITING_PHRASE
|
||||||
? currentLoadingPhrase
|
? 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 =
|
const cancelAndTimerContent =
|
||||||
showCancelAndTimer &&
|
showCancelAndTimer &&
|
||||||
streamingState !== StreamingState.WaitingForConfirmation
|
streamingState !== StreamingState.WaitingForConfirmation &&
|
||||||
|
streamingState !== StreamingState.Idle
|
||||||
? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})`
|
? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (inline) {
|
if (inline) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box marginRight={1}>
|
{streamingState !== StreamingState.Idle && (
|
||||||
<GeminiRespondingSpinner
|
<Box marginRight={1}>
|
||||||
nonRespondingDisplay={
|
<GeminiRespondingSpinner
|
||||||
streamingState === StreamingState.WaitingForConfirmation
|
nonRespondingDisplay={
|
||||||
? '⠏'
|
streamingState === StreamingState.WaitingForConfirmation
|
||||||
: ''
|
? '⠏'
|
||||||
}
|
: ''
|
||||||
/>
|
}
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{primaryText && (
|
{primaryText && (
|
||||||
<Text color={theme.text.accent} wrap="truncate-end">
|
<Text color={textColor} italic={italic} wrap="truncate-end">
|
||||||
{primaryText}
|
{primaryText}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -94,17 +96,19 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
|||||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Box marginRight={1}>
|
{streamingState !== StreamingState.Idle && (
|
||||||
<GeminiRespondingSpinner
|
<Box marginRight={1}>
|
||||||
nonRespondingDisplay={
|
<GeminiRespondingSpinner
|
||||||
streamingState === StreamingState.WaitingForConfirmation
|
nonRespondingDisplay={
|
||||||
? '⠏'
|
streamingState === StreamingState.WaitingForConfirmation
|
||||||
: ''
|
? '⠏'
|
||||||
}
|
: ''
|
||||||
/>
|
}
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{primaryText && (
|
{primaryText && (
|
||||||
<Text color={theme.text.accent} wrap="truncate-end">
|
<Text color={textColor} italic={italic} wrap="truncate-end">
|
||||||
{primaryText}
|
{primaryText}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export interface UIState {
|
|||||||
showEscapePrompt: boolean;
|
showEscapePrompt: boolean;
|
||||||
shortcutsHelpVisible: boolean;
|
shortcutsHelpVisible: boolean;
|
||||||
elapsedTime: number;
|
elapsedTime: number;
|
||||||
currentLoadingPhrase: string;
|
currentLoadingPhrase: string | undefined;
|
||||||
historyRemountKey: number;
|
historyRemountKey: number;
|
||||||
activeHooks: ActiveHook[];
|
activeHooks: ActiveHook[];
|
||||||
messageQueue: string[];
|
messageQueue: string[];
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ export const usePhraseCycler = (
|
|||||||
? customPhrases
|
? customPhrases
|
||||||
: WITTY_LOADING_PHRASES;
|
: WITTY_LOADING_PHRASES;
|
||||||
|
|
||||||
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState(
|
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState<
|
||||||
loadingPhrases[0],
|
string | undefined
|
||||||
);
|
>(isActive ? loadingPhrases[0] : undefined);
|
||||||
|
|
||||||
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const hasShownFirstRequestTipRef = useRef(false);
|
const hasShownFirstRequestTipRef = useRef(false);
|
||||||
@@ -56,7 +56,7 @@ export const usePhraseCycler = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
setCurrentLoadingPhrase(loadingPhrases[0]);
|
setCurrentLoadingPhrase(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user