From 372f41eab872e6d291ebe347055a9f0b06bdf423 Mon Sep 17 00:00:00 2001 From: Dmitry Lyalin Date: Thu, 19 Feb 2026 13:43:12 -0500 Subject: [PATCH] feat(cli): replace loading phrases boolean with enum setting (#19347) --- docs/cli/settings.md | 60 +++++++------- docs/get-started/configuration.md | 9 ++- packages/cli/src/config/settings.test.ts | 79 +++++++++++++++++++ packages/cli/src/config/settings.ts | 19 +++++ .../cli/src/config/settingsSchema.test.ts | 13 +++ packages/cli/src/config/settingsSchema.ts | 21 ++++- packages/cli/src/ui/AppContainer.tsx | 2 + .../cli/src/ui/components/Composer.test.tsx | 8 +- packages/cli/src/ui/components/Composer.tsx | 8 +- packages/cli/src/ui/constants/tips.ts | 2 +- .../src/ui/hooks/useLoadingIndicator.test.tsx | 20 ++++- .../cli/src/ui/hooks/useLoadingIndicator.ts | 4 + .../cli/src/ui/hooks/usePhraseCycler.test.tsx | 5 ++ packages/cli/src/ui/hooks/usePhraseCycler.ts | 67 ++++++++++------ packages/core/src/config/config.ts | 1 + schemas/settings.schema.json | 12 ++- 16 files changed, 260 insertions(+), 70 deletions(-) diff --git a/docs/cli/settings.md b/docs/cli/settings.md index e29ed30214..318fdbea75 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -41,36 +41,36 @@ they appear in the UI. ### UI -| UI Label | Setting | Description | Default | -| ------------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` | -| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` | -| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` | -| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` | -| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` | -| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` | -| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` | -| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` | -| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` | -| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` | -| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` | -| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` | -| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` | -| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` | -| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` | -| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` | -| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` | -| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` | -| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` | -| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` | -| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` | -| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` | -| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` | -| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` | -| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` | -| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` | -| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` | -| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` | +| UI Label | Setting | Description | Default | +| ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` | +| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` | +| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` | +| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` | +| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` | +| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` | +| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` | +| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` | +| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` | +| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` | +| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` | +| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` | +| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` | +| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` | +| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` | +| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` | +| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` | +| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` | +| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` | +| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` | +| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` | +| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` | +| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` | +| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` | +| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` | +| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` | +| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, both, or nothing. | `"tips"` | +| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` | ### IDE diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index c61aab5d18..8e2164080f 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -305,13 +305,20 @@ their corresponding top-level category object in your `settings.json` file. - **Description:** Show the spinner during operations. - **Default:** `true` +- **`ui.loadingPhrases`** (enum): + - **Description:** What to show while the model is working: tips, witty + comments, both, or nothing. + - **Default:** `"tips"` + - **Values:** `"tips"`, `"witty"`, `"all"`, `"off"` + - **`ui.customWittyPhrases`** (array): - **Description:** Custom witty phrases to display during loading. When provided, the CLI cycles through these instead of the defaults. - **Default:** `[]` - **`ui.accessibility.enableLoadingPhrases`** (boolean): - - **Description:** Enable loading phrases during operations. + - **Description:** @deprecated Use ui.loadingPhrases instead. Enable loading + phrases during operations. - **Default:** `true` - **Requires restart:** Yes diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index e88c9104dd..7b341b3ee0 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -2032,6 +2032,85 @@ describe('Settings Loading and Merging', () => { }), }), ); + + // Check that enableLoadingPhrases: false was further migrated to loadingPhrases: 'off' + expect(setValueSpy).toHaveBeenCalledWith( + SettingScope.User, + 'ui', + expect.objectContaining({ + loadingPhrases: 'off', + }), + ); + }); + + it('should migrate enableLoadingPhrases: false to loadingPhrases: off', () => { + const userSettingsContent = { + ui: { + accessibility: { + enableLoadingPhrases: false, + }, + }, + }; + + const loadedSettings = createMockSettings(userSettingsContent); + const setValueSpy = vi.spyOn(loadedSettings, 'setValue'); + + migrateDeprecatedSettings(loadedSettings); + + expect(setValueSpy).toHaveBeenCalledWith( + SettingScope.User, + 'ui', + expect.objectContaining({ + loadingPhrases: 'off', + }), + ); + }); + + it('should not migrate enableLoadingPhrases: true to loadingPhrases', () => { + const userSettingsContent = { + ui: { + accessibility: { + enableLoadingPhrases: true, + }, + }, + }; + + const loadedSettings = createMockSettings(userSettingsContent); + const setValueSpy = vi.spyOn(loadedSettings, 'setValue'); + + migrateDeprecatedSettings(loadedSettings); + + // Should not set loadingPhrases when enableLoadingPhrases is true + const uiCalls = setValueSpy.mock.calls.filter((call) => call[1] === 'ui'); + for (const call of uiCalls) { + const uiValue = call[2] as Record; + expect(uiValue).not.toHaveProperty('loadingPhrases'); + } + }); + + it('should not overwrite existing loadingPhrases during migration', () => { + const userSettingsContent = { + ui: { + loadingPhrases: 'witty', + accessibility: { + enableLoadingPhrases: false, + }, + }, + }; + + const loadedSettings = createMockSettings(userSettingsContent); + const setValueSpy = vi.spyOn(loadedSettings, 'setValue'); + + migrateDeprecatedSettings(loadedSettings); + + // Should not overwrite existing loadingPhrases + const uiCalls = setValueSpy.mock.calls.filter((call) => call[1] === 'ui'); + for (const call of uiCalls) { + const uiValue = call[2] as Record; + if (uiValue['loadingPhrases'] !== undefined) { + expect(uiValue['loadingPhrases']).toBe('witty'); + } + } }); it('should prioritize new settings over deprecated ones and respect removeDeprecated flag', () => { diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 7644588407..2f6f2f7450 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -165,7 +165,10 @@ export interface SummarizeToolOutputSettings { tokenBudget?: number; } +export type LoadingPhrasesMode = 'tips' | 'witty' | 'all' | 'off'; + export interface AccessibilitySettings { + /** @deprecated Use ui.loadingPhrases instead. */ enableLoadingPhrases?: boolean; screenReader?: boolean; } @@ -928,6 +931,22 @@ export function migrateDeprecatedSettings( anyModified = true; } } + + // Migrate enableLoadingPhrases: false → loadingPhrases: 'off' + const enableLP = newAccessibility['enableLoadingPhrases']; + if ( + typeof enableLP === 'boolean' && + newUi['loadingPhrases'] === undefined + ) { + if (!enableLP) { + newUi['loadingPhrases'] = 'off'; + loadedSettings.setValue(scope, 'ui', newUi); + if (!settingsFile.readOnly) { + anyModified = true; + } + } + foundDeprecated.push('ui.accessibility.enableLoadingPhrases'); + } } } diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 2a2b535eea..2638ac0347 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -83,6 +83,19 @@ describe('SettingsSchema', () => { ).toBe('boolean'); }); + it('should have loadingPhrases enum property', () => { + const definition = getSettingsSchema().ui?.properties?.loadingPhrases; + expect(definition).toBeDefined(); + expect(definition?.type).toBe('enum'); + expect(definition?.default).toBe('tips'); + expect(definition?.options?.map((o) => o.value)).toEqual([ + 'tips', + 'witty', + 'all', + 'off', + ]); + }); + it('should have checkpointing nested properties', () => { expect( getSettingsSchema().general?.properties?.checkpointing.properties diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 1138614235..70c5363659 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -672,6 +672,22 @@ const SETTINGS_SCHEMA = { description: 'Show the spinner during operations.', showInDialog: true, }, + loadingPhrases: { + type: 'enum', + label: 'Loading Phrases', + category: 'UI', + requiresRestart: false, + default: 'tips', + description: + 'What to show while the model is working: tips, witty comments, both, or nothing.', + showInDialog: true, + options: [ + { value: 'tips', label: 'Tips' }, + { value: 'witty', label: 'Witty' }, + { value: 'all', label: 'All' }, + { value: 'off', label: 'Off' }, + ], + }, customWittyPhrases: { type: 'array', label: 'Custom Witty Phrases', @@ -700,8 +716,9 @@ const SETTINGS_SCHEMA = { category: 'UI', requiresRestart: true, default: true, - description: 'Enable loading phrases during operations.', - showInDialog: true, + description: + '@deprecated Use ui.loadingPhrases instead. Enable loading phrases during operations.', + showInDialog: false, }, screenReader: { type: 'boolean', diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index a3b460555b..08bae44959 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -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( diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index 946a041841..12deda3e76 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -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'); diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index fd30e33858..fe2adba9ab 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -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 } diff --git a/packages/cli/src/ui/constants/tips.ts b/packages/cli/src/ui/constants/tips.ts index 4678efb54e..f061175adb 100644 --- a/packages/cli/src/ui/constants/tips.ts +++ b/packages/cli/src/ui/constants/tips.ts @@ -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)…', diff --git a/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx b/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx index 23da2131b2..16ab6198ab 100644 --- a/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx +++ b/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx @@ -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; 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(), + mode?: LoadingPhrasesMode; + }) => rerender(), }; }; @@ -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(); + }); }); diff --git a/packages/cli/src/ui/hooks/useLoadingIndicator.ts b/packages/cli/src/ui/hooks/useLoadingIndicator.ts index 2c8f6b0d1e..b6c85da6b8 100644 --- a/packages/cli/src/ui/hooks/useLoadingIndicator.ts +++ b/packages/cli/src/ui/hooks/useLoadingIndicator.ts @@ -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, ); diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx index 9926e017d8..837d953c3c 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx +++ b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx @@ -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 {phrase}; @@ -289,6 +293,7 @@ describe('usePhraseCycler', () => { ); diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.ts b/packages/cli/src/ui/hooks/usePhraseCycler.ts index ffc469f02a..8ddab6eef9 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.ts +++ b/packages/cli/src/ui/hooks/usePhraseCycler.ts @@ -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(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; }; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index ad2b0a1a1b..33e02abf89 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -128,6 +128,7 @@ import { isSubpath } from '../utils/paths.js'; import { UserHintService } from './userHintService.js'; export interface AccessibilitySettings { + /** @deprecated Use ui.loadingPhrases instead. */ enableLoadingPhrases?: boolean; screenReader?: boolean; } diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index abe8e97de0..f15af605a3 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -405,6 +405,14 @@ "default": true, "type": "boolean" }, + "loadingPhrases": { + "title": "Loading Phrases", + "description": "What to show while the model is working: tips, witty comments, both, or nothing.", + "markdownDescription": "What to show while the model is working: tips, witty comments, both, or nothing.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `tips`", + "default": "tips", + "type": "string", + "enum": ["tips", "witty", "all", "off"] + }, "customWittyPhrases": { "title": "Custom Witty Phrases", "description": "Custom witty phrases to display during loading. When provided, the CLI cycles through these instead of the defaults.", @@ -424,8 +432,8 @@ "properties": { "enableLoadingPhrases": { "title": "Enable Loading Phrases", - "description": "Enable loading phrases during operations.", - "markdownDescription": "Enable loading phrases during operations.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`", + "description": "@deprecated Use ui.loadingPhrases instead. Enable loading phrases during operations.", + "markdownDescription": "@deprecated Use ui.loadingPhrases instead. Enable loading phrases during operations.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`", "default": true, "type": "boolean" },