mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-01 07:24:38 -07:00
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:
@@ -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();
|
||||
|
||||
@@ -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}>
|
||||
> 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
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user