diff --git a/src/vs/workbench/contrib/agentsVoice/browser/media/voiceModeOnboarding.css b/src/vs/workbench/contrib/agentsVoice/browser/media/voiceModeOnboarding.css index 4506c78f30464b..2cd4bdddc8e8c8 100644 --- a/src/vs/workbench/contrib/agentsVoice/browser/media/voiceModeOnboarding.css +++ b/src/vs/workbench/contrib/agentsVoice/browser/media/voiceModeOnboarding.css @@ -135,6 +135,15 @@ transition: border-color 100ms ease-out; } +/* + * A single microphone is no choice, so the row hides itself. The `display: flex` + * above outweighs the `[hidden]` attribute's own rule, so it has to be undone + * here - otherwise an empty bordered box is left where the picker would be. + */ +.voice-mode-onboarding-microphone-picker[hidden] { + display: none; +} + .voice-mode-onboarding-microphone-picker:hover { border-color: var(--vscode-focusBorder); } diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/media/dictationOnboarding.css b/src/vs/workbench/contrib/chat/browser/speechToText/media/dictationOnboarding.css index 5c861b26f2fe8c..30a43c3d1d58d4 100644 --- a/src/vs/workbench/contrib/chat/browser/speechToText/media/dictationOnboarding.css +++ b/src/vs/workbench/contrib/chat/browser/speechToText/media/dictationOnboarding.css @@ -130,6 +130,15 @@ transition: border-color 100ms ease-out; } +/* + * A single microphone is no choice, so the row hides itself. The `display: flex` + * above outweighs the `[hidden]` attribute's own rule, so it has to be undone + * here - otherwise an empty bordered box is left where the picker would be. + */ +.dictation-onboarding-picker[hidden] { + display: none; +} + .dictation-onboarding-picker:hover { border-color: var(--vscode-focusBorder); } diff --git a/src/vs/workbench/contrib/chat/test/browser/dictationOnboarding.test.ts b/src/vs/workbench/contrib/chat/test/browser/dictationOnboarding.test.ts index 5d870ff1b285c5..931273c559786c 100644 --- a/src/vs/workbench/contrib/chat/test/browser/dictationOnboarding.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/dictationOnboarding.test.ts @@ -192,6 +192,52 @@ suite('Dictation onboarding', () => { }); }); + test('omits the microphone picker entirely when there is only one microphone', async () => { + const host = createHost(disposables); + const mediaDevices = Object.assign(new EventTarget(), { + enumerateDevices: async () => [ + // One physical microphone, reported alongside the virtual default + // entry that duplicates it - so after normalization there is a + // single choice and the row has no picking to do. + device('audioinput', 'default', 'Default - Studio Mic'), + device('audioinput', 'studio', 'Studio Mic'), + ], + getUserMedia: async (): Promise => { + throw new Error('Automatic onboarding must not acquire a stream'); + }, + }); + const instantiationService = workbenchInstantiationService(undefined, disposables); + const banner = disposables.add(instantiationService.createInstance(DictationOnboardingBanner, { + container: host.container, + onDismiss: () => { }, + previewMicrophone: false, + source: 'automatic', + }, mediaDevices)); + + const analyser = new class extends mock() { + override readonly fftSize = 256; + override getByteTimeDomainData(array: Uint8Array): void { + array.fill(128); + } + }; + await banner.refreshMicrophones(analyser, async () => analyser); + + assert.deepStrictEqual( + { + pickerHidden: host.container.querySelector('.dictation-onboarding-picker')?.hidden, + // Neither a dropdown nor the microphone's name is shown. + selects: host.container.querySelectorAll('.dictation-onboarding-picker select').length, + pickerText: host.container.querySelector('.dictation-onboarding-picker')?.textContent, + hasWaveform: host.container.querySelector('.dictation-onboarding-waveform') !== null, + }, + { + pickerHidden: true, + selects: 0, + pickerText: '', + hasWaveform: true, + }); + }); + test('escape dismisses the card', () => { const service = createService(disposables); const host = createHost(disposables);