From ef040eb392a6735f500804d789dabc08ae1325e5 Mon Sep 17 00:00:00 2001 From: Dev Randalpura Date: Thu, 30 Apr 2026 14:21:54 -0500 Subject: [PATCH] feat(ui): added microphone and updated placeholder for voice mode (#26270) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../src/ui/components/InputPrompt.test.tsx | 43 ++++++++++--------- .../cli/src/ui/components/InputPrompt.tsx | 39 +++++++++-------- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 5be237a15f..3bf48259fe 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -4979,9 +4979,10 @@ describe('InputPrompt', () => { ); // Initially not recording - expect(lastFrame()).not.toContain('🎙️ Listening...'); + expect(lastFrame()).not.toContain('Listening...'); + expect(lastFrame()).toContain('🎤 >'); expect(lastFrame()).toContain( - 'Voice mode: Space to start/stop recording', + 'Type your message or space to talk (Esc to exit)', ); // Press space to start @@ -4991,7 +4992,7 @@ describe('InputPrompt', () => { // Now should show listening await waitFor(() => { - expect(lastFrame()).toContain('🎙️ Listening...'); + expect(lastFrame()).toContain('Listening...'); }); unmount(); @@ -5016,7 +5017,7 @@ describe('InputPrompt', () => { stdin.write(' '); }); await waitFor(() => { - expect(lastFrame()).toContain('🎙️ Listening...'); + expect(lastFrame()).toContain('Listening...'); }); // Stop recording @@ -5024,10 +5025,8 @@ describe('InputPrompt', () => { stdin.write(' '); }); await waitFor(() => { - expect(lastFrame()).not.toContain('🎙️ Listening...'); - expect(lastFrame()).toContain( - 'Voice mode: Space to start/stop recording', - ); + expect(lastFrame()).not.toContain('Listening...'); + expect(lastFrame()).toContain('🎤 >'); }); unmount(); @@ -5047,10 +5046,8 @@ describe('InputPrompt', () => { }, ); - // Should show voice mode hint even if buffer is not empty (new behavior) - expect(lastFrame()).toContain( - 'Voice mode: Space to start/stop recording', - ); + // Should show voice mode prefix even if buffer is not empty + expect(lastFrame()).toContain('🎤 >'); expect(lastFrame()).toContain('some existing text'); // Press space to start recording again @@ -5059,7 +5056,7 @@ describe('InputPrompt', () => { }); await waitFor(() => { - expect(lastFrame()).toContain('🎙️ Listening...'); + expect(lastFrame()).toContain('Listening...'); }); unmount(); @@ -5085,7 +5082,7 @@ describe('InputPrompt', () => { }); // Should NOT show listening, instead should call handleInput which handles space - expect(lastFrame()).not.toContain('🎙️ Listening...'); + expect(lastFrame()).not.toContain('Listening...'); expect(mockBuffer.handleInput).toHaveBeenCalled(); unmount(); }); @@ -5202,7 +5199,10 @@ describe('InputPrompt', () => { }, ); - expect(lastFrame()).toContain('Voice mode: Hold Space to record'); + expect(lastFrame()).toContain('🎤 >'); + expect(lastFrame()).toContain( + 'Type your message or hold space to talk (Esc to exit)', + ); // Press space once await act(async () => { @@ -5211,14 +5211,14 @@ describe('InputPrompt', () => { // Should insert space optimistically expect(mockBuffer.insert).toHaveBeenCalledWith(' '); - expect(lastFrame()).not.toContain('🎙️ Listening...'); + expect(lastFrame()).not.toContain('Listening...'); // Advance timer past HOLD_DELAY_MS await act(async () => { vi.advanceTimersByTime(700); }); - expect(lastFrame()).not.toContain('🎙️ Listening...'); + expect(lastFrame()).not.toContain('Listening...'); unmount(); }); @@ -5248,7 +5248,7 @@ describe('InputPrompt', () => { // Should have backspaced the optimistic space expect(mockBuffer.backspace).toHaveBeenCalled(); // Should show listening - expect(lastFrame()).toContain('🎙️ Listening...'); + expect(lastFrame()).toContain('Listening...'); }); unmount(); @@ -5274,7 +5274,7 @@ describe('InputPrompt', () => { // Use a short interval in waitFor to prevent advancing fake timers past the 300ms RELEASE_DELAY_MS await waitFor( () => { - expect(lastFrame()).toContain('🎙️ Listening...'); + expect(lastFrame()).toContain('Listening...'); }, { interval: 10 }, ); @@ -5284,7 +5284,8 @@ describe('InputPrompt', () => { stdin.write(' '); vi.advanceTimersByTime(100); }); - expect(lastFrame()).toContain('🎙️ Listening...'); + expect(lastFrame()).toContain('🎤 >'); + expect(lastFrame()).toContain('Listening...'); // Stop heartbeat (release) await act(async () => { @@ -5292,7 +5293,7 @@ describe('InputPrompt', () => { }); await waitFor(() => { - expect(lastFrame()).not.toContain('🎙️ Listening...'); + expect(lastFrame()).not.toContain('Listening...'); }); unmount(); diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index f69138c8c7..0e823d77a4 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -360,6 +360,20 @@ export const InputPrompt: React.FC = ({ isShellSuggestionsVisible, } = completion; + const effectivePlaceholder = useMemo(() => { + if (!isVoiceModeEnabled) return placeholder; + const voiceAction = + (settings.experimental.voice?.activationMode ?? 'push-to-talk') === + 'push-to-talk' + ? 'hold space to talk' + : 'space to talk'; + return ` Type your message or ${voiceAction} (Esc to exit)`; + }, [ + isVoiceModeEnabled, + placeholder, + settings.experimental.voice?.activationMode, + ]); + const showCursor = focus && isShellFocused && !isEmbeddedShellFocused && !copyModeEnabled; @@ -1786,6 +1800,7 @@ export const InputPrompt: React.FC = ({ useBackgroundColor={useBackgroundColor} > + {isVoiceModeEnabled && 🎤 } = ({ {isRecording && ( - 🎙️ Listening... - - )} - {isVoiceModeEnabled && !isRecording && ( - - - > Voice mode:{' '} - {(settings.experimental.voice?.activationMode ?? - 'push-to-talk') === 'push-to-talk' - ? 'Hold Space to record' - : 'Space to start/stop recording'}{' '} - (Esc to exit) - + Listening... )} {buffer.text.length === 0 && !isRecording ? ( - !isVoiceModeEnabled && placeholder ? ( + effectivePlaceholder ? ( showCursor ? ( - {chalk.inverse(placeholder.slice(0, 1))} + {chalk.inverse(effectivePlaceholder.slice(0, 1))} - {placeholder.slice(1)} + {effectivePlaceholder.slice(1)} ) : ( - {placeholder} + + {effectivePlaceholder} + ) ) : null ) : (