diff --git a/_footer-ui.md b/_footer-ui.md
index db3eed6b50..40d685adf6 100644
--- a/_footer-ui.md
+++ b/_footer-ui.md
@@ -730,6 +730,18 @@ better discoverability of features:
wit based on the available terminal width, ensuring that only phrases that fit
without colliding with the system status are selected.
+### 4. Witty Phrase Positioning
+
+A new setting `ui.wittyPhrasePosition` allows controlling where entertainment
+phrases are displayed:
+
+- **`status`**: Replaces the status text when the model is thinking but hasn't
+ emitted a specific thought yet.
+- **`inline` (Default)**: Appends the witty phrase in gray immediately following
+ the real system status (e.g., `⠏ Searching... Loading wit.exe`).
+- **`ambient`**: Displays witty phrases on the far right, interspersed with
+ tips.
+
---
## 12. Testing Summary & Final Feedback
@@ -748,6 +760,7 @@ review against the updated specification.
- **Toasts:** Claims 100% width, left-aligned, prominent warning color.
Overrides ambient tips.
- **Hooks:** Uses `↪` (Before) / `↩` (After) icons. Text is white and italic.
+- **Witty Phrases:** Default to `inline` position (gray text after status).
- **Responsive:**
- Tips/Wit disappear on narrow windows or if they collide with long statuses.
- Status text wraps onto multiple lines only when the window is narrow.
diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index d1c7992c41..574017c217 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -633,6 +633,20 @@ const SETTINGS_SCHEMA = {
{ value: 'new_divider_down', label: 'New Layout (Divider Down)' },
],
},
+ wittyPhrasePosition: {
+ type: 'enum',
+ label: 'Witty Phrase Position',
+ category: 'UI',
+ requiresRestart: false,
+ default: 'inline',
+ description: 'Where to show witty phrases while waiting.',
+ showInDialog: true,
+ options: [
+ { value: 'status', label: 'Status' },
+ { value: 'inline', label: 'Inline (after status)' },
+ { value: 'ambient', label: 'Ambient (at right)' },
+ ],
+ },
showMemoryUsage: {
type: 'boolean',
label: 'Show Memory Usage',
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 268a0f0295..637a7a8309 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -2097,15 +2097,16 @@ Logging in with Google... Restarting Gemini CLI to continue.
? terminalWidth - estimatedStatusLength - 5
: undefined;
- const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator({
- streamingState,
- shouldShowFocusHint,
- retryStatus,
- loadingPhrasesMode: settings.merged.ui.loadingPhrases,
- customWittyPhrases: settings.merged.ui.customWittyPhrases,
- errorVerbosity: settings.merged.ui.errorVerbosity,
- maxLength,
- });
+ const { elapsedTime, currentLoadingPhrase, currentTip, currentWittyPhrase } =
+ useLoadingIndicator({
+ streamingState,
+ shouldShowFocusHint,
+ retryStatus,
+ loadingPhrasesMode: settings.merged.ui.loadingPhrases,
+ customWittyPhrases: settings.merged.ui.customWittyPhrases,
+ errorVerbosity: settings.merged.ui.errorVerbosity,
+ maxLength,
+ });
const allowPlanMode =
config.isPlanEnabled() &&
@@ -2304,6 +2305,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
isFocused,
elapsedTime,
currentLoadingPhrase,
+ currentTip,
+ currentWittyPhrase,
historyRemountKey,
activeHooks,
messageQueue,
@@ -2434,6 +2437,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
isFocused,
elapsedTime,
currentLoadingPhrase,
+ currentTip,
+ currentWittyPhrase,
historyRemountKey,
activeHooks,
messageQueue,
diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx
index acbac114d5..6ebca992d9 100644
--- a/packages/cli/src/ui/components/Composer.tsx
+++ b/packages/cli/src/ui/components/Composer.tsx
@@ -59,6 +59,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const isAlternateBuffer = useAlternateBuffer();
const { showApprovalModeIndicator } = uiState;
const newLayoutSetting = settings.merged.ui.newFooterLayout;
+ const wittyPosition = settings.merged.ui.wittyPhrasePosition;
const isExperimentalLayout = newLayoutSetting !== 'legacy';
const showUiDetails = uiState.cleanUiDetailsVisible;
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
@@ -197,15 +198,8 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const ambientText = isInteractiveShellWaiting
? undefined
- : uiState.currentLoadingPhrase;
-
- // Wit often ends with an ellipsis or similar, tips usually don't.
- const isAmbientTip =
- ambientText &&
- !ambientText.includes('…') &&
- !ambientText.includes('...') &&
- !ambientText.includes('feeling lucky');
- const ambientPrefix = isAmbientTip ? 'Tip: ' : '';
+ : uiState.currentTip ||
+ (wittyPosition === 'ambient' ? uiState.currentWittyPhrase : undefined);
let estimatedStatusLength = 0;
if (
@@ -225,13 +219,16 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
estimatedStatusLength = hookLabel.length + hookNames.length + 10; // +10 for spinner and spacing
} else if (showLoadingIndicator) {
const thoughtText = uiState.thought?.subject || 'Waiting for model...';
- estimatedStatusLength = thoughtText.length + 25; // Spinner(3) + timer(15) + padding
+ const inlineWittyLength =
+ wittyPosition === 'inline' && uiState.currentWittyPhrase
+ ? uiState.currentWittyPhrase.length + 1
+ : 0;
+ estimatedStatusLength = thoughtText.length + 25 + inlineWittyLength; // Spinner(3) + timer(15) + padding + witty
} else if (hasPendingActionRequired) {
- estimatedStatusLength = 35; // "⏸ Awaiting user approval..."
+ estimatedStatusLength = 25; // "↑ Awaiting approval"
}
- const estimatedAmbientLength =
- ambientPrefix.length + (ambientText?.length || 0);
+ const estimatedAmbientLength = ambientText?.length || 0;
const willCollideAmbient =
estimatedStatusLength + estimatedAmbientLength + 5 > terminalWidth;
const willCollideShortcuts = estimatedStatusLength + 45 > terminalWidth; // Assume worst-case shortcut hint is 45 chars
@@ -263,7 +260,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
return (
- {ambientPrefix}
{ambientText}
@@ -322,13 +318,13 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
elapsedTime={uiState.elapsedTime}
forceRealStatusOnly={isExperimentalLayout}
showCancelAndTimer={!isExperimentalLayout}
+ wittyPhrase={uiState.currentWittyPhrase}
+ wittyPosition={wittyPosition}
/>
);
}
if (hasPendingActionRequired) {
- return (
- ⏸ Awaiting user approval...
- );
+ return ↑ Awaiting approval;
}
return null;
};
diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx
index 94bdd23ec3..eda956fd03 100644
--- a/packages/cli/src/ui/components/LoadingIndicator.tsx
+++ b/packages/cli/src/ui/components/LoadingIndicator.tsx
@@ -18,6 +18,8 @@ import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
interface LoadingIndicatorProps {
currentLoadingPhrase?: string;
+ wittyPhrase?: string;
+ wittyPosition?: 'status' | 'inline' | 'ambient';
elapsedTime: number;
inline?: boolean;
rightContent?: React.ReactNode;
@@ -29,6 +31,8 @@ interface LoadingIndicatorProps {
export const LoadingIndicator: React.FC = ({
currentLoadingPhrase,
+ wittyPhrase,
+ wittyPosition = 'inline',
elapsedTime,
inline = false,
rightContent,
@@ -57,9 +61,11 @@ export const LoadingIndicator: React.FC = ({
: thought?.subject
? (thoughtLabel ?? thought.subject)
: forceRealStatusOnly
- ? streamingState === StreamingState.Responding
- ? 'Waiting for model...'
- : undefined
+ ? wittyPosition === 'status' && wittyPhrase
+ ? wittyPhrase
+ : streamingState === StreamingState.Responding
+ ? 'Waiting for model...'
+ : undefined
: currentLoadingPhrase;
const thinkingIndicator = '';
@@ -69,6 +75,16 @@ export const LoadingIndicator: React.FC = ({
? `esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)}`
: null;
+ const wittyPhraseNode =
+ forceRealStatusOnly &&
+ wittyPosition === 'inline' &&
+ wittyPhrase &&
+ primaryText ? (
+
+ {wittyPhrase}
+
+ ) : null;
+
if (inline) {
return (
@@ -91,6 +107,7 @@ export const LoadingIndicator: React.FC = ({
{primaryText}
)}
+ {wittyPhraseNode}
{cancelAndTimerContent && (
<>
@@ -129,6 +146,7 @@ export const LoadingIndicator: React.FC = ({
{primaryText}
)}
+ {wittyPhraseNode}
{!isNarrow && cancelAndTimerContent && (
<>
diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx
index 554cff34f9..ab54dba9f7 100644
--- a/packages/cli/src/ui/contexts/UIStateContext.tsx
+++ b/packages/cli/src/ui/contexts/UIStateContext.tsx
@@ -170,6 +170,8 @@ export interface UIState {
cleanUiDetailsVisible: boolean;
elapsedTime: number;
currentLoadingPhrase: string | undefined;
+ currentTip: string | undefined;
+ currentWittyPhrase: string | undefined;
historyRemountKey: number;
activeHooks: ActiveHook[];
messageQueue: string[];
diff --git a/packages/cli/src/ui/hooks/useLoadingIndicator.ts b/packages/cli/src/ui/hooks/useLoadingIndicator.ts
index b04df7ea9a..0d9240738f 100644
--- a/packages/cli/src/ui/hooks/useLoadingIndicator.ts
+++ b/packages/cli/src/ui/hooks/useLoadingIndicator.ts
@@ -42,7 +42,7 @@ export const useLoadingIndicator = ({
const isPhraseCyclingActive = streamingState === StreamingState.Responding;
const isWaiting = streamingState === StreamingState.WaitingForConfirmation;
- const currentLoadingPhrase = usePhraseCycler(
+ const { currentTip, currentWittyPhrase } = usePhraseCycler(
isPhraseCyclingActive,
isWaiting,
shouldShowFocusHint,
@@ -89,6 +89,8 @@ export const useLoadingIndicator = ({
streamingState === StreamingState.WaitingForConfirmation
? retainedElapsedTime
: elapsedTimeFromTimer,
- currentLoadingPhrase: retryPhrase || currentLoadingPhrase,
+ currentLoadingPhrase: retryPhrase || currentTip || currentWittyPhrase,
+ currentTip,
+ currentWittyPhrase,
};
};
diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx
index ca89c623ac..02517f80ec 100644
--- a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx
+++ b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx
@@ -30,14 +30,14 @@ const TestComponent = ({
loadingPhrasesMode?: LoadingPhrasesMode;
customPhrases?: string[];
}) => {
- const phrase = usePhraseCycler(
+ const { currentTip, currentWittyPhrase } = usePhraseCycler(
isActive,
isWaiting,
isInteractiveShellWaiting,
loadingPhrasesMode,
customPhrases,
);
- return {phrase};
+ return {currentTip || currentWittyPhrase};
};
describe('usePhraseCycler', () => {
diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.ts b/packages/cli/src/ui/hooks/usePhraseCycler.ts
index 007844c13a..231b2048e2 100644
--- a/packages/cli/src/ui/hooks/usePhraseCycler.ts
+++ b/packages/cli/src/ui/hooks/usePhraseCycler.ts
@@ -31,7 +31,8 @@ export const usePhraseCycler = (
customPhrases?: string[],
maxLength?: number,
) => {
- const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState<
+ const [currentTip, setCurrentTip] = useState(undefined);
+ const [currentWittyPhrase, setCurrentWittyPhrase] = useState<
string | undefined
>(undefined);
@@ -46,17 +47,20 @@ export const usePhraseCycler = (
}
if (shouldShowFocusHint) {
- setCurrentLoadingPhrase(INTERACTIVE_SHELL_WAITING_PHRASE);
+ setCurrentTip(INTERACTIVE_SHELL_WAITING_PHRASE);
+ setCurrentWittyPhrase(undefined);
return;
}
if (isWaiting) {
- setCurrentLoadingPhrase('Waiting for user confirmation...');
+ setCurrentTip('Waiting for user confirmation...');
+ setCurrentWittyPhrase(undefined);
return;
}
if (!isActive || loadingPhrasesMode === 'off') {
- setCurrentLoadingPhrase(undefined);
+ setCurrentTip(undefined);
+ setCurrentWittyPhrase(undefined);
return;
}
@@ -66,7 +70,6 @@ export const usePhraseCycler = (
: WITTY_LOADING_PHRASES;
const setRandomPhrase = () => {
- let phraseList: readonly string[];
let currentMode = loadingPhrasesMode;
// In 'all' mode, we decide once per phrase cycle what to show
@@ -79,23 +82,12 @@ export const usePhraseCycler = (
}
}
- switch (currentMode) {
- case 'tips':
- phraseList = INFORMATIVE_TIPS;
- break;
- case 'witty':
- phraseList = wittyPhrases;
- break;
- default:
- phraseList = INFORMATIVE_TIPS;
- break;
- }
+ const phraseList =
+ currentMode === 'witty' ? wittyPhrases : INFORMATIVE_TIPS;
// If we have a maxLength, we need to account for potential prefixes.
// Tips are prefixed with "Tip: " in the Composer UI.
- const prefixLength = currentMode === 'tips' ? 5 : 0;
- const adjustedMaxLength =
- maxLength !== undefined ? maxLength - prefixLength : undefined;
+ const adjustedMaxLength = maxLength;
const filteredList =
adjustedMaxLength !== undefined
@@ -104,10 +96,18 @@ export const usePhraseCycler = (
if (filteredList.length > 0) {
const randomIndex = Math.floor(Math.random() * filteredList.length);
- setCurrentLoadingPhrase(filteredList[randomIndex]);
+ const selected = filteredList[randomIndex];
+ if (currentMode === 'witty') {
+ setCurrentWittyPhrase(selected);
+ setCurrentTip(undefined);
+ } else {
+ setCurrentTip(selected);
+ setCurrentWittyPhrase(undefined);
+ }
} else {
// If no phrases fit, try to fallback to a very short list or nothing
- setCurrentLoadingPhrase(undefined);
+ setCurrentTip(undefined);
+ setCurrentWittyPhrase(undefined);
}
};
@@ -134,5 +134,5 @@ export const usePhraseCycler = (
maxLength,
]);
- return currentLoadingPhrase;
+ return { currentTip, currentWittyPhrase };
};