Skip to content

Commit 1e4c843

Browse files
committed
custom witty message
1 parent 4d07cb7 commit 1e4c843

File tree

7 files changed

+98
-21
lines changed

7 files changed

+98
-21
lines changed

packages/cli/src/config/settingsSchema.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,15 @@ export const SETTINGS_SCHEMA = {
277277
description: 'Show citations for generated text in the chat.',
278278
showInDialog: true,
279279
},
280+
customWittyPhrases: {
281+
type: 'array',
282+
label: 'Custom Witty Phrases',
283+
category: 'UI',
284+
requiresRestart: false,
285+
default: [] as string[],
286+
description: 'Custom witty phrases to display during loading.',
287+
showInDialog: false,
288+
},
280289
accessibility: {
281290
type: 'object',
282291
label: 'Accessibility',

packages/cli/src/ui/App.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,7 @@ describe('App UI', () => {
10401040
);
10411041
currentUnmount = unmount;
10421042

1043-
expect(lastFrame()).toContain("I'm Feeling Lucky (esc to cancel");
1043+
expect(lastFrame()).toContain('(esc to cancel');
10441044
});
10451045

10461046
it('should display a message if NO_COLOR is set', async () => {
@@ -1055,7 +1055,7 @@ describe('App UI', () => {
10551055
);
10561056
currentUnmount = unmount;
10571057

1058-
expect(lastFrame()).toContain("I'm Feeling Lucky (esc to cancel");
1058+
expect(lastFrame()).toContain('(esc to cancel');
10591059
expect(lastFrame()).not.toContain('Select Theme');
10601060
});
10611061
});

packages/cli/src/ui/App.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -737,8 +737,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
737737

738738
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
739739

740-
const { elapsedTime, currentLoadingPhrase } =
741-
useLoadingIndicator(streamingState);
740+
const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(
741+
streamingState,
742+
settings.merged.ui?.customWittyPhrases,
743+
);
742744
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config, addItem });
743745

744746
const handleExit = useCallback(

packages/cli/src/ui/hooks/useLoadingIndicator.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ describe('useLoadingIndicator', () => {
2828
useLoadingIndicator(StreamingState.Idle),
2929
);
3030
expect(result.current.elapsedTime).toBe(0);
31-
expect(result.current.currentLoadingPhrase).toBe(WITTY_LOADING_PHRASES[0]);
31+
expect(WITTY_LOADING_PHRASES).toContain(
32+
result.current.currentLoadingPhrase,
33+
);
3234
});
3335

