Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/vs/sessions/contrib/chat/browser/newChatInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ import { AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING } from './sessionsChatHisto
import { IChatStatusItemService } from '../../../../workbench/contrib/chat/browser/chatStatus/chatStatusItemService.js';
import { handleTerminalCommandPaste, isTerminalCommandInput } from '../../../../workbench/contrib/chat/browser/chatTerminalCommandPaste.js';
import { getChatSessionType } from '../../../../workbench/contrib/chat/common/model/chatUri.js';
import { ChatSpeechToTextState, IChatSpeechToTextService } from '../../../../workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.js';
import { ChatSpeechToTextState, DictationSettingId, IChatSpeechToTextService } from '../../../../workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.js';
import { setupDictationMicGlow } from '../../../../workbench/contrib/chat/browser/speechToText/dictationMicGlow.js';
import { IDictationOnboardingService } from '../../../../workbench/contrib/chat/browser/speechToText/dictationOnboarding.js';
import { ChatVoiceInputModeAction, VoiceInputModeActionViewItem } from '../../../../workbench/contrib/chat/browser/voiceInputMode/voiceInputModeActionViewItem.js';
Expand Down Expand Up @@ -982,7 +982,10 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
// keys off `isConnected` only, not the connecting phase.
const sessionActive = this.voiceSessionController.isConnected.get();
const pillActive = (dict && voice) || (voice && !dict && !handsFree && sessionActive);
button.classList.toggle('hidden', !sttService.isConfigured || voiceActive || pillActive);
// Honor the shared `dictation.showButton` visibility toggle: hiding the
// button still leaves Cmd/Ctrl+I working (its keybinding is independent).
const buttonShown = this.configurationService.getValue<boolean>(DictationSettingId.ShowButton) !== false;
button.classList.toggle('hidden', !sttService.isConfigured || voiceActive || pillActive || !buttonShown);
};
updateVisibility();
this._register(autorun(reader => {
Expand All @@ -999,7 +1002,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
// Both the enable kill-switch and the model selection can change
// availability (e.g. an unsupported on-device platform becomes
// configured when switching to the cloud backend).
if (e.affectsConfiguration('dictation.enabled') || e.affectsConfiguration('dictation.model')) {
if (e.affectsConfiguration('dictation.enabled') || e.affectsConfiguration('dictation.model') || e.affectsConfiguration(DictationSettingId.ShowButton)) {
updateVisibility();
}
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { CHAT_CATEGORY } from './chatActions.js';
import { IChatExecuteActionContext } from './chatExecuteActions.js';
import { IChatWidgetService } from '../chat.js';
import { ChatSpeechToTextState, IChatSpeechToTextService } from '../speechToText/chatSpeechToTextService.js';
import { ChatSpeechToTextState, DictationSettingId, IChatSpeechToTextService } from '../speechToText/chatSpeechToTextService.js';
import { buildMicrophoneOptions, IDictationOnboardingService, RESET_DICTATION_ONBOARDING_COMMAND, SHOW_DICTATION_ONBOARDING_COMMAND } from '../speechToText/dictationOnboarding.js';
import { cancelDictation, isDictating, startDictation, stopDictation } from '../speechToText/dictationSession.js';

Expand All @@ -37,6 +37,13 @@ export const ChatSpeechToTextConfigured = ContextKeyExpr.and(ChatContextKeys.ena
/** True while the selected dictation backend is preparing. */
export const ChatSpeechToTextPreparing = ContextKeyExpr.has(ChatContextKeys.speechToTextPreparing.key);
const ChatSpeechToTextMaiBackend = ContextKeyExpr.equals('config.dictation.model', 'mai');
/**
* True unless the user has hidden the chat-input dictation microphone button via
* {@link DictationSettingId.ShowButton}. Gates only the toolbar button; the
* Cmd/Ctrl+I dictation shortcut stays available so dictation can still be
* launched when the button is hidden.
*/
const ChatSpeechToTextButtonShown = ContextKeyExpr.notEquals(`config.${DictationSettingId.ShowButton}`, false);

/** Releases shorter than this are treated as an accidental tap and discarded. */
const HOLD_TO_TALK_THRESHOLD_MS = 500;
Expand Down Expand Up @@ -144,7 +151,7 @@ export class ToggleChatSpeechToTextAction extends Action2 {
menu: [{
id: MenuId.ChatExecute,
order: -11,
when: ContextKeyExpr.and(ChatSpeechToTextConfigured, ChatSpeechToTextPreparing.negate(), AGENTS_VOICE_CONNECTED.negate(), SegmentedVoiceInputModePillInactive),
when: ContextKeyExpr.and(ChatSpeechToTextConfigured, ChatSpeechToTextButtonShown, ChatSpeechToTextPreparing.negate(), AGENTS_VOICE_CONNECTED.negate(), SegmentedVoiceInputModePillInactive),
group: 'navigation',
}],
keybinding: {
Expand Down Expand Up @@ -200,7 +207,7 @@ export class ChatSpeechToTextPreparingAction extends Action2 {
menu: [{
id: MenuId.ChatExecute,
order: -11,
when: ContextKeyExpr.and(ChatSpeechToTextConfigured, ChatSpeechToTextPreparing, ChatSpeechToTextMaiBackend.negate(), AGENTS_VOICE_CONNECTED.negate(), SegmentedVoiceInputModePillInactive),
when: ContextKeyExpr.and(ChatSpeechToTextConfigured, ChatSpeechToTextButtonShown, ChatSpeechToTextPreparing, ChatSpeechToTextMaiBackend.negate(), AGENTS_VOICE_CONNECTED.negate(), SegmentedVoiceInputModePillInactive),
group: 'navigation',
}],
});
Expand Down Expand Up @@ -228,7 +235,7 @@ export class ChatSpeechToTextConnectingAction extends Action2 {
menu: [{
id: MenuId.ChatExecute,
order: -11,
when: ContextKeyExpr.and(ChatSpeechToTextConfigured, ChatSpeechToTextPreparing, ChatSpeechToTextMaiBackend, AGENTS_VOICE_CONNECTED.negate(), SegmentedVoiceInputModePillInactive),
when: ContextKeyExpr.and(ChatSpeechToTextConfigured, ChatSpeechToTextButtonShown, ChatSpeechToTextPreparing, ChatSpeechToTextMaiBackend, AGENTS_VOICE_CONNECTED.negate(), SegmentedVoiceInputModePillInactive),
group: 'navigation',
}],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@ configurationRegistry.registerConfiguration({
default: true,
tags: ['experimental']
},
[DictationSettingId.ShowButton]: {
type: 'boolean',
markdownDescription: nls.localize('dictation.showButton', "Controls whether the dictation microphone button is shown in the chat input. When hidden, dictation can still be started with its keyboard shortcut."),
default: true,
tags: ['experimental']
},
'dictation.experimental.llmCleanup': {
type: 'boolean',
markdownDescription: nls.localize('dictation.experimental.llmCleanup', "Experimental: when dictation ends, the final transcript is passed through a small language model to restore punctuation, capitalization, paragraphs, and lists. Requires Copilot to be enabled; the transcript is sent to the language model for cleanup. Falls back to the raw transcript when no model is available. Use [dictation instructions](command:{0}) to customize terminology and formatting.", CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const DICTATION_MODEL_SETTING = 'dictation.model';

export const enum DictationSettingId {
ShowTranscript = 'dictation.showTranscript',
ShowButton = 'dictation.showButton',
}

/** `dictation.model` sentinel selecting the cloud voice backend used by Voice Mode. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
import { CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID, CONFIGURE_VOICE_INSTRUCTIONS_ACTION_ID } from '../actions/configureVoiceInstructionsAction.js';
import { DictationSettingId } from './chatSpeechToTextService.js';
import { SHOW_DICTATION_ONBOARDING_COMMAND } from './dictationOnboarding.js';

/** Command that opens the microphone picker shared by dictation and Voice Mode. */
Expand Down Expand Up @@ -71,6 +72,22 @@ function createShowDictationOnboardingAction(commandService: ICommandService): I
});
}

/**
* Checkable "Microphone Button" entry mirroring the action bar visibility toggles:
* checked while the button is shown, unchecking it hides the button. Hiding only
* removes the toolbar affordance — dictation can still be launched with its
* Cmd/Ctrl+I shortcut, and the button can be restored from Settings.
*/
function createToggleDictationButtonAction(configurationService: IConfigurationService): IAction {
const shown = configurationService.getValue<boolean>(DictationSettingId.ShowButton) !== false;
return toAction({
id: 'chat.dictation.toggleButton',
label: localize('dictation.microphoneButton', "Microphone Button"),
checked: shown,
run: () => configurationService.updateValue(DictationSettingId.ShowButton, !shown),
});
}

/**
* "Disable" entry for Voice Mode. Tears down any active session first so disabling
* the setting doesn't leave the microphone capturing while the toolbar
Expand All @@ -95,6 +112,7 @@ export function getDictationContextMenuActions(commandService: ICommandService,
return Separator.join(
[
createConfigureKeybindingAction(commandService, keybindingService, keybindingCommandId),
createToggleDictationButtonAction(configurationService),
createDisableDictationAction(commandService, configurationService),
],
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ suite('Mic button menu actions', () => {
});

test('groups and shortens dictation actions', () => {
const configurationService = upcastPartial<IConfigurationService>({ getValue: () => true, updateValue: async () => { } });
const actions = getDictationContextMenuActions(commandService, configurationService, keybindingService, 'dictation.start');

assert.deepStrictEqual(actions.map(action => action.label), [
'Configure Keybinding',
'Microphone Button',
'Disable',
'',
'Open Settings',
Expand All @@ -46,4 +48,18 @@ suite('Mic button menu actions', () => {
'Select Microphone',
]);
});

test('dictation "Microphone Button" toggle reflects and flips the visibility setting', async () => {
const updated: [string, unknown][] = [];
const configurationService = upcastPartial<IConfigurationService>({
getValue: () => false,
updateValue: async (key: string, value: unknown) => { updated.push([key, value]); },
});
const actions = getDictationContextMenuActions(commandService, configurationService, keybindingService, 'dictation.start');
const toggle = actions.find(action => action.label === 'Microphone Button')!;

assert.strictEqual(toggle.checked, false);
await toggle.run();
assert.deepStrictEqual(updated, [['dictation.showButton', true]]);
});
});