Skip to content

Commit 155242a

Browse files
feat: Blend educative tips with witty phrases during loading times (fun, subtle learning...) (#10569)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
1 parent c96fd82 commit 155242a

File tree

3 files changed

+185
-13
lines changed

3 files changed

+185
-13
lines changed

packages/cli/src/ui/hooks/useLoadingIndicator.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ describe('useLoadingIndicator', () => {
2121
afterEach(() => {
2222
vi.useRealTimers(); // Restore real timers after each test
2323
act(() => vi.runOnlyPendingTimers);
24+
vi.restoreAllMocks();
2425
});
2526

2627
it('should initialize with default values when Idle', () => {
28+
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
2729
const { result } = renderHook(() =>
2830
useLoadingIndicator(StreamingState.Idle),
2931
);
@@ -34,6 +36,7 @@ describe('useLoadingIndicator', () => {
3436
});
3537

3638
it('should reflect values when Responding', async () => {
39+
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
3740
const { result } = renderHook(() =>
3841
useLoadingIndicator(StreamingState.Responding),
3942
);
@@ -82,6 +85,7 @@ describe('useLoadingIndicator', () => {
8285
});
8386

8487
it('should reset elapsedTime and use a witty phrase when transitioning from WaitingForConfirmation to Responding', async () => {
88+
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
8589
const { result, rerender } = renderHook(
8690
({ streamingState }) => useLoadingIndicator(streamingState),
8791
{ initialProps: { streamingState: StreamingState.Responding } },
@@ -115,6 +119,7 @@ describe('useLoadingIndicator', () => {
115119
});
116120

117121
it('should reset timer and phrase when streamingState changes from Responding to Idle', async () => {
122+
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
118123
const { result, rerender } = renderHook(
119124
({ streamingState }) => useLoadingIndicator(streamingState),
120125
{ initialProps: { streamingState: StreamingState.Responding } },

packages/cli/src/ui/hooks/usePhraseCycler.test.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe('usePhraseCycler', () => {
2222
});
2323

2424
it('should initialize with a witty phrase when not active and not waiting', () => {
25+
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
2526
const { result } = renderHook(() => usePhraseCycler(false, false));
2627
expect(WITTY_LOADING_PHRASES).toContain(result.current);
2728
});
@@ -45,6 +46,7 @@ describe('usePhraseCycler', () => {
4546
});
4647

4748
it('should cycle through witty phrases when isActive is true and not waiting', () => {
49+
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
4850
const { result } = renderHook(() => usePhraseCycler(true, false));
4951
// Initial phrase should be one of the witty phrases
5052
expect(WITTY_LOADING_PHRASES).toContain(result.current);
@@ -70,12 +72,19 @@ describe('usePhraseCycler', () => {
7072
}
7173

7274
// Mock Math.random to make the test deterministic.
73-
let callCount = 0;
75+
const mockRandomValues = [
76+
0.5, // -> witty
77+
0, // -> index 0
78+
0.5, // -> witty
79+
1 / WITTY_LOADING_PHRASES.length, // -> index 1
80+
0.5, // -> witty
81+
0, // -> index 0
82+
];
83+
let randomCallCount = 0;
7484
vi.spyOn(Math, 'random').mockImplementation(() => {
75-
// Cycle through 0, 1, 0, 1, ...
76-
const val = callCount % 2;
77-
callCount++;
78-
return val / WITTY_LOADING_PHRASES.length;
85+
const val = mockRandomValues[randomCallCount % mockRandomValues.length];
86+
randomCallCount++;
87+
return val;
7988
});
8089

8190
const { result, rerender } = renderHook(
@@ -120,7 +129,7 @@ describe('usePhraseCycler', () => {
120129
it('should use custom phrases when provided', () => {
121130
const customPhrases = ['Custom Phrase 1', 'Custom Phrase 2'];
122131
let callCount = 0;
123-
vi.spyOn(Math, 'random').mockImplementation(() => {
132+
const randomMock = vi.spyOn(Math, 'random').mockImplementation(() => {
124133
const val = callCount % 2;
125134
callCount++;
126135
return val / customPhrases.length;
@@ -146,12 +155,17 @@ describe('usePhraseCycler', () => {
146155

147156
expect(result.current).toBe(customPhrases[1]);
148157

158+
// Test fallback to default phrases.
159+
randomMock.mockRestore();
160+
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
161+
149162
rerender({ isActive: true, isWaiting: false, customPhrases: undefined });
150163

151164
expect(WITTY_LOADING_PHRASES).toContain(result.current);
152165
});
153166

154167
it('should fall back to witty phrases if custom phrases are an empty array', () => {
168+
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
155169
const { result } = renderHook(
156170
({ isActive, isWaiting, customPhrases: phrases }) =>
157171
usePhraseCycler(isActive, isWaiting, phrases),
@@ -168,6 +182,7 @@ describe('usePhraseCycler', () => {
168182
});
169183

170184
it('should reset to a witty phrase when transitioning from waiting to active', () => {
185+
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
171186
const { result, rerender } = renderHook(
172187
({ isActive, isWaiting }) => usePhraseCycler(isActive, isWaiting),
173188
{ initialProps: { isActive: true, isWaiting: false } },

packages/cli/src/ui/hooks/usePhraseCycler.ts

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,148 @@ export const WITTY_LOADING_PHRASES = [
139139
'Releasing the HypnoDrones...',
140140
];
141141

142+
export const INFORMATIVE_TIPS = [
143+
//Settings tips start here
144+
'Set your preferred editor for opening files (/settings)...',
145+
'Toggle Vim mode for a modal editing experience (/settings)...',
146+
'Disable automatic updates if you prefer manual control (/settings)...',
147+
'Turn off nagging update notifications (settings.json)...',
148+
'Enable checkpointing to recover your session after a crash (settings.json)...',
149+
'Change CLI output format to JSON for scripting (/settings)...',
150+
'Personalize your CLI with a new color theme (/settings)...',
151+
'Create and use your own custom themes (settings.json)...',
152+
'Hide window title for a more minimal UI (/settings)...',
153+
"Don't like these tips? You can hide them (/settings)...",
154+
'Hide the startup banner for a cleaner launch (/settings)...',
155+
'Reclaim vertical space by hiding the footer (/settings)...',
156+
'Show memory usage for performance monitoring (/settings)...',
157+
'Show citations to see where the model gets information (/settings)...',
158+
'Disable loading phrases for a quieter experience (/settings)...',
159+
'Add custom witty phrases to the loading screen (settings.json)...',
160+
'Choose a specific Gemini model for conversations (/settings)...',
161+
'Limit the number of turns in your session history (/settings)...',
162+
'Automatically summarize large tool outputs to save tokens (settings.json)...',
163+
'Control when chat history gets compressed based on token usage (settings.json)...',
164+
'Define custom context file names, like CONTEXT.md (settings.json)...',
165+
'Set max directories to scan for context files (/settings)...',
166+
'Expand your workspace with additional directories (/directory)...',
167+
'Control how /memory refresh loads context files (/settings)...',
168+
'Toggle respect for .gitignore files in context (/settings)...',
169+
'Toggle respect for .geminiignore files in context (/settings)...',
170+
'Enable recursive file search for @-file completions (/settings)...',
171+
'Run tools in a secure sandbox environment (settings.json)...',
172+
'Use an interactive terminal for shell commands (/settings)...',
173+
'Restrict available built-in tools (settings.json)...',
174+
'Exclude specific tools from being used (settings.json)...',
175+
'Bypass confirmation for trusted tools (settings.json)...',
176+
'Use a custom command for tool discovery (settings.json)...',
177+
'Define a custom command for calling discovered tools (settings.json)...',
178+
'Define and manage connections to MCP servers (settings.json)...',
179+
'Enable folder trust to enhance security (/settings)...',
180+
'Change your authentication method (/settings)...',
181+
'Enforce auth type for enterprise use (settings.json)...',
182+
'Let Node.js auto-configure memory (settings.json)...',
183+
'Customize the DNS resolution order (settings.json)...',
184+
'Exclude env vars from the context (settings.json)...',
185+
'Configure a custom command for filing bug reports (settings.json)...',
186+
'Enable or disable telemetry collection (/settings)...',
187+
'Send telemetry data to a local file or GCP (settings.json)...',
188+
'Configure the OTLP endpoint for telemetry (settings.json)...',
189+
'Choose whether to log prompt content (settings.json)...',
190+
'Enable AI-powered prompt completion while typing (/settings)...',
191+
'Enable debug logging of keystrokes to the console (/settings)...',
192+
'Enable automatic session cleanup of old conversations (/settings)...',
193+
'Show Gemini CLI status in the terminal window title (/settings)...',
194+
'Use the entire width of the terminal for output (/settings)...',
195+
'Enable screen reader mode for better accessibility (/settings)...',
196+
'Skip the next speaker check for faster responses (/settings)...',
197+
'Use ripgrep for faster file content search (/settings)...',
198+
'Enable truncation of large tool outputs to save tokens (/settings)...',
199+
'Set the character threshold for truncating tool outputs (/settings)...',
200+
'Set the number of lines to keep when truncating outputs (/settings)...',
201+
'Enable policy-based tool confirmation via message bus (/settings)...',
202+
'Enable smart-edit tool for more precise editing (/settings)...',
203+
'Enable write_todos_list tool to generate task lists (/settings)...',
204+
'Enable model routing based on complexity (/settings)...',
205+
'Enable experimental subagents for task delegation (/settings)...',
206+
//Settings tips end here
207+
// Keyboard shortcut tips start here
208+
'Close dialogs and suggestions with Esc...',
209+
'Cancel a request with Ctrl+C, or press twice to exit...',
210+
'Exit the app with Ctrl+D on an empty line...',
211+
'Clear your screen at any time with Ctrl+L...',
212+
'Toggle the debug console display with Ctrl+O...',
213+
'See full, untruncated responses with Ctrl+S...',
214+
'Show or hide tool descriptions with Ctrl+T...',
215+
'Toggle auto-approval (YOLO mode) for all tools with Ctrl+Y...',
216+
'Toggle shell mode by typing ! in an empty prompt...',
217+
'Insert a newline with a backslash (\\) followed by Enter...',
218+
'Navigate your prompt history with the Up and Down arrows...',
219+
'You can also use Ctrl+P (up) and Ctrl+N (down) for history...',
220+
'Submit your prompt to Gemini with Enter...',
221+
'Accept an autocomplete suggestion with Tab or Enter...',
222+
'Move to the start of the line with Ctrl+A or Home...',
223+
'Move to the end of the line with Ctrl+E or End...',
224+
'Move one character left or right with Ctrl+B/F or the arrow keys...',
225+
'Move one word left or right with Ctrl+Left/Right Arrow...',
226+
'Delete the character to the left with Ctrl+H or Backspace...',
227+
'Delete the character to the right with Ctrl+D or Delete...',
228+
'Delete the word to the left of the cursor with Ctrl+W...',
229+
'Delete the word to the right of the cursor with Ctrl+Delete...',
230+
'Delete from the cursor to the start of the line with Ctrl+U...',
231+
'Delete from the cursor to the end of the line with Ctrl+K...',
232+
'Clear the entire input prompt with a double-press of Esc...',
233+
'Paste from your clipboard with Ctrl+V...',
234+
'Open the current prompt in an external editor with Ctrl+X...',
235+
'In menus, move up/down with k/j or the arrow keys...',
236+
'In menus, select an item by typing its number...',
237+
"If you're using an IDE, see the context with Ctrl+G...",
238+
// Keyboard shortcut tips end here
239+
// Command tips start here
240+
'Show version info with /about...',
241+
'Change your authentication method with /auth...',
242+
'File a bug report directly with /bug...',
243+
'List your saved chat checkpoints with /chat list...',
244+
'Save your current conversation with /chat save <tag>...',
245+
'Resume a saved conversation with /chat resume <tag>...',
246+
'Delete a conversation checkpoint with /chat delete <tag>...',
247+
'Share your conversation to a file with /chat share <file>...',
248+
'Clear the screen and history with /clear...',
249+
'Save tokens by summarizing the context with /compress...',
250+
'Copy the last response to your clipboard with /copy...',
251+
'Open the full documentation in your browser with /docs...',
252+
'Add directories to your workspace with /directory add <path>...',
253+
'Show all directories in your workspace with /directory show...',
254+
'Set your preferred external editor with /editor...',
255+
'List all active extensions with /extensions list...',
256+
'Update all or specific extensions with /extensions update...',
257+
'Get help on commands with /help...',
258+
'Manage IDE integration with /ide...',
259+
'Create a project-specific GEMINI.md file with /init...',
260+
'List configured MCP servers and tools with /mcp list...',
261+
'Authenticate with an OAuth-enabled MCP server with /mcp auth...',
262+
'Restart MCP servers with /mcp refresh...',
263+
'See the current instructional context with /memory show...',
264+
'Add content to the instructional memory with /memory add...',
265+
'Reload instructional context from GEMINI.md files with /memory refresh...',
266+
'List the paths of the GEMINI.md files in use with /memory list...',
267+
'Display the privacy notice with /privacy...',
268+
'Exit the CLI with /quit or /exit...',
269+
'Check model-specific usage stats with /stats model...',
270+
'Check tool-specific usage stats with /stats tools...',
271+
"Change the CLI's color theme with /theme...",
272+
'List all available tools with /tools...',
273+
'View and edit settings with the /settings editor...',
274+
'Toggle Vim keybindings on and off with /vim...',
275+
'Set up GitHub Actions with /setup-github...',
276+
'Configure terminal keybindings for multiline input with /terminal-setup...',
277+
'Find relevant documentation with /find-docs...',
278+
'Review a pull request with /oncall:pr-review...',
279+
'Go back to main and clean up the branch with /github:cleanup-back-to-main...',
280+
'Execute any shell command with !<command>...',
281+
// Command tips end here
282+
];
283+
142284
export const PHRASE_CHANGE_INTERVAL_MS = 15000;
143285

144286
/**
@@ -173,16 +315,26 @@ export const usePhraseCycler = (
173315
if (phraseIntervalRef.current) {
174316
clearInterval(phraseIntervalRef.current);
175317
}
318+
319+
const setRandomPhrase = () => {
320+
if (customPhrases && customPhrases.length > 0) {
321+
const randomIndex = Math.floor(Math.random() * customPhrases.length);
322+
setCurrentLoadingPhrase(customPhrases[randomIndex]);
323+
} else {
324+
// Roughly 1 in 6 chance to show a tip.
325+
const showTip = Math.random() < 1 / 6;
326+
const phraseList = showTip ? INFORMATIVE_TIPS : WITTY_LOADING_PHRASES;
327+
const randomIndex = Math.floor(Math.random() * phraseList.length);
328+
setCurrentLoadingPhrase(phraseList[randomIndex]);
329+
}
330+
};
331+
176332
// Select an initial random phrase
177-
const initialRandomIndex = Math.floor(
178-
Math.random() * loadingPhrases.length,
179-
);
180-
setCurrentLoadingPhrase(loadingPhrases[initialRandomIndex]);
333+
setRandomPhrase();
181334

182335
phraseIntervalRef.current = setInterval(() => {
183336
// Select a new random phrase
184-
const randomIndex = Math.floor(Math.random() * loadingPhrases.length);
185-
setCurrentLoadingPhrase(loadingPhrases[randomIndex]);
337+
setRandomPhrase();
186338
}, PHRASE_CHANGE_INTERVAL_MS);
187339
} else {
188340
// Idle or other states, clear the phrase interval
@@ -200,7 +352,7 @@ export const usePhraseCycler = (
200352
phraseIntervalRef.current = null;
201353
}
202354
};
203-
}, [isActive, isWaiting, loadingPhrases]);
355+
}, [isActive, isWaiting, customPhrases, loadingPhrases]);
204356

205357
return currentLoadingPhrase;
206358
};

0 commit comments

Comments
 (0)