Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/ui/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,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 () => {
Expand All @@ -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');
expect(lastFrame()).not.toContain('Select Theme');
});
});
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/ui/hooks/useLoadingIndicator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/ui/hooks/useLoadingIndicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,6 +23,7 @@ export const useLoadingIndicator = (streamingState: StreamingState) => {
const currentLoadingPhrase = usePhraseCycler(
isPhraseCyclingActive,
isWaiting,
customWittyPhrases,
);

const [retainedElapsedTime, setRetainedElapsedTime] = useState(0);
Expand Down
59 changes: 55 additions & 4 deletions packages/cli/src/ui/hooks/usePhraseCycler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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),
Expand Down
27 changes: 17 additions & 10 deletions packages/cli/src/ui/hooks/usePhraseCycler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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
Expand All @@ -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 () => {
Expand All @@ -192,7 +199,7 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
phraseIntervalRef.current = null;
}
};
}, [isActive, isWaiting]);
}, [isActive, isWaiting, loadingPhrases]);

return currentLoadingPhrase;
};