3436
it('should reflect values when Responding', async () => {
@@ -128,7 +130,9 @@ describe('useLoadingIndicator', () => {
128130
});
129131

130132
expect(result.current.elapsedTime).toBe(0);
131-
expect(result.current.currentLoadingPhrase).toBe(WITTY_LOADING_PHRASES[0]);
133+
expect(WITTY_LOADING_PHRASES).toContain(
134+
result.current.currentLoadingPhrase,
135+
);
132136

133137
// Timer should not advance
134138
await act(async () => {

packages/cli/src/ui/hooks/useLoadingIndicator.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { useTimer } from './useTimer.js';
99
import { usePhraseCycler } from './usePhraseCycler.js';
1010
import { useState, useEffect, useRef } from 'react'; // Added useRef
1111

12-
export const useLoadingIndicator = (streamingState: StreamingState) => {
12+
export const useLoadingIndicator = (
13+
streamingState: StreamingState,
14+
customWittyPhrases?: string[],
15+
) => {
1316
const [timerResetKey, setTimerResetKey] = useState(0);
1417
const isTimerActive = streamingState === StreamingState.Responding;
1518

@@ -20,6 +23,7 @@ export const useLoadingIndicator = (streamingState: StreamingState) => {
2023
const currentLoadingPhrase = usePhraseCycler(
2124
isPhraseCyclingActive,
2225
isWaiting,
26+
customWittyPhrases,
2327
);
2428

2529
const [retainedElapsedTime, setRetainedElapsedTime] = useState(0);

packages/cli/src/ui/hooks/usePhraseCycler.test.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ describe('usePhraseCycler', () => {
2121
vi.restoreAllMocks();
2222
});
2323

24-
it('should initialize with the first witty phrase when not active and not waiting', () => {
24+
it('should initialize with a witty phrase when not active and not waiting', () => {
2525
const { result } = renderHook(() => usePhraseCycler(false, false));
26-
expect(result.current).toBe(WITTY_LOADING_PHRASES[0]);
26+
expect(WITTY_LOADING_PHRASES).toContain(result.current);
2727
});
2828

2929
it('should show "Waiting for user confirmation..." when isWaiting is true', () => {
@@ -37,10 +37,11 @@ describe('usePhraseCycler', () => {
3737

3838
it('should not cycle phrases if isActive is false and not waiting', () => {
3939
const { result } = renderHook(() => usePhraseCycler(false, false));
40+
const initialPhrase = result.current;
4041
act(() => {
4142
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS * 2);
4243
});
43-
expect(result.current).toBe(WITTY_LOADING_PHRASES[0]);
44+
expect(result.current).toBe(initialPhrase);
4445
});
4546

4647
it('should cycle through witty phrases when isActive is true and not waiting', () => {
@@ -99,7 +100,7 @@ describe('usePhraseCycler', () => {
99100

100101
// Set to inactive - should reset to the default initial phrase
101102
rerender({ isActive: false, isWaiting: false });
102-
expect(result.current).toBe(WITTY_LOADING_PHRASES[0]);
103+
expect(WITTY_LOADING_PHRASES).toContain(result.current);
103104

104105
// Set back to active - should pick a random witty phrase (which our mock controls)
105106
act(() => {
@@ -116,6 +117,56 @@ describe('usePhraseCycler', () => {
116117
expect(clearIntervalSpy).toHaveBeenCalledOnce();
117118
});
118119

120+
it('should use custom phrases when provided', () => {
121+
const customPhrases = ['Custom Phrase 1', 'Custom Phrase 2'];
122+
let callCount = 0;
123+
vi.spyOn(Math, 'random').mockImplementation(() => {
124+
const val = callCount % 2;
125+
callCount++;
126+
return val / customPhrases.length;
127+
});
128+
129+
const { result, rerender } = renderHook(
130+
({ isActive, isWaiting, customPhrases: phrases }) =>
131+
usePhraseCycler(isActive, isWaiting, phrases),
132+
{
133+
initialProps: {
134+
isActive: true,
135+
isWaiting: false,
136+
customPhrases,
137+
},
138+
},
139+
);
140+
141+
expect(result.current).toBe(customPhrases[0]);
142+
143+
act(() => {
144+
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS);
145+
});
146+
147+
expect(result.current).toBe(customPhrases[1]);
148+
149+
rerender({ isActive: true, isWaiting: false, customPhrases: undefined });
150+
151+
expect(WITTY_LOADING_PHRASES).toContain(result.current);
152+
});
153+
154+
it('should fall back to witty phrases if custom phrases are an empty array', () => {
155+
const { result } = renderHook(
156+
({ isActive, isWaiting, customPhrases: phrases }) =>
157+
usePhraseCycler(isActive, isWaiting, phrases),
158+
{
159+
initialProps: {
160+
isActive: true,
161+
isWaiting: false,
162+
customPhrases: [],
163+
},
164+
},
165+
);
166+
167+
expect(WITTY_LOADING_PHRASES).toContain(result.current);
168+
});
169+
119170
it('should reset to a witty phrase when transitioning from waiting to active', () => {
120171
const { result, rerender } = renderHook(
121172
({ isActive, isWaiting }) => usePhraseCycler(isActive, isWaiting),

packages/cli/src/ui/hooks/usePhraseCycler.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,18 @@ export const PHRASE_CHANGE_INTERVAL_MS = 15000;
146146
* @param isWaiting Whether to show a specific waiting phrase.
147147
* @returns The current loading phrase.
148148
*/
149-
export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
149+
export const usePhraseCycler = (
150+
isActive: boolean,
151+
isWaiting: boolean,
152+
customPhrases?: string[],
153+
) => {
154+
const loadingPhrases =
155+
customPhrases && customPhrases.length > 0
156+
? customPhrases
157+
: WITTY_LOADING_PHRASES;
158+
150159
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState(
151-
WITTY_LOADING_PHRASES[0],
160+
loadingPhrases[0],
152161
);
153162
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
154163

@@ -165,16 +174,14 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
165174
}
166175
// Select an initial random phrase
167176
const initialRandomIndex = Math.floor(
168-
Math.random() * WITTY_LOADING_PHRASES.length,
177+
Math.random() * loadingPhrases.length,
169178
);
170-
setCurrentLoadingPhrase(WITTY_LOADING_PHRASES[initialRandomIndex]);
179+
setCurrentLoadingPhrase(loadingPhrases[initialRandomIndex]);
171180

172181
phraseIntervalRef.current = setInterval(() => {
173182
// Select a new random phrase
174-
const randomIndex = Math.floor(
175-
Math.random() * WITTY_LOADING_PHRASES.length,
176-
);
177-
setCurrentLoadingPhrase(WITTY_LOADING_PHRASES[randomIndex]);
183+
const randomIndex = Math.floor(Math.random() * loadingPhrases.length);
184+
setCurrentLoadingPhrase(loadingPhrases[randomIndex]);
178185
}, PHRASE_CHANGE_INTERVAL_MS);
179186
} else {
180187
// Idle or other states, clear the phrase interval
@@ -183,7 +190,7 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
183190
clearInterval(phraseIntervalRef.current);
184191
phraseIntervalRef.current = null;
185192
}
186-
setCurrentLoadingPhrase(WITTY_LOADING_PHRASES[0]);
193+
setCurrentLoadingPhrase(loadingPhrases[0]);
187194
}
188195

189196
return () => {
@@ -192,7 +199,7 @@ export const usePhraseCycler = (isActive: boolean, isWaiting: boolean) => {
192199
phraseIntervalRef.current = null;
193200
}
194201
};
195-
}, [isActive, isWaiting]);
202+
}, [isActive, isWaiting, loadingPhrases]);
196203

197204
return currentLoadingPhrase;
198205
};

0 commit comments

Comments
 (0)