diff --git a/evals/hierarchical_memory.eval.ts b/evals/hierarchical_memory.eval.ts
index 374610aeabb..0a3b76cea27 100644
--- a/evals/hierarchical_memory.eval.ts
+++ b/evals/hierarchical_memory.eval.ts
@@ -107,7 +107,7 @@ Set the theme to "Light".
Set the theme to "Dark".
-What theme should I use?`,
+What theme should I use? Tell me just the name of the theme.`,
assert: async (_rig, result) => {
assertModelHasOutput(result);
expect(result).toMatch(/Dark/i);
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.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx
index 102ddfb1b71..635a3bfa831 100644
--- a/packages/cli/src/ui/components/Footer.test.tsx
+++ b/packages/cli/src/ui/components/Footer.test.tsx
@@ -128,7 +128,7 @@ describe('', () => {
}),
});
expect(lastFrame()).toContain(defaultProps.model);
- expect(lastFrame()).toMatch(/\(\d+% context left\)/);
+ expect(lastFrame()).toMatch(/\d+% context left/);
});
it('displays the usage indicator when usage is low', () => {
@@ -207,7 +207,7 @@ describe('', () => {
}),
});
expect(lastFrame()).toContain(defaultProps.model);
- expect(lastFrame()).toMatch(/\(\d+%\)/);
+ expect(lastFrame()).toMatch(/\d+%/);
});
describe('sandbox and trust info', () => {
@@ -352,9 +352,8 @@ describe('', () => {
}),
});
expect(lastFrame()).toContain(defaultProps.model);
- expect(lastFrame()).not.toMatch(/\(\d+% context left\)/);
+ expect(lastFrame()).not.toMatch(/\d+% context left/);
});
-
it('shows the context percentage when hideContextPercentage is false', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
@@ -368,9 +367,8 @@ describe('', () => {
}),
});
expect(lastFrame()).toContain(defaultProps.model);
- expect(lastFrame()).toMatch(/\(\d+% context left\)/);
+ expect(lastFrame()).toMatch(/\d+% context left/);
});
-
it('renders complete footer in narrow terminal (baseline narrow)', () => {
const { lastFrame } = renderWithProviders(, {
width: 79,
diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx
index 1ace3f7d2b3..3fc830c1b70 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 { QuotaDisplay } from './QuotaDisplay.js';
@@ -41,7 +40,6 @@ export const Footer: React.FC = () => {
errorCount,
showErrorDetails,
promptTokenCount,
- nightly,
isTrustedFolder,
terminalWidth,
quotaStats,
@@ -55,7 +53,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,
quotaStats: uiState.quota.stats,
@@ -90,20 +87,14 @@ export const Footer: React.FC = () => {
{displayVimMode && (
[{displayVimMode}]
)}
- {!hideCWD &&
- (nightly ? (
-
- {displayPath}
- {branchName && ({branchName}*)}
-
- ) : (
-
- {displayPath}
- {branchName && (
- ({branchName}*)
- )}
-
- ))}
+ {!hideCWD && (
+
+ {displayPath}
+ {branchName && (
+ ({branchName}*)
+ )}
+
+ )}
{debugMode && (
{' ' + (debugMessage || '--debug')}
@@ -149,9 +140,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/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 122988a07fd..0621255f903 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -56,7 +56,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';
@@ -1405,12 +1408,12 @@ export const InputPrompt: React.FC = ({
/>
) : null}
', () => {
elapsedTime: 5,
};
- it('should not render when streamingState is Idle and no loading phrase or thought', () => {
+ it('should render blank when streamingState is Idle and no loading phrase or thought', () => {
const { lastFrame } = renderWithContext(
,
StreamingState.Idle,
);
- expect(lastFrame()).toBe('');
+ expect(lastFrame()?.trim()).toBe('');
});
it('should render spinner, phrase, and time when streamingState is Responding', () => {
@@ -152,7 +152,7 @@ describe('', () => {
,
StreamingState.Idle,
);
- expect(lastFrame()).toBe(''); // Initial: Idle (no loading phrase)
+ expect(lastFrame()?.trim()).toBe(''); // Initial: Idle (no loading phrase)
// Transition to Responding
rerender(
@@ -189,7 +189,7 @@ describe('', () => {
,
);
- expect(lastFrame()).toBe(''); // Idle with no loading phrase
+ expect(lastFrame()?.trim()).toBe(''); // Idle with no loading phrase and no spinner
unmount();
});
diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx
index f2e9c4ff406..3d6a838370b 100644
--- a/packages/cli/src/ui/components/LoadingIndicator.tsx
+++ b/packages/cli/src/ui/components/LoadingIndicator.tsx
@@ -82,7 +82,7 @@ export const LoadingIndicator: React.FC = ({
/>
{primaryText && (
-
+
{thinkingIndicator}
{primaryText}
@@ -116,7 +116,7 @@ export const LoadingIndicator: React.FC = ({
/>
{primaryText && (
-
+
{thinkingIndicator}
{primaryText}
diff --git a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
index cc0357c58af..659e7f1e6f0 100644
--- a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
@@ -1,12 +1,12 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[` > displays "Limit reached" message when remaining is 0 1`] = `" ...directories/to/make/it/long no sandbox (see /docs) gemini-pro /model Limit reached"`;
+exports[` > displays "Limit reached" message when remaining is 0 1`] = `" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro Limit reached"`;
-exports[` > displays the usage indicator when usage is low 1`] = `" ...directories/to/make/it/long no sandbox (see /docs) gemini-pro /model 15%"`;
+exports[` > displays the usage indicator when usage is low 1`] = `" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro 15%"`;
-exports[` > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `" ...s/to/make/it/long no sandbox gemini-pro /model (100%)"`;
+exports[` > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `" ...s/to/make/it/long no sandbox /model gemini-pro 100%"`;
-exports[` > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `" ...directories/to/make/it/long no sandbox (see /docs) gemini-pro /model (100% context left)"`;
+exports[` > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro 100% context left"`;
exports[` > footer configuration filtering (golden snapshots) > renders footer with CWD and model info hidden to test alignment (only sandbox visible) > footer-only-sandbox 1`] = `" no sandbox (see /docs)"`;
@@ -14,4 +14,4 @@ exports[` > footer configuration filtering (golden snapshots) > render
exports[` > footer configuration filtering (golden snapshots) > renders footer with only model info hidden (partial filtering) > footer-no-model 1`] = `" ...directories/to/make/it/long no sandbox (see /docs)"`;
-exports[` > hides the usage indicator when usage is not near limit 1`] = `" ...directories/to/make/it/long no sandbox (see /docs) gemini-pro /model"`;
+exports[` > hides the usage indicator when usage is not near limit 1`] = `" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro"`;
diff --git a/packages/cli/src/ui/components/messages/UserMessage.tsx b/packages/cli/src/ui/components/messages/UserMessage.tsx
index 0c3c99cb12e..ab45db7cf05 100644
--- a/packages/cli/src/ui/components/messages/UserMessage.tsx
+++ b/packages/cli/src/ui/components/messages/UserMessage.tsx
@@ -52,7 +52,7 @@ export const UserMessage: React.FC = ({ 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 6d33e55d454..197711f82f2 100644
--- a/packages/cli/src/ui/constants.ts
+++ b/packages/cli/src/ui/constants.ts
@@ -34,7 +34,9 @@ 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 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/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx
index 138eeac482d..cf41896232f 100644
--- a/packages/cli/src/ui/contexts/UIStateContext.tsx
+++ b/packages/cli/src/ui/contexts/UIStateContext.tsx
@@ -121,7 +121,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/useLoadingIndicator.test.tsx b/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx
index df0a766fae4..23da2131b29 100644
--- a/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx
+++ b/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx
@@ -76,9 +76,7 @@ describe('useLoadingIndicator', () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { result } = renderLoadingIndicatorHook(StreamingState.Idle);
expect(result.current.elapsedTime).toBe(0);
- expect(WITTY_LOADING_PHRASES).toContain(
- result.current.currentLoadingPhrase,
- );
+ expect(result.current.currentLoadingPhrase).toBeUndefined();
});
it('should show interactive shell waiting phrase when shouldShowFocusHint is true', async () => {
@@ -198,9 +196,7 @@ describe('useLoadingIndicator', () => {
});
expect(result.current.elapsedTime).toBe(0);
- expect(WITTY_LOADING_PHRASES).toContain(
- result.current.currentLoadingPhrase,
- );
+ expect(result.current.currentLoadingPhrase).toBeUndefined();
// Timer should not advance
await act(async () => {
diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx
index 40b47664d17..62c5430f231 100644
--- a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx
+++ b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx
@@ -45,12 +45,12 @@ describe('usePhraseCycler', () => {
vi.restoreAllMocks();
});
- it('should initialize with a witty phrase when not active and not waiting', () => {
+ it('should initialize with an empty string when not active and not waiting', () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { lastFrame } = render(
,
);
- expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
+ expect(lastFrame()).toBe('');
});
it('should show "Waiting for user confirmation..." when isWaiting is true', async () => {
@@ -195,7 +195,7 @@ describe('usePhraseCycler', () => {
});
expect(customPhrases).toContain(lastFrame()); // Should be one of the custom phrases
- // Deactivate -> resets to first phrase in sequence
+ // Deactivate -> resets to undefined (empty string in output)
rerender(
{
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
- // The phrase should be the first phrase after reset
- expect(customPhrases).toContain(lastFrame());
+ // The phrase should be empty after reset
+ expect(lastFrame()).toBe('');
// Activate again -> this will show a tip on first activation, then cycle from where mock is
rerender(
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;
}
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: {