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>
This commit is contained in:
Dev Randalpura
2026-04-30 14:21:54 -05:00
committed by GitHub
parent 8c1e255ac0
commit ef040eb392
2 changed files with 44 additions and 38 deletions
@@ -4979,9 +4979,10 @@ describe('InputPrompt', () => {
); );
// Initially not recording // Initially not recording
expect(lastFrame()).not.toContain('🎙️ Listening...'); expect(lastFrame()).not.toContain('Listening...');
expect(lastFrame()).toContain('🎤 >');
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 // Press space to start
@@ -4991,7 +4992,7 @@ describe('InputPrompt', () => {
// Now should show listening // Now should show listening
await waitFor(() => { await waitFor(() => {
expect(lastFrame()).toContain('🎙️ Listening...'); expect(lastFrame()).toContain('Listening...');
}); });
unmount(); unmount();
@@ -5016,7 +5017,7 @@ describe('InputPrompt', () => {
stdin.write(' '); stdin.write(' ');
}); });
await waitFor(() => { await waitFor(() => {
expect(lastFrame()).toContain('🎙️ Listening...'); expect(lastFrame()).toContain('Listening...');
}); });
// Stop recording // Stop recording
@@ -5024,10 +5025,8 @@ describe('InputPrompt', () => {
stdin.write(' '); stdin.write(' ');
}); });
await waitFor(() => { await waitFor(() => {
expect(lastFrame()).not.toContain('🎙️ Listening...'); expect(lastFrame()).not.toContain('Listening...');
expect(lastFrame()).toContain( expect(lastFrame()).toContain('🎤 >');
'Voice mode: Space to start/stop recording',
);
}); });
unmount(); unmount();
@@ -5047,10 +5046,8 @@ describe('InputPrompt', () => {
}, },
); );
// Should show voice mode hint even if buffer is not empty (new behavior) // Should show voice mode prefix even if buffer is not empty
expect(lastFrame()).toContain( expect(lastFrame()).toContain('🎤 >');
'Voice mode: Space to start/stop recording',
);
expect(lastFrame()).toContain('some existing text'); expect(lastFrame()).toContain('some existing text');
// Press space to start recording again // Press space to start recording again
@@ -5059,7 +5056,7 @@ describe('InputPrompt', () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(lastFrame()).toContain('🎙️ Listening...'); expect(lastFrame()).toContain('Listening...');
}); });
unmount(); unmount();
@@ -5085,7 +5082,7 @@ describe('InputPrompt', () => {
}); });
// Should NOT show listening, instead should call handleInput which handles space // 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(); expect(mockBuffer.handleInput).toHaveBeenCalled();
unmount(); 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 // Press space once
await act(async () => { await act(async () => {
@@ -5211,14 +5211,14 @@ describe('InputPrompt', () => {
// Should insert space optimistically // Should insert space optimistically
expect(mockBuffer.insert).toHaveBeenCalledWith(' '); expect(mockBuffer.insert).toHaveBeenCalledWith(' ');
expect(lastFrame()).not.toContain('🎙️ Listening...'); expect(lastFrame()).not.toContain('Listening...');
// Advance timer past HOLD_DELAY_MS // Advance timer past HOLD_DELAY_MS
await act(async () => { await act(async () => {
vi.advanceTimersByTime(700); vi.advanceTimersByTime(700);
}); });
expect(lastFrame()).not.toContain('🎙️ Listening...'); expect(lastFrame()).not.toContain('Listening...');
unmount(); unmount();
}); });
@@ -5248,7 +5248,7 @@ describe('InputPrompt', () => {
// Should have backspaced the optimistic space // Should have backspaced the optimistic space
expect(mockBuffer.backspace).toHaveBeenCalled(); expect(mockBuffer.backspace).toHaveBeenCalled();
// Should show listening // Should show listening
expect(lastFrame()).toContain('🎙️ Listening...'); expect(lastFrame()).toContain('Listening...');
}); });
unmount(); unmount();
@@ -5274,7 +5274,7 @@ describe('InputPrompt', () => {
// Use a short interval in waitFor to prevent advancing fake timers past the 300ms RELEASE_DELAY_MS // Use a short interval in waitFor to prevent advancing fake timers past the 300ms RELEASE_DELAY_MS
await waitFor( await waitFor(
() => { () => {
expect(lastFrame()).toContain('🎙️ Listening...'); expect(lastFrame()).toContain('Listening...');
}, },
{ interval: 10 }, { interval: 10 },
); );
@@ -5284,7 +5284,8 @@ describe('InputPrompt', () => {
stdin.write(' '); stdin.write(' ');
vi.advanceTimersByTime(100); vi.advanceTimersByTime(100);
}); });
expect(lastFrame()).toContain('🎙️ Listening...'); expect(lastFrame()).toContain('🎤 >');
expect(lastFrame()).toContain('Listening...');
// Stop heartbeat (release) // Stop heartbeat (release)
await act(async () => { await act(async () => {
@@ -5292,7 +5293,7 @@ describe('InputPrompt', () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(lastFrame()).not.toContain('🎙️ Listening...'); expect(lastFrame()).not.toContain('Listening...');
}); });
unmount(); unmount();
+22 -17
View File
@@ -360,6 +360,20 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
isShellSuggestionsVisible, isShellSuggestionsVisible,
} = completion; } = 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 = const showCursor =
focus && isShellFocused && !isEmbeddedShellFocused && !copyModeEnabled; focus && isShellFocused && !isEmbeddedShellFocused && !copyModeEnabled;
@@ -1786,6 +1800,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
useBackgroundColor={useBackgroundColor} useBackgroundColor={useBackgroundColor}
> >
<Box flexGrow={1} flexDirection="row" paddingX={1}> <Box flexGrow={1} flexDirection="row" paddingX={1}>
{isVoiceModeEnabled && <Text color={theme.text.accent}>🎤 </Text>}
<Text <Text
color={statusColor ?? theme.text.accent} color={statusColor ?? theme.text.accent}
aria-label={statusText || undefined} aria-label={statusText || undefined}
@@ -1812,35 +1827,25 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
<Box flexGrow={1} flexDirection="column" ref={innerBoxRef}> <Box flexGrow={1} flexDirection="column" ref={innerBoxRef}>
{isRecording && ( {isRecording && (
<Box flexDirection="row" marginBottom={0}> <Box flexDirection="row" marginBottom={0}>
<Text color={theme.status.success}>🎙 Listening...</Text> <Text color={theme.status.success}>Listening...</Text>
</Box>
)}
{isVoiceModeEnabled && !isRecording && (
<Box flexDirection="row" marginBottom={0}>
<Text color={theme.text.secondary}>
&gt; Voice mode:{' '}
{(settings.experimental.voice?.activationMode ??
'push-to-talk') === 'push-to-talk'
? 'Hold Space to record'
: 'Space to start/stop recording'}{' '}
(Esc to exit)
</Text>
</Box> </Box>
)} )}
{buffer.text.length === 0 && !isRecording ? ( {buffer.text.length === 0 && !isRecording ? (
!isVoiceModeEnabled && placeholder ? ( effectivePlaceholder ? (
showCursor ? ( showCursor ? (
<Text <Text
terminalCursorFocus={showCursor} terminalCursorFocus={showCursor}
terminalCursorPosition={0} terminalCursorPosition={0}
> >
{chalk.inverse(placeholder.slice(0, 1))} {chalk.inverse(effectivePlaceholder.slice(0, 1))}
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
{placeholder.slice(1)} {effectivePlaceholder.slice(1)}
</Text> </Text>
</Text> </Text>
) : ( ) : (
<Text color={theme.text.secondary}>{placeholder}</Text> <Text color={theme.text.secondary}>
{effectivePlaceholder}
</Text>
) )
) : null ) : null
) : ( ) : (