Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -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 () => {
Expand All @@ -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');
});
});
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;
};
Loading