mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
feat(cli): implement customizable witty phrase positioning
- Add ui.wittyPhrasePosition setting (status, inline, ambient) - Refactor usePhraseCycler to return tips and wit separately - Implement 'inline' position: append witty phrases in gray after status - Update status length estimation to account for inline wit - Replace pause icon with up arrow (↑) for awaiting approval - Remove 'Tip:' prefix from loading phrases - Update unit tests and research report
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<Box flexDirection="row" justifyContent="flex-end" marginLeft={1}>
|
||||
<Text color={theme.text.secondary} wrap="truncate-end">
|
||||
{ambientPrefix}
|
||||
{ambientText}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -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 (
|
||||
<Text color={theme.status.warning}>⏸ Awaiting user approval...</Text>
|
||||
);
|
||||
return <Text color={theme.status.warning}>↑ Awaiting approval</Text>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -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<LoadingIndicatorProps> = ({
|
||||
currentLoadingPhrase,
|
||||
wittyPhrase,
|
||||
wittyPosition = 'inline',
|
||||
elapsedTime,
|
||||
inline = false,
|
||||
rightContent,
|
||||
@@ -57,9 +61,11 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
: 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<LoadingIndicatorProps> = ({
|
||||
? `esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)}`
|
||||
: null;
|
||||
|
||||
const wittyPhraseNode =
|
||||
forceRealStatusOnly &&
|
||||
wittyPosition === 'inline' &&
|
||||
wittyPhrase &&
|
||||
primaryText ? (
|
||||
<Box marginLeft={1}>
|
||||
<Text color={theme.text.secondary}>{wittyPhrase}</Text>
|
||||
</Box>
|
||||
) : null;
|
||||
|
||||
if (inline) {
|
||||
return (
|
||||
<Box>
|
||||
@@ -91,6 +107,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
{primaryText}
|
||||
</Text>
|
||||
)}
|
||||
{wittyPhraseNode}
|
||||
{cancelAndTimerContent && (
|
||||
<>
|
||||
<Box flexShrink={0} width={1} />
|
||||
@@ -129,6 +146,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
{primaryText}
|
||||
</Text>
|
||||
)}
|
||||
{wittyPhraseNode}
|
||||
{!isNarrow && cancelAndTimerContent && (
|
||||
<>
|
||||
<Box flexShrink={0} width={1} />
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -30,14 +30,14 @@ const TestComponent = ({
|
||||
loadingPhrasesMode?: LoadingPhrasesMode;
|
||||
customPhrases?: string[];
|
||||
}) => {
|
||||
const phrase = usePhraseCycler(
|
||||
const { currentTip, currentWittyPhrase } = usePhraseCycler(
|
||||
isActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting,
|
||||
loadingPhrasesMode,
|
||||
customPhrases,
|
||||
);
|
||||
return <Text>{phrase}</Text>;
|
||||
return <Text>{currentTip || currentWittyPhrase}</Text>;
|
||||
};
|
||||
|
||||
describe('usePhraseCycler', () => {
|
||||
|
||||
@@ -31,7 +31,8 @@ export const usePhraseCycler = (
|
||||
customPhrases?: string[],
|
||||
maxLength?: number,
|
||||
) => {
|
||||
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState<
|
||||
const [currentTip, setCurrentTip] = useState<string | undefined>(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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user