feat(cli): replace loading phrases boolean with enum setting (#19347)

This commit is contained in:
Dmitry Lyalin
2026-02-19 13:43:12 -05:00
committed by GitHub
parent 09b623fbd7
commit 372f41eab8
16 changed files with 260 additions and 70 deletions
+2
View File
@@ -1596,6 +1596,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
streamingState,
shouldShowFocusHint,
retryStatus,
loadingPhrasesMode: settings.merged.ui.loadingPhrases,
customWittyPhrases: settings.merged.ui.customWittyPhrases,
});
const handleGlobalKeypress = useCallback(
@@ -391,16 +391,16 @@ describe('Composer', () => {
expect(output).not.toContain('ShortcutsHint');
});
it('renders LoadingIndicator without thought when accessibility disables loading phrases', async () => {
it('renders LoadingIndicator without thought when loadingPhrases is off', async () => {
const uiState = createMockUIState({
streamingState: StreamingState.Responding,
thought: { subject: 'Hidden', description: 'Should not show' },
});
const config = createMockConfig({
getAccessibility: vi.fn(() => ({ enableLoadingPhrases: false })),
const settings = createMockSettings({
merged: { ui: { loadingPhrases: 'off' } },
});
const { lastFrame } = await renderComposer(uiState, undefined, config);
const { lastFrame } = await renderComposer(uiState, settings);
const output = lastFrame();
expect(output).toContain('LoadingIndicator');
+4 -4
View File
@@ -211,12 +211,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
thought={
uiState.streamingState ===
StreamingState.WaitingForConfirmation ||
config.getAccessibility()?.enableLoadingPhrases === false
settings.merged.ui.loadingPhrases === 'off'
? undefined
: uiState.thought
}
currentLoadingPhrase={
config.getAccessibility()?.enableLoadingPhrases === false
settings.merged.ui.loadingPhrases === 'off'
? undefined
: uiState.currentLoadingPhrase
}
@@ -255,12 +255,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
thought={
uiState.streamingState ===
StreamingState.WaitingForConfirmation ||
config.getAccessibility()?.enableLoadingPhrases === false
settings.merged.ui.loadingPhrases === 'off'
? undefined
: uiState.thought
}
currentLoadingPhrase={
config.getAccessibility()?.enableLoadingPhrases === false
settings.merged.ui.loadingPhrases === 'off'
? undefined
: uiState.currentLoadingPhrase
}
+1 -1
View File
@@ -24,7 +24,7 @@ export const INFORMATIVE_TIPS = [
'Show memory usage for performance monitoring (/settings)…',
'Show line numbers in the chat for easier reference (/settings)…',
'Show citations to see where the model gets information (/settings)…',
'Disable loading phrases for a quieter experience (/settings)…',
'Customize loading phrases: tips, witty, all, or off (/settings)…',
'Add custom witty phrases to the loading screen (settings.json)…',
'Use alternate screen buffer to preserve shell history (/settings)…',
'Choose a specific Gemini model for conversations (/settings)…',
@@ -16,6 +16,7 @@ import {
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
import { INFORMATIVE_TIPS } from '../constants/tips.js';
import type { RetryAttemptPayload } from '@google/gemini-cli-core';
import type { LoadingPhrasesMode } from '../../config/settings.js';
describe('useLoadingIndicator', () => {
beforeEach(() => {
@@ -33,21 +34,25 @@ describe('useLoadingIndicator', () => {
initialStreamingState: StreamingState,
initialShouldShowFocusHint: boolean = false,
initialRetryStatus: RetryAttemptPayload | null = null,
loadingPhrasesMode: LoadingPhrasesMode = 'all',
) => {
let hookResult: ReturnType<typeof useLoadingIndicator>;
function TestComponent({
streamingState,
shouldShowFocusHint,
retryStatus,
mode,
}: {
streamingState: StreamingState;
shouldShowFocusHint?: boolean;
retryStatus?: RetryAttemptPayload | null;
mode?: LoadingPhrasesMode;
}) {
hookResult = useLoadingIndicator({
streamingState,
shouldShowFocusHint: !!shouldShowFocusHint,
retryStatus: retryStatus || null,
loadingPhrasesMode: mode,
});
return null;
}
@@ -56,6 +61,7 @@ describe('useLoadingIndicator', () => {
streamingState={initialStreamingState}
shouldShowFocusHint={initialShouldShowFocusHint}
retryStatus={initialRetryStatus}
mode={loadingPhrasesMode}
/>,
);
return {
@@ -68,7 +74,8 @@ describe('useLoadingIndicator', () => {
streamingState: StreamingState;
shouldShowFocusHint?: boolean;
retryStatus?: RetryAttemptPayload | null;
}) => rerender(<TestComponent {...newProps} />),
mode?: LoadingPhrasesMode;
}) => rerender(<TestComponent mode={loadingPhrasesMode} {...newProps} />),
};
};
@@ -221,4 +228,15 @@ describe('useLoadingIndicator', () => {
expect(result.current.currentLoadingPhrase).toContain('Trying to reach');
expect(result.current.currentLoadingPhrase).toContain('Attempt 3/3');
});
it('should show no phrases when loadingPhrasesMode is "off"', () => {
const { result } = renderLoadingIndicatorHook(
StreamingState.Responding,
false,
null,
'off',
);
expect(result.current.currentLoadingPhrase).toBeUndefined();
});
});
@@ -12,11 +12,13 @@ import {
getDisplayString,
type RetryAttemptPayload,
} from '@google/gemini-cli-core';
import type { LoadingPhrasesMode } from '../../config/settings.js';
export interface UseLoadingIndicatorProps {
streamingState: StreamingState;
shouldShowFocusHint: boolean;
retryStatus: RetryAttemptPayload | null;
loadingPhrasesMode?: LoadingPhrasesMode;
customWittyPhrases?: string[];
}
@@ -24,6 +26,7 @@ export const useLoadingIndicator = ({
streamingState,
shouldShowFocusHint,
retryStatus,
loadingPhrasesMode,
customWittyPhrases,
}: UseLoadingIndicatorProps) => {
const [timerResetKey, setTimerResetKey] = useState(0);
@@ -37,6 +40,7 @@ export const useLoadingIndicator = ({
isPhraseCyclingActive,
isWaiting,
shouldShowFocusHint,
loadingPhrasesMode,
customWittyPhrases,
);
@@ -14,23 +14,27 @@ import {
} from './usePhraseCycler.js';
import { INFORMATIVE_TIPS } from '../constants/tips.js';
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
import type { LoadingPhrasesMode } from '../../config/settings.js';
// Test component to consume the hook
const TestComponent = ({
isActive,
isWaiting,
isInteractiveShellWaiting = false,
loadingPhrasesMode = 'all',
customPhrases,
}: {
isActive: boolean;
isWaiting: boolean;
isInteractiveShellWaiting?: boolean;
loadingPhrasesMode?: LoadingPhrasesMode;
customPhrases?: string[];
}) => {
const phrase = usePhraseCycler(
isActive,
isWaiting,
isInteractiveShellWaiting,
loadingPhrasesMode,
customPhrases,
);
return <Text>{phrase}</Text>;
@@ -289,6 +293,7 @@ describe('usePhraseCycler', () => {
<TestComponent
isActive={config.isActive}
isWaiting={false}
loadingPhrasesMode="witty"
customPhrases={config.customPhrases}
/>
);
+42 -25
View File
@@ -7,6 +7,7 @@
import { useState, useEffect, useRef } from 'react';
import { INFORMATIVE_TIPS } from '../constants/tips.js';
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
import type { LoadingPhrasesMode } from '../../config/settings.js';
export const PHRASE_CHANGE_INTERVAL_MS = 15000;
export const INTERACTIVE_SHELL_WAITING_PHRASE =
@@ -17,23 +18,20 @@ export const INTERACTIVE_SHELL_WAITING_PHRASE =
* @param isActive Whether the phrase cycling should be active.
* @param isWaiting Whether to show a specific waiting phrase.
* @param shouldShowFocusHint Whether to show the shell focus hint.
* @param customPhrases Optional list of custom phrases to use.
* @param loadingPhrasesMode Which phrases to show: tips, witty, all, or off.
* @param customPhrases Optional list of custom phrases to use instead of built-in witty phrases.
* @returns The current loading phrase.
*/
export const usePhraseCycler = (
isActive: boolean,
isWaiting: boolean,
shouldShowFocusHint: boolean,
loadingPhrasesMode: LoadingPhrasesMode = 'tips',
customPhrases?: string[],
) => {
const loadingPhrases =
customPhrases && customPhrases.length > 0
? customPhrases
: WITTY_LOADING_PHRASES;
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState<
string | undefined
>(isActive ? loadingPhrases[0] : undefined);
>(undefined);
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
const hasShownFirstRequestTipRef = useRef(false);
@@ -55,30 +53,43 @@ export const usePhraseCycler = (
return;
}
if (!isActive) {
if (!isActive || loadingPhrasesMode === 'off') {
setCurrentLoadingPhrase(undefined);
return;
}
const wittyPhrases =
customPhrases && customPhrases.length > 0
? customPhrases
: WITTY_LOADING_PHRASES;
const setRandomPhrase = () => {
if (customPhrases && customPhrases.length > 0) {
const randomIndex = Math.floor(Math.random() * customPhrases.length);
setCurrentLoadingPhrase(customPhrases[randomIndex]);
} else {
let phraseList;
// Show a tip on the first request after startup, then continue with 1/6 chance
if (!hasShownFirstRequestTipRef.current) {
// Show a tip during the first request
let phraseList: readonly string[];
switch (loadingPhrasesMode) {
case 'tips':
phraseList = INFORMATIVE_TIPS;
hasShownFirstRequestTipRef.current = true;
} else {
// Roughly 1 in 6 chance to show a tip after the first request
const showTip = Math.random() < 1 / 6;
phraseList = showTip ? INFORMATIVE_TIPS : WITTY_LOADING_PHRASES;
}
const randomIndex = Math.floor(Math.random() * phraseList.length);
setCurrentLoadingPhrase(phraseList[randomIndex]);
break;
case 'witty':
phraseList = wittyPhrases;
break;
case 'all':
// Show a tip on the first request after startup, then continue with 1/6 chance
if (!hasShownFirstRequestTipRef.current) {
phraseList = INFORMATIVE_TIPS;
hasShownFirstRequestTipRef.current = true;
} else {
const showTip = Math.random() < 1 / 6;
phraseList = showTip ? INFORMATIVE_TIPS : wittyPhrases;
}
break;
default:
phraseList = INFORMATIVE_TIPS;
break;
}
const randomIndex = Math.floor(Math.random() * phraseList.length);
setCurrentLoadingPhrase(phraseList[randomIndex]);
};
// Select an initial random phrase
@@ -95,7 +106,13 @@ export const usePhraseCycler = (
phraseIntervalRef.current = null;
}
};
}, [isActive, isWaiting, shouldShowFocusHint, customPhrases, loadingPhrases]);
}, [
isActive,
isWaiting,
shouldShowFocusHint,
loadingPhrasesMode,
customPhrases,
]);
return currentLoadingPhrase;
};