feat(cli): custom witty message (#7641)

This commit is contained in:
JAYADITYA
2025-09-03 22:09:04 +05:30
committed by GitHub
parent 50b5c4303e
commit de53b30e69
7 changed files with 98 additions and 21 deletions

View File

@@ -277,6 +277,15 @@ export const SETTINGS_SCHEMA = {
description: 'Show citations for generated text in the chat.',
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: {
type: 'object',
label: 'Accessibility',

View File

@@ -1055,7 +1055,7 @@ describe('App UI', () => {
);
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 () => {
@@ -1070,7 +1070,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
expect(lastFrame()).toContain("I'm Feeling Lucky (esc to cancel");
expect(lastFrame()).toContain('(esc to cancel');
expect(lastFrame()).not.toContain('Select Theme');
});
});

View File

@@ -737,8 +737,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
const { elapsedTime, currentLoadingPhrase } =
useLoadingIndicator(streamingState);
const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(
streamingState,
settings.merged.ui?.customWittyPhrases,
);
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config, addItem });
const handleExit = useCallback(

View File

@@ -28,7 +28,9 @@ describe('useLoadingIndicator', () => {
useLoadingIndicator(StreamingState.Idle),
);
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 () => {
@@ -128,7 +130,9 @@ describe('useLoadingIndicator', () => {
});
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
await act(async () => {

View File

@@ -9,7 +9,10 @@ import { useTimer } from './useTimer.js';
import { usePhraseCycler } from './usePhraseCycler.js';
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 isTimerActive = streamingState === StreamingState.Responding;
@@ -20,6 +23,7 @@ export const useLoadingIndicator = (streamingState: StreamingState) => {
const currentLoadingPhrase = usePhraseCycler(
isPhraseCyclingActive,
isWaiting,
customWittyPhrases,
);
const [retainedElapsedTime, setRetainedElapsedTime] = useState(0);

View File

@@ -21,9 +21,9 @@ describe('usePhraseCycler', () => {
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));
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', () => {
@@ -37,10 +37,11 @@ describe('usePhraseCycler', () => {
it('should not cycle phrases if isActive is false and not waiting', () => {
const { result } = renderHook(() => usePhraseCycler(false, false));
const initialPhrase = result.current;
act(() => {
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', () => {
@@ -99,7 +100,7 @@ describe('usePhraseCycler', () => {
// Set to inactive - should reset to the default initial phrase
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)
act(() => {
@@ -116,6 +117,56 @@ describe('usePhraseCycler', () => {
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', () => {
const { result, rerender } = renderHook(
({ isActive, isWaiting }) => usePhraseCycler(isActive, isWaiting),

View File

@@ -146,9 +146,18 @@ export const PHRASE_CHANGE_INTERVAL_MS = 15000;
* @param isWaiting Whether to show a specific waiting 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(
WITTY_LOADING_PHRASES[0],
loadingPhrases[0],
);
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
@@ -165,16 +174,14 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
}
// Select an initial random phrase
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(() => {
// Select a new random phrase
const randomIndex = Math.floor(
Math.random() * WITTY_LOADING_PHRASES.length,
);
setCurrentLoadingPhrase(WITTY_LOADING_PHRASES[randomIndex]);
const randomIndex = Math.floor(Math.random() * loadingPhrases.length);
setCurrentLoadingPhrase(loadingPhrases[randomIndex]);
}, PHRASE_CHANGE_INTERVAL_MS);
} else {
// Idle or other states, clear the phrase interval
@@ -183,7 +190,7 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
clearInterval(phraseIntervalRef.current);
phraseIntervalRef.current = null;
}
setCurrentLoadingPhrase(WITTY_LOADING_PHRASES[0]);
setCurrentLoadingPhrase(loadingPhrases[0]);
}
return () => {
@@ -192,7 +199,7 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
phraseIntervalRef.current = null;
}
};
}, [isActive, isWaiting]);
}, [isActive, isWaiting, loadingPhrases]);
return currentLoadingPhrase;
};