From d99625458ffa3ff5f989c2ab31e336e7f25d45a9 Mon Sep 17 00:00:00 2001 From: Keith Guerin Date: Tue, 24 Mar 2026 22:55:10 -0700 Subject: [PATCH] style(ui): refine duration display and documentation strings --- .gemini/commands/strict-development-rules.md | 16 +++--- .gemini/skills/string-reviewer/SKILL.md | 4 +- docs/get-started/gemini-3.md | 5 +- .../ui/components/LoadingIndicator.test.tsx | 49 +++++++++---------- .../src/ui/components/LoadingIndicator.tsx | 13 +++-- .../src/ui/components/triage/TriageIssues.tsx | 4 +- packages/core/src/prompts/snippets.ts | 3 +- 7 files changed, 46 insertions(+), 48 deletions(-) diff --git a/.gemini/commands/strict-development-rules.md b/.gemini/commands/strict-development-rules.md index 3bc00c2717..f1b8364c91 100644 --- a/.gemini/commands/strict-development-rules.md +++ b/.gemini/commands/strict-development-rules.md @@ -136,16 +136,12 @@ Gemini CLI project. ## UI Text & Telemetry -- **Precise Conciseness**: Keep status and error messages under 5 words whenever - possible to prevent "information snowblindness." -- **Telemetry over Etiquette**: Favor status data and raw telemetry over polite - filler. Avoid "Please wait," "I'm sorry," or "We're still on it." -- **Attribution**: Correctly attribute the source of a delay or error: - - Use **Gemini** for generative analysis or thinking latency. - - Use **System** or **API** for infrastructure, quota, or network issues. -- **Tone**: Use a friendly, helpful, and humble tone. Use sentence-style - capitalization. Solitary sentences aren't punctuated. Speak to the user - ("you"). Avoid first-person pronouns ("I" or "we"). +- **UX Writing Standards**: All user-facing strings, error messages, and status + telemetry must adhere to the project's UX writing standards. +- **Reference**: Follow the rules defined in the [string-reviewer skill](../skills/string-reviewer/SKILL.md). +- **Core Goal**: Prioritize **Precise Conciseness** (under 5 words) and + **Telemetry over Etiquette** (raw data over "Please wait"). + ## Code Cleanup diff --git a/.gemini/skills/string-reviewer/SKILL.md b/.gemini/skills/string-reviewer/SKILL.md index f37d83b4ad..6bd8eb84be 100644 --- a/.gemini/skills/string-reviewer/SKILL.md +++ b/.gemini/skills/string-reviewer/SKILL.md @@ -40,7 +40,9 @@ Use this checklist to audit UI strings and AI responses. ### Identity and voice - **Eliminate the "I":** Remove all first-person pronouns (I, me, my, mine). - **Subject attribution:** Refer to the AI as Gemini and the infrastructure as - the - system or the CLI. + the system or the CLI. Use visual cues (the Gemini icon/spinner) for + attribution of active thoughts; avoid redundant 3rd-person narrator + prefixes (e.g., "Gemini is thinking about...") in favor of direct telemetry. - **Active voice:** Ensure the subject (Gemini or the system) is clearly performing the action. - **Ownership rule:** Use the system for execution (doing) and Gemini for diff --git a/docs/get-started/gemini-3.md b/docs/get-started/gemini-3.md index 8e0af1a9ce..d52161d649 100644 --- a/docs/get-started/gemini-3.md +++ b/docs/get-started/gemini-3.md @@ -59,9 +59,8 @@ or fallback to Gemini 2.5 Pro. > [!NOTE] > The **Keep trying** option uses exponential backoff, in which Gemini -> CLI waits longer between each retry, when the system is busy. If the retry -> doesn't happen immediately, please wait a few minutes for the request to -> process. +> CLI waits longer between each retry, when the system is busy. Retry after +> a few minutes if results are not immediate. ### Model selection and routing types diff --git a/packages/cli/src/ui/components/LoadingIndicator.test.tsx b/packages/cli/src/ui/components/LoadingIndicator.test.tsx index 089e371c80..9616cba303 100644 --- a/packages/cli/src/ui/components/LoadingIndicator.test.tsx +++ b/packages/cli/src/ui/components/LoadingIndicator.test.tsx @@ -50,7 +50,7 @@ const renderWithContext = async ( describe('', () => { const defaultProps = { - currentLoadingPhrase: 'Gemini is thinking...', + currentLoadingPhrase: 'Thinking...', elapsedTime: 5, }; @@ -71,7 +71,7 @@ describe('', () => { await waitUntilReady(); const output = lastFrame(); expect(output).toContain('MockRespondingSpinner'); - expect(output).toContain('Gemini is thinking...'); + expect(output).toContain('Thinking...'); expect(output).toContain('(esc to cancel, 5s)'); }); @@ -108,7 +108,7 @@ describe('', () => { it('should display the elapsedTime correctly when Responding', async () => { const props = { - currentLoadingPhrase: 'Gemini is thinking...', + currentLoadingPhrase: 'Thinking...', elapsedTime: 60, }; const { lastFrame, unmount, waitUntilReady } = await renderWithContext( @@ -120,9 +120,9 @@ describe('', () => { unmount(); }); - it('should display the elapsedTime correctly in human-readable format', async () => { + it('should display the elapsedTime correctly in minutes and OMIT seconds for > 1m', async () => { const props = { - currentLoadingPhrase: 'Gemini is thinking...', + currentLoadingPhrase: 'Thinking...', elapsedTime: 125, }; const { lastFrame, unmount, waitUntilReady } = await renderWithContext( @@ -130,7 +130,9 @@ describe('', () => { StreamingState.Responding, ); await waitUntilReady(); - expect(lastFrame()).toContain('(esc to cancel, 2m 5s)'); + const output = lastFrame(); + expect(output).toContain('(esc to cancel, 2m)'); + expect(output).not.toContain('5s'); unmount(); }); @@ -229,7 +231,7 @@ describe('', () => { it('should display fallback phrase if thought is empty', async () => { const props = { thought: null, - currentLoadingPhrase: 'Gemini is thinking...', + currentLoadingPhrase: 'Thinking...', elapsedTime: 5, }; const { lastFrame, unmount, waitUntilReady } = await renderWithContext( @@ -238,7 +240,7 @@ describe('', () => { ); await waitUntilReady(); const output = lastFrame(); - expect(output).toContain('Gemini is thinking...'); + expect(output).toContain('Thinking...'); unmount(); }); @@ -258,15 +260,13 @@ describe('', () => { const output = lastFrame(); expect(output).toBeDefined(); if (output) { - expect(output).toContain( - 'Gemini is thinking about Thinking about something...', - ); - expect(output).not.toContain('and other stuff.'); + expect(output).not.toContain('Gemini is thinking'); + expect(output).toContain('Thinking about something...'); } unmount(); }); - it('should use "Gemini is thinking about" if a subject is provided', async () => { + it('should NOT prepend "Thinking... " if a subject is provided', async () => { const props = { thought: { subject: 'Planning the response...', @@ -280,13 +280,12 @@ describe('', () => { ); await waitUntilReady(); const output = lastFrame(); - expect(output).toContain( - 'Gemini is thinking about Planning the response...', - ); + expect(output).toContain('Planning the response...'); + expect(output).not.toContain('Thinking... '); unmount(); }); - it('should prioritize thought.subject over currentLoadingPhrase using the new Gemini pattern', async () => { + it('should prioritize thought.subject over currentLoadingPhrase', async () => { const props = { thought: { subject: 'This should be displayed', @@ -301,9 +300,7 @@ describe('', () => { ); await waitUntilReady(); const output = lastFrame(); - expect(output).toContain( - 'Gemini is thinking about This should be displayed', - ); + expect(output).toContain('This should be displayed'); expect(output).not.toContain('This should not be displayed'); unmount(); }); @@ -351,7 +348,7 @@ describe('', () => { const output = lastFrame(); // Check for single line output expect(output?.trim().includes('\n')).toBe(false); - expect(output).toContain('Gemini is thinking...'); + expect(output).toContain('Thinking...'); expect(output).toContain('(esc to cancel, 5s)'); expect(output).toContain('Right'); unmount(); @@ -375,7 +372,7 @@ describe('', () => { // 3. Right Content expect(lines).toHaveLength(3); if (lines) { - expect(lines[0]).toContain('Gemini is thinking...'); + expect(lines[0]).toContain('Thinking...'); expect(lines[0]).not.toContain('(esc to cancel, 5s)'); expect(lines[1]).toContain('(esc to cancel, 5s)'); expect(lines[2]).toContain('Right'); @@ -411,7 +408,7 @@ describe('', () => { elapsedTime={5} wittyPhrase="I am witty" showWit={true} - currentLoadingPhrase="Gemini is thinking..." + currentLoadingPhrase="Thinking..." />, StreamingState.Responding, 120, @@ -419,7 +416,7 @@ describe('', () => { await waitUntilReady(); const output = lastFrame(); // Sequence should be: Primary Text -> Cancel/Timer -> Witty Phrase - expect(output).toContain('Gemini is thinking... (esc to cancel, 5s) I am witty'); + expect(output).toContain('Thinking... (esc to cancel, 5s) I am witty'); unmount(); }); @@ -429,7 +426,7 @@ describe('', () => { elapsedTime={5} wittyPhrase="I am witty" showWit={true} - currentLoadingPhrase="Gemini is thinking..." + currentLoadingPhrase="Thinking..." />, StreamingState.Responding, 79, @@ -443,7 +440,7 @@ describe('', () => { // 3. Witty Phrase expect(lines).toHaveLength(3); if (lines) { - expect(lines[0]).toContain('Gemini is thinking...'); + expect(lines[0]).toContain('Thinking...'); expect(lines[1]).toContain('(esc to cancel, 5s)'); expect(lines[2]).toContain('I am witty'); } diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx index 673ab8c998..7777af81d9 100644 --- a/packages/cli/src/ui/components/LoadingIndicator.tsx +++ b/packages/cli/src/ui/components/LoadingIndicator.tsx @@ -11,7 +11,6 @@ import { theme } from '../semantic-colors.js'; import { useStreamingContext } from '../contexts/StreamingContext.js'; import { StreamingState } from '../types.js'; import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js'; -import { formatDuration } from '../utils/formatters.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js'; import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js'; @@ -65,23 +64,27 @@ export const LoadingIndicator: React.FC = ({ currentLoadingPhrase === INTERACTIVE_SHELL_WAITING_PHRASE ? currentLoadingPhrase : thought?.subject - ? `Gemini is thinking about ${thoughtLabel ?? thought.subject.trim()}` + ? (thoughtLabel ?? thought.subject.trim()) : currentLoadingPhrase || (streamingState === StreamingState.Responding - ? 'Gemini is thinking...' + ? 'Thinking...' : undefined); const cancelAndTimerContent = showCancelAndTimer && streamingState !== StreamingState.WaitingForConfirmation - ? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})` + ? `(esc to cancel, ${ + elapsedTime < 60 + ? `${elapsedTime}s` + : `${Math.floor(elapsedTime / 60)}m` + })` : null; const wittyPhraseNode = !forceRealStatusOnly && showWit && wittyPhrase && - (primaryText === 'Thinking...' || primaryText === 'Gemini is thinking...') ? ( + primaryText === 'Thinking...' ? ( {wittyPhrase} diff --git a/packages/cli/src/ui/components/triage/TriageIssues.tsx b/packages/cli/src/ui/components/triage/TriageIssues.tsx index 62c0f50e1c..b19d158569 100644 --- a/packages/cli/src/ui/components/triage/TriageIssues.tsx +++ b/packages/cli/src/ui/components/triage/TriageIssues.tsx @@ -208,8 +208,8 @@ INSTRUCTIONS: 3. If it seems like a legitimate bug or feature request that needs triage by a human, recommend "keep". 4. Provide a brief reason for your recommendation. 5. If recommending "close", provide a polite, professional, and helpful 'suggested_comment' explaining why it's being closed and what the user can do (e.g., provide more logs, follow contributing guidelines). -6. CRITICAL: If the reason for closing is "Non-deterministic model output", you MUST use the following text EXACTLY as the 'suggested_comment': -"Thank you for the report. Model outputs are non-deterministic, and we are unable to troubleshoot isolated quality issues that lack a repeatable test case. We are closing this issue while we continue to work on overall model performance and reliability. If you find a way to consistently reproduce this specific issue, please let us know and we can take another look." +L211- 6. CRITICAL: If the reason for closing is "Non-deterministic model output", you MUST use the following text EXACTLY as the 'suggested_comment': +"Closing: model outputs are non-deterministic. Re-open with a repeatable test case once available." Return a JSON object with: - "recommendation": "close" or "keep" diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index 27c1fa60a1..66f160cb65 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -364,7 +364,8 @@ export function renderOperationalGuidelines( ? 'per-tool explanations.' : 'mechanical tool-use narration (e.g., "I will now call...").' } -- **Concise & Direct:** Adopt a professional, direct, and concise tone suitable for a CLI environment. +- **Precise Conciseness:** Prioritize **Precise Conciseness** (under 5 words) and **Telemetry over Etiquette** (raw data over polite filler like "Please wait"). +- **Attribution:** Use visual cues (the Gemini icon/spinner) for attribution of active thoughts; avoid redundant 3rd-person narrator prefixes (e.g., "Gemini is thinking about...") in favor of direct telemetry. - **Minimal Output:** Aim for fewer than 3 lines of text output (excluding tool use/code generation) per response whenever practical. - **No Chitchat:** Avoid conversational filler, preambles ("Okay, I will now..."), or postambles ("I have finished the changes...") unless they are ${ options.topicUpdateNarration