mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(cli): custom witty message (#7641)
This commit is contained in:
@@ -277,6 +277,15 @@ export const SETTINGS_SCHEMA = {
|
|||||||
description: 'Show citations for generated text in the chat.',
|
description: 'Show citations for generated text in the chat.',
|
||||||
showInDialog: true,
|
showInDialog: true,
|
||||||
},
|
},
|
||||||
|
customWittyPhrases: {
|
||||||
|
type: 'array',
|
||||||
|
label: 'Custom Witty Phrases',
|
||||||
|
category: 'UI',
|
||||||
|
requiresRestart: false,
|
||||||
|
default: [] as string[],
|
||||||
|
description: 'Custom witty phrases to display during loading.',
|
||||||
|
showInDialog: false,
|
||||||
|
},
|
||||||
accessibility: {
|
accessibility: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
label: 'Accessibility',
|
label: 'Accessibility',
|
||||||
|
|||||||
@@ -1055,7 +1055,7 @@ describe('App UI', () => {
|
|||||||
);
|
);
|
||||||
currentUnmount = unmount;
|
currentUnmount = unmount;
|
||||||
|
|
||||||
expect(lastFrame()).toContain("I'm Feeling Lucky (esc to cancel");
|
expect(lastFrame()).toContain('(esc to cancel');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a message if NO_COLOR is set', async () => {
|
it('should display a message if NO_COLOR is set', async () => {
|
||||||
@@ -1070,7 +1070,7 @@ describe('App UI', () => {
|
|||||||
);
|
);
|
||||||
currentUnmount = unmount;
|
currentUnmount = unmount;
|
||||||
|
|
||||||
expect(lastFrame()).toContain("I'm Feeling Lucky (esc to cancel");
|
expect(lastFrame()).toContain('(esc to cancel');
|
||||||
expect(lastFrame()).not.toContain('Select Theme');
|
expect(lastFrame()).not.toContain('Select Theme');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -737,8 +737,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
|
|
||||||
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
|
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
|
||||||
|
|
||||||
const { elapsedTime, currentLoadingPhrase } =
|
const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(
|
||||||
useLoadingIndicator(streamingState);
|
streamingState,
|
||||||
|
settings.merged.ui?.customWittyPhrases,
|
||||||
|
);
|
||||||
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config, addItem });
|
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config, addItem });
|
||||||
|
|
||||||
const handleExit = useCallback(
|
const handleExit = useCallback(
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ describe('useLoadingIndicator', () => {
|
|||||||
useLoadingIndicator(StreamingState.Idle),
|
useLoadingIndicator(StreamingState.Idle),
|
||||||
);
|
);
|
||||||
expect(result.current.elapsedTime).toBe(0);
|
expect(result.current.elapsedTime).toBe(0);
|
||||||
expect(result.current.currentLoadingPhrase).toBe(WITTY_LOADING_PHRASES[0]);
|
expect(WITTY_LOADING_PHRASES).toContain(
|
||||||
|
result.current.currentLoadingPhrase,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reflect values when Responding', async () => {
|
it('should reflect values when Responding', async () => {
|
||||||
@@ -128,7 +130,9 @@ describe('useLoadingIndicator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.elapsedTime).toBe(0);
|
expect(result.current.elapsedTime).toBe(0);
|
||||||
expect(result.current.currentLoadingPhrase).toBe(WITTY_LOADING_PHRASES[0]);
|
expect(WITTY_LOADING_PHRASES).toContain(
|
||||||
|
result.current.currentLoadingPhrase,
|
||||||
|
);
|
||||||
|
|
||||||
// Timer should not advance
|
// Timer should not advance
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import { useTimer } from './useTimer.js';
|
|||||||
import { usePhraseCycler } from './usePhraseCycler.js';
|
import { usePhraseCycler } from './usePhraseCycler.js';
|
||||||
import { useState, useEffect, useRef } from 'react'; // Added useRef
|
import { useState, useEffect, useRef } from 'react'; // Added useRef
|
||||||
|
|
||||||
export const useLoadingIndicator = (streamingState: StreamingState) => {
|
export const useLoadingIndicator = (
|
||||||
|
streamingState: StreamingState,
|
||||||
|
customWittyPhrases?: string[],
|
||||||
|
) => {
|
||||||
const [timerResetKey, setTimerResetKey] = useState(0);
|
const [timerResetKey, setTimerResetKey] = useState(0);
|
||||||
const isTimerActive = streamingState === StreamingState.Responding;
|
const isTimerActive = streamingState === StreamingState.Responding;
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ export const useLoadingIndicator = (streamingState: StreamingState) => {
|
|||||||
const currentLoadingPhrase = usePhraseCycler(
|
const currentLoadingPhrase = usePhraseCycler(
|
||||||
isPhraseCyclingActive,
|
isPhraseCyclingActive,
|
||||||
isWaiting,
|
isWaiting,
|
||||||
|
customWittyPhrases,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [retainedElapsedTime, setRetainedElapsedTime] = useState(0);
|
const [retainedElapsedTime, setRetainedElapsedTime] = useState(0);
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ describe('usePhraseCycler', () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize with the first witty phrase when not active and not waiting', () => {
|
it('should initialize with a witty phrase when not active and not waiting', () => {
|
||||||
const { result } = renderHook(() => usePhraseCycler(false, false));
|
const { result } = renderHook(() => usePhraseCycler(false, false));
|
||||||
expect(result.current).toBe(WITTY_LOADING_PHRASES[0]);
|
expect(WITTY_LOADING_PHRASES).toContain(result.current);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show "Waiting for user confirmation..." when isWaiting is true', () => {
|
it('should show "Waiting for user confirmation..." when isWaiting is true', () => {
|
||||||
@@ -37,10 +37,11 @@ describe('usePhraseCycler', () => {
|
|||||||
|
|
||||||
it('should not cycle phrases if isActive is false and not waiting', () => {
|
it('should not cycle phrases if isActive is false and not waiting', () => {
|
||||||
const { result } = renderHook(() => usePhraseCycler(false, false));
|
const { result } = renderHook(() => usePhraseCycler(false, false));
|
||||||
|
const initialPhrase = result.current;
|
||||||
act(() => {
|
act(() => {
|
||||||
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS * 2);
|
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS * 2);
|
||||||
});
|
});
|
||||||
expect(result.current).toBe(WITTY_LOADING_PHRASES[0]);
|
expect(result.current).toBe(initialPhrase);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should cycle through witty phrases when isActive is true and not waiting', () => {
|
it('should cycle through witty phrases when isActive is true and not waiting', () => {
|
||||||
@@ -99,7 +100,7 @@ describe('usePhraseCycler', () => {
|
|||||||
|
|
||||||
// Set to inactive - should reset to the default initial phrase
|
// Set to inactive - should reset to the default initial phrase
|
||||||
rerender({ isActive: false, isWaiting: false });
|
rerender({ isActive: false, isWaiting: false });
|
||||||
expect(result.current).toBe(WITTY_LOADING_PHRASES[0]);
|
expect(WITTY_LOADING_PHRASES).toContain(result.current);
|
||||||
|
|
||||||
// Set back to active - should pick a random witty phrase (which our mock controls)
|
// Set back to active - should pick a random witty phrase (which our mock controls)
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -116,6 +117,56 @@ describe('usePhraseCycler', () => {
|
|||||||
expect(clearIntervalSpy).toHaveBeenCalledOnce();
|
expect(clearIntervalSpy).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use custom phrases when provided', () => {
|
||||||
|
const customPhrases = ['Custom Phrase 1', 'Custom Phrase 2'];
|
||||||
|
let callCount = 0;
|
||||||
|
vi.spyOn(Math, 'random').mockImplementation(() => {
|
||||||
|
const val = callCount % 2;
|
||||||
|
callCount++;
|
||||||
|
return val / customPhrases.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
({ isActive, isWaiting, customPhrases: phrases }) =>
|
||||||
|
usePhraseCycler(isActive, isWaiting, phrases),
|
||||||
|
{
|
||||||
|
initialProps: {
|
||||||
|
isActive: true,
|
||||||
|
isWaiting: false,
|
||||||
|
customPhrases,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current).toBe(customPhrases[0]);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current).toBe(customPhrases[1]);
|
||||||
|
|
||||||
|
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', () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
({ isActive, isWaiting, customPhrases: phrases }) =>
|
||||||
|
usePhraseCycler(isActive, isWaiting, phrases),
|
||||||
|
{
|
||||||
|
initialProps: {
|
||||||
|
isActive: true,
|
||||||
|
isWaiting: false,
|
||||||
|
customPhrases: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(WITTY_LOADING_PHRASES).toContain(result.current);
|
||||||
|
});
|
||||||
|
|
||||||
it('should reset to a witty phrase when transitioning from waiting to active', () => {
|
it('should reset to a witty phrase when transitioning from waiting to active', () => {
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
({ isActive, isWaiting }) => usePhraseCycler(isActive, isWaiting),
|
({ isActive, isWaiting }) => usePhraseCycler(isActive, isWaiting),
|
||||||
|
|||||||
@@ -146,9 +146,18 @@ export const PHRASE_CHANGE_INTERVAL_MS = 15000;
|
|||||||
* @param isWaiting Whether to show a specific waiting phrase.
|
* @param isWaiting Whether to show a specific waiting phrase.
|
||||||
* @returns The current loading phrase.
|
* @returns The current loading phrase.
|
||||||
*/
|
*/
|
||||||
export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
|
export const usePhraseCycler = (
|
||||||
|
isActive: boolean,
|
||||||
|
isWaiting: boolean,
|
||||||
|
customPhrases?: string[],
|
||||||
|
) => {
|
||||||
|
const loadingPhrases =
|
||||||
|
customPhrases && customPhrases.length > 0
|
||||||
|
? customPhrases
|
||||||
|
: WITTY_LOADING_PHRASES;
|
||||||
|
|
||||||
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState(
|
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState(
|
||||||
WITTY_LOADING_PHRASES[0],
|
loadingPhrases[0],
|
||||||
);
|
);
|
||||||
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
@@ -165,16 +174,14 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
|
|||||||
}
|
}
|
||||||
// Select an initial random phrase
|
// Select an initial random phrase
|
||||||
const initialRandomIndex = Math.floor(
|
const initialRandomIndex = Math.floor(
|
||||||
Math.random() * WITTY_LOADING_PHRASES.length,
|
Math.random() * loadingPhrases.length,
|
||||||
);
|
);
|
||||||
setCurrentLoadingPhrase(WITTY_LOADING_PHRASES[initialRandomIndex]);
|
setCurrentLoadingPhrase(loadingPhrases[initialRandomIndex]);
|
||||||
|
|
||||||
phraseIntervalRef.current = setInterval(() => {
|
phraseIntervalRef.current = setInterval(() => {
|
||||||
// Select a new random phrase
|
// Select a new random phrase
|
||||||
const randomIndex = Math.floor(
|
const randomIndex = Math.floor(Math.random() * loadingPhrases.length);
|
||||||
Math.random() * WITTY_LOADING_PHRASES.length,
|
setCurrentLoadingPhrase(loadingPhrases[randomIndex]);
|
||||||
);
|
|
||||||
setCurrentLoadingPhrase(WITTY_LOADING_PHRASES[randomIndex]);
|
|
||||||
}, PHRASE_CHANGE_INTERVAL_MS);
|
}, PHRASE_CHANGE_INTERVAL_MS);
|
||||||
} else {
|
} else {
|
||||||
// Idle or other states, clear the phrase interval
|
// Idle or other states, clear the phrase interval
|
||||||
@@ -183,7 +190,7 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
|
|||||||
clearInterval(phraseIntervalRef.current);
|
clearInterval(phraseIntervalRef.current);
|
||||||
phraseIntervalRef.current = null;
|
phraseIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
setCurrentLoadingPhrase(WITTY_LOADING_PHRASES[0]);
|
setCurrentLoadingPhrase(loadingPhrases[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -192,7 +199,7 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
|
|||||||
phraseIntervalRef.current = null;
|
phraseIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isActive, isWaiting]);
|
}, [isActive, isWaiting, loadingPhrases]);
|
||||||
|
|
||||||
return currentLoadingPhrase;
|
return currentLoadingPhrase;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user