diff --git a/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts b/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts index 734a92606dd..77e381b873d 100644 --- a/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts @@ -21,9 +21,11 @@ describe('useLoadingIndicator', () => { afterEach(() => { vi.useRealTimers(); // Restore real timers after each test act(() => vi.runOnlyPendingTimers); + vi.restoreAllMocks(); }); it('should initialize with default values when Idle', () => { + vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty const { result } = renderHook(() => useLoadingIndicator(StreamingState.Idle), ); @@ -34,6 +36,7 @@ describe('useLoadingIndicator', () => { }); it('should reflect values when Responding', async () => { + vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty const { result } = renderHook(() => useLoadingIndicator(StreamingState.Responding), ); @@ -82,6 +85,7 @@ describe('useLoadingIndicator', () => { }); it('should reset elapsedTime and use a witty phrase when transitioning from WaitingForConfirmation to Responding', async () => { + vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty const { result, rerender } = renderHook( ({ streamingState }) => useLoadingIndicator(streamingState), { initialProps: { streamingState: StreamingState.Responding } }, @@ -115,6 +119,7 @@ describe('useLoadingIndicator', () => { }); it('should reset timer and phrase when streamingState changes from Responding to Idle', async () => { + vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty const { result, rerender } = renderHook( ({ streamingState }) => useLoadingIndicator(streamingState), { initialProps: { streamingState: StreamingState.Responding } }, diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.test.ts b/packages/cli/src/ui/hooks/usePhraseCycler.test.ts index 88eed68c472..1ed99b05df1 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.test.ts +++ b/packages/cli/src/ui/hooks/usePhraseCycler.test.ts @@ -22,6 +22,7 @@ describe('usePhraseCycler', () => { }); it('should initialize with a witty phrase when not active and not waiting', () => { + vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty const { result } = renderHook(() => usePhraseCycler(false, false)); expect(WITTY_LOADING_PHRASES).toContain(result.current); }); @@ -45,6 +46,7 @@ describe('usePhraseCycler', () => { }); it('should cycle through witty phrases when isActive is true and not waiting', () => { + vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty const { result } = renderHook(() => usePhraseCycler(true, false)); // Initial phrase should be one of the witty phrases expect(WITTY_LOADING_PHRASES).toContain(result.current); @@ -70,12 +72,19 @@ describe('usePhraseCycler', () => { } // Mock Math.random to make the test deterministic. - let callCount = 0; + const mockRandomValues = [ + 0.5, // -> witty + 0, // -> index 0 + 0.5, // -> witty + 1 / WITTY_LOADING_PHRASES.length, // -> index 1 + 0.5, // -> witty + 0, // -> index 0 + ]; + let randomCallCount = 0; vi.spyOn(Math, 'random').mockImplementation(() => { - // Cycle through 0, 1, 0, 1, ... - const val = callCount % 2; - callCount++; - return val / WITTY_LOADING_PHRASES.length; + const val = mockRandomValues[randomCallCount % mockRandomValues.length]; + randomCallCount++; + return val; }); const { result, rerender } = renderHook( @@ -120,7 +129,7 @@ describe('usePhraseCycler', () => { it('should use custom phrases when provided', () => { const customPhrases = ['Custom Phrase 1', 'Custom Phrase 2']; let callCount = 0; - vi.spyOn(Math, 'random').mockImplementation(() => { + const randomMock = vi.spyOn(Math, 'random').mockImplementation(() => { const val = callCount % 2; callCount++; return val / customPhrases.length; @@ -146,12 +155,17 @@ describe('usePhraseCycler', () => { expect(result.current).toBe(customPhrases[1]); + // Test fallback to default phrases. + randomMock.mockRestore(); + vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty + rerender({ isActive: true, isWaiting: false, customPhrases: undefined }); expect(WITTY_LOADING_PHRASES).toContain(result.current); }); it('should fall back to witty phrases if custom phrases are an empty array', () => { + vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty const { result } = renderHook( ({ isActive, isWaiting, customPhrases: phrases }) => usePhraseCycler(isActive, isWaiting, phrases), @@ -168,6 +182,7 @@ describe('usePhraseCycler', () => { }); it('should reset to a witty phrase when transitioning from waiting to active', () => { + vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty const { result, rerender } = renderHook( ({ isActive, isWaiting }) => usePhraseCycler(isActive, isWaiting), { initialProps: { isActive: true, isWaiting: false } }, diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.ts b/packages/cli/src/ui/hooks/usePhraseCycler.ts index d872466bbf3..f031b4cea70 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.ts +++ b/packages/cli/src/ui/hooks/usePhraseCycler.ts @@ -139,6 +139,148 @@ export const WITTY_LOADING_PHRASES = [ 'Releasing the HypnoDrones...', ]; +export const INFORMATIVE_TIPS = [ + //Settings tips start here + 'Set your preferred editor for opening files (/settings)...', + 'Toggle Vim mode for a modal editing experience (/settings)...', + 'Disable automatic updates if you prefer manual control (/settings)...', + 'Turn off nagging update notifications (settings.json)...', + 'Enable checkpointing to recover your session after a crash (settings.json)...', + 'Change CLI output format to JSON for scripting (/settings)...', + 'Personalize your CLI with a new color theme (/settings)...', + 'Create and use your own custom themes (settings.json)...', + 'Hide window title for a more minimal UI (/settings)...', + "Don't like these tips? You can hide them (/settings)...", + 'Hide the startup banner for a cleaner launch (/settings)...', + 'Reclaim vertical space by hiding the footer (/settings)...', + 'Show memory usage for performance monitoring (/settings)...', + 'Show citations to see where the model gets information (/settings)...', + 'Disable loading phrases for a quieter experience (/settings)...', + 'Add custom witty phrases to the loading screen (settings.json)...', + 'Choose a specific Gemini model for conversations (/settings)...', + 'Limit the number of turns in your session history (/settings)...', + 'Automatically summarize large tool outputs to save tokens (settings.json)...', + 'Control when chat history gets compressed based on token usage (settings.json)...', + 'Define custom context file names, like CONTEXT.md (settings.json)...', + 'Set max directories to scan for context files (/settings)...', + 'Expand your workspace with additional directories (/directory)...', + 'Control how /memory refresh loads context files (/settings)...', + 'Toggle respect for .gitignore files in context (/settings)...', + 'Toggle respect for .geminiignore files in context (/settings)...', + 'Enable recursive file search for @-file completions (/settings)...', + 'Run tools in a secure sandbox environment (settings.json)...', + 'Use an interactive terminal for shell commands (/settings)...', + 'Restrict available built-in tools (settings.json)...', + 'Exclude specific tools from being used (settings.json)...', + 'Bypass confirmation for trusted tools (settings.json)...', + 'Use a custom command for tool discovery (settings.json)...', + 'Define a custom command for calling discovered tools (settings.json)...', + 'Define and manage connections to MCP servers (settings.json)...', + 'Enable folder trust to enhance security (/settings)...', + 'Change your authentication method (/settings)...', + 'Enforce auth type for enterprise use (settings.json)...', + 'Let Node.js auto-configure memory (settings.json)...', + 'Customize the DNS resolution order (settings.json)...', + 'Exclude env vars from the context (settings.json)...', + 'Configure a custom command for filing bug reports (settings.json)...', + 'Enable or disable telemetry collection (/settings)...', + 'Send telemetry data to a local file or GCP (settings.json)...', + 'Configure the OTLP endpoint for telemetry (settings.json)...', + 'Choose whether to log prompt content (settings.json)...', + 'Enable AI-powered prompt completion while typing (/settings)...', + 'Enable debug logging of keystrokes to the console (/settings)...', + 'Enable automatic session cleanup of old conversations (/settings)...', + 'Show Gemini CLI status in the terminal window title (/settings)...', + 'Use the entire width of the terminal for output (/settings)...', + 'Enable screen reader mode for better accessibility (/settings)...', + 'Skip the next speaker check for faster responses (/settings)...', + 'Use ripgrep for faster file content search (/settings)...', + 'Enable truncation of large tool outputs to save tokens (/settings)...', + 'Set the character threshold for truncating tool outputs (/settings)...', + 'Set the number of lines to keep when truncating outputs (/settings)...', + 'Enable policy-based tool confirmation via message bus (/settings)...', + 'Enable smart-edit tool for more precise editing (/settings)...', + 'Enable write_todos_list tool to generate task lists (/settings)...', + 'Enable model routing based on complexity (/settings)...', + 'Enable experimental subagents for task delegation (/settings)...', + //Settings tips end here + // Keyboard shortcut tips start here + 'Close dialogs and suggestions with Esc...', + 'Cancel a request with Ctrl+C, or press twice to exit...', + 'Exit the app with Ctrl+D on an empty line...', + 'Clear your screen at any time with Ctrl+L...', + 'Toggle the debug console display with Ctrl+O...', + 'See full, untruncated responses with Ctrl+S...', + 'Show or hide tool descriptions with Ctrl+T...', + 'Toggle auto-approval (YOLO mode) for all tools with Ctrl+Y...', + 'Toggle shell mode by typing ! in an empty prompt...', + 'Insert a newline with a backslash (\\) followed by Enter...', + 'Navigate your prompt history with the Up and Down arrows...', + 'You can also use Ctrl+P (up) and Ctrl+N (down) for history...', + 'Submit your prompt to Gemini with Enter...', + 'Accept an autocomplete suggestion with Tab or Enter...', + 'Move to the start of the line with Ctrl+A or Home...', + 'Move to the end of the line with Ctrl+E or End...', + 'Move one character left or right with Ctrl+B/F or the arrow keys...', + 'Move one word left or right with Ctrl+Left/Right Arrow...', + 'Delete the character to the left with Ctrl+H or Backspace...', + 'Delete the character to the right with Ctrl+D or Delete...', + 'Delete the word to the left of the cursor with Ctrl+W...', + 'Delete the word to the right of the cursor with Ctrl+Delete...', + 'Delete from the cursor to the start of the line with Ctrl+U...', + 'Delete from the cursor to the end of the line with Ctrl+K...', + 'Clear the entire input prompt with a double-press of Esc...', + 'Paste from your clipboard with Ctrl+V...', + 'Open the current prompt in an external editor with Ctrl+X...', + 'In menus, move up/down with k/j or the arrow keys...', + 'In menus, select an item by typing its number...', + "If you're using an IDE, see the context with Ctrl+G...", + // Keyboard shortcut tips end here + // Command tips start here + 'Show version info with /about...', + 'Change your authentication method with /auth...', + 'File a bug report directly with /bug...', + 'List your saved chat checkpoints with /chat list...', + 'Save your current conversation with /chat save ...', + 'Resume a saved conversation with /chat resume ...', + 'Delete a conversation checkpoint with /chat delete ...', + 'Share your conversation to a file with /chat share ...', + 'Clear the screen and history with /clear...', + 'Save tokens by summarizing the context with /compress...', + 'Copy the last response to your clipboard with /copy...', + 'Open the full documentation in your browser with /docs...', + 'Add directories to your workspace with /directory add ...', + 'Show all directories in your workspace with /directory show...', + 'Set your preferred external editor with /editor...', + 'List all active extensions with /extensions list...', + 'Update all or specific extensions with /extensions update...', + 'Get help on commands with /help...', + 'Manage IDE integration with /ide...', + 'Create a project-specific GEMINI.md file with /init...', + 'List configured MCP servers and tools with /mcp list...', + 'Authenticate with an OAuth-enabled MCP server with /mcp auth...', + 'Restart MCP servers with /mcp refresh...', + 'See the current instructional context with /memory show...', + 'Add content to the instructional memory with /memory add...', + 'Reload instructional context from GEMINI.md files with /memory refresh...', + 'List the paths of the GEMINI.md files in use with /memory list...', + 'Display the privacy notice with /privacy...', + 'Exit the CLI with /quit or /exit...', + 'Check model-specific usage stats with /stats model...', + 'Check tool-specific usage stats with /stats tools...', + "Change the CLI's color theme with /theme...", + 'List all available tools with /tools...', + 'View and edit settings with the /settings editor...', + 'Toggle Vim keybindings on and off with /vim...', + 'Set up GitHub Actions with /setup-github...', + 'Configure terminal keybindings for multiline input with /terminal-setup...', + 'Find relevant documentation with /find-docs...', + 'Review a pull request with /oncall:pr-review...', + 'Go back to main and clean up the branch with /github:cleanup-back-to-main...', + 'Execute any shell command with !...', + // Command tips end here +]; + export const PHRASE_CHANGE_INTERVAL_MS = 15000; /** @@ -173,16 +315,26 @@ export const usePhraseCycler = ( if (phraseIntervalRef.current) { clearInterval(phraseIntervalRef.current); } + + const setRandomPhrase = () => { + if (customPhrases && customPhrases.length > 0) { + const randomIndex = Math.floor(Math.random() * customPhrases.length); + setCurrentLoadingPhrase(customPhrases[randomIndex]); + } else { + // Roughly 1 in 6 chance to show a tip. + const showTip = Math.random() < 1 / 6; + const phraseList = showTip ? INFORMATIVE_TIPS : WITTY_LOADING_PHRASES; + const randomIndex = Math.floor(Math.random() * phraseList.length); + setCurrentLoadingPhrase(phraseList[randomIndex]); + } + }; + // Select an initial random phrase - const initialRandomIndex = Math.floor( - Math.random() * loadingPhrases.length, - ); - setCurrentLoadingPhrase(loadingPhrases[initialRandomIndex]); + setRandomPhrase(); phraseIntervalRef.current = setInterval(() => { // Select a new random phrase - const randomIndex = Math.floor(Math.random() * loadingPhrases.length); - setCurrentLoadingPhrase(loadingPhrases[randomIndex]); + setRandomPhrase(); }, PHRASE_CHANGE_INTERVAL_MS); } else { // Idle or other states, clear the phrase interval @@ -200,7 +352,7 @@ export const usePhraseCycler = ( phraseIntervalRef.current = null; } }; - }, [isActive, isWaiting, loadingPhrases]); + }, [isActive, isWaiting, customPhrases, loadingPhrases]); return currentLoadingPhrase; };