Skip to content
Merged
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
8 changes: 8 additions & 0 deletions src/vs/sessions/contrib/chat/browser/newChatInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import { handleTerminalCommandPaste, isTerminalCommandInput } from '../../../../
import { getChatSessionType } from '../../../../workbench/contrib/chat/common/model/chatUri.js';
import { ChatSpeechToTextState, 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';
import { IVoiceInputModeService } from '../../../../workbench/contrib/chat/browser/voiceInputMode/voiceInputMode.js';
import { toAction } from '../../../../base/common/actions.js';
Expand Down Expand Up @@ -370,6 +371,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@IChatSpeechToTextService private readonly chatSpeechToTextService: IChatSpeechToTextService,
@IDictationOnboardingService private readonly dictationOnboardingService: IDictationOnboardingService,
@IChatSubmitRequestHandlerService private readonly chatSubmitRequestHandlerService: IChatSubmitRequestHandlerService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@ICommandService private readonly commandService: ICommandService,
Expand Down Expand Up @@ -451,6 +453,11 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
));
notificationContainer.appendChild(notificationWidget.domNode);

// First-run dictation introduction, docked directly above the input area
// so it reads as one stack with it - the same slot the chat view uses.
const dictationOnboardingContainer = dom.append(chatInputContainer, dom.$('.dictation-onboarding-container'));
this._register(this.dictationOnboardingService.registerHost(dictationOnboardingContainer, chatInputContainer));

// Input area inside the input slot
const inputAreaWrapper = dom.append(chatInputContainer, dom.$('.new-chat-input-area-wrapper'));
const inputArea = dom.append(inputAreaWrapper, dom.$('.new-chat-input-area'));
Expand Down Expand Up @@ -995,6 +1002,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
speechService: this.chatSpeechToTextService,
keybindingService: this.keybindingService,
logService: this.logService,
onboardingService: this.dictationOnboardingService,
}, TOGGLE_DICTATION_COMMAND_ID, this._editor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ServicesAccessor } from '../../../../../platform/instantiation/common/i
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { AgentsVoiceStorageKeys, AGENTS_VOICE_CONNECTED } from '../../../agentsVoice/common/agentsVoice.js';
Expand All @@ -26,6 +27,7 @@ import { CHAT_CATEGORY } from './chatActions.js';
import { IChatExecuteActionContext } from './chatExecuteActions.js';
import { IChatWidgetService } from '../chat.js';
import { ChatSpeechToTextState, IChatSpeechToTextService } from '../speechToText/chatSpeechToTextService.js';
import { IDictationOnboardingService } from '../speechToText/dictationOnboarding.js';
import { cancelDictation, isDictating, startDictation, stopDictation } from '../speechToText/dictationSession.js';

// Gate on `ChatContextKeys.enabled` so the dictation UI and its commands are
Expand All @@ -44,6 +46,13 @@ export interface IDictationShortcutContext {
readonly speechService: IChatSpeechToTextService;
readonly keybindingService: IKeybindingService;
readonly logService: ILogService;
/**
* Supplied by chat inputs only. The first dictation started from one is
* handed to the introduction card so the microphone can be chosen and
* checked before anything is transcribed; editor and terminal dictation have
* nowhere to dock the card and dictate straight away.
*/
readonly onboardingService?: IDictationOnboardingService;
}

/** Resolves a toggle invocation against the current dictation lifecycle. */
Expand Down Expand Up @@ -80,6 +89,14 @@ export async function runDictationShortcut(context: IDictationShortcutContext, c

const window = getWindow(editor.getDomNode()) ?? getActiveWindow();

// First run: let the introduction card take this dictation over so the user
// can confirm the microphone is the right one and is actually being heard.
// Nothing is recorded until they confirm the card, which is what starts the
// dictation this press asked for.
if (context.onboardingService?.showIfNeeded(() => void startDictation(speechService, editor, window, logService))) {
Comment thread
meganrogge marked this conversation as resolved.
return;
}

// Attempt to detect a held keybinding. Returns `undefined` when not invoked
// through a held key (e.g. the toolbar mic or the command palette), which
// collapses the behavior to a plain toggle.
Expand Down Expand Up @@ -156,6 +173,7 @@ export class ToggleChatSpeechToTextAction extends Action2 {
speechService: accessor.get(IChatSpeechToTextService),
keybindingService: accessor.get(IKeybindingService),
logService: accessor.get(ILogService),
onboardingService: accessor.get(IDictationOnboardingService),
}, ToggleChatSpeechToTextAction.ID, widget.inputEditor);
}
}
Expand Down Expand Up @@ -334,6 +352,31 @@ class SelectSpeechToTextMicrophoneAction extends Action2 {
}
}

class ShowChatSpeechToTextIntroductionAction extends Action2 {
static readonly ID = 'workbench.action.chat.showSpeechToTextIntroduction';

constructor() {
super({
id: ShowChatSpeechToTextIntroductionAction.ID,
title: localize2('chat.speechToText.showIntroduction', "Dictate: Show Introduction"),
category: CHAT_CATEGORY,
f1: true,
precondition: ChatSpeechToTextConfigured,
});
}

async run(accessor: ServicesAccessor): Promise<void> {
const onboardingService = accessor.get(IDictationOnboardingService);
if (onboardingService.show()) {
Comment thread
meganrogge marked this conversation as resolved.
return;
}

// The card docks to a chat input, so there is nothing to show it in when
// no chat is open. Say so rather than appearing to do nothing.
accessor.get(INotificationService).info(localize('chatStt.introductionNeedsChat', "Open a chat to see the dictation introduction."));
}
}

class CancelChatSpeechToTextAction extends Action2 {
static readonly ID = 'workbench.action.chat.cancelSpeechToText';

Expand Down Expand Up @@ -371,6 +414,7 @@ export function registerChatSpeechToTextActions(): DisposableStore {
store.add(registerAction2(ChatSpeechToTextConnectingAction));
store.add(registerAction2(HoldToSpeechToTextAction));
store.add(registerAction2(CancelChatSpeechToTextAction));
store.add(registerAction2(ShowChatSpeechToTextIntroductionAction));
store.add(registerAction2(SelectSpeechToTextMicrophoneAction));
return store;
}
Loading
Loading