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
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();
+22 -17
View File
@@ -360,6 +360,20 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
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<InputPromptProps> = ({
useBackgroundColor={useBackgroundColor}
>
<Box flexGrow={1} flexDirection="row" paddingX={1}>
{isVoiceModeEnabled && <Text color={theme.text.accent}>🎤 </Text>}
<Text
color={statusColor ?? theme.text.accent}
aria-label={statusText || undefined}
@@ -1812,35 +1827,25 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
<Box flexGrow={1} flexDirection="column" ref={innerBoxRef}>
{isRecording && (
<Box flexDirection="row" marginBottom={0}>
<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>
<Text color={theme.status.success}>Listening...</Text>
</Box>
)}
{buffer.text.length === 0 && !isRecording ? (
!isVoiceModeEnabled && placeholder ? (
effectivePlaceholder ? (
showCursor ? (
<Text
terminalCursorFocus={showCursor}
terminalCursorPosition={0}
>
{chalk.inverse(placeholder.slice(0, 1))}
{chalk.inverse(effectivePlaceholder.slice(0, 1))}
<Text color={theme.text.secondary}>
{placeholder.slice(1)}
{effectivePlaceholder.slice(1)}
</Text>
</Text>
) : (
<Text color={theme.text.secondary}>{placeholder}</Text>
<Text color={theme.text.secondary}>
{effectivePlaceholder}
</Text>
)
) : null
) : (