Skip to content

Commit 73ad7fb

Browse files
authored
voice - align hold to speak with inline chat (microsoft#205655)
1 parent 5c2f9eb commit 73ad7fb

File tree

2 files changed

+75
-23
lines changed

2 files changed

+75
-23
lines changed

src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ import { firstOrDefault } from 'vs/base/common/arrays';
99
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
1010
import { Codicon } from 'vs/base/common/codicons';
1111
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
12-
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
1312
import { localize, localize2 } from 'vs/nls';
1413
import { Action2, IAction2Options, MenuId } from 'vs/platform/actions/common/actions';
1514
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
16-
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
15+
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
1716
import { spinningLoading } from 'vs/platform/theme/common/iconRegistry';
1817
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
1918
import { IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat';
@@ -36,7 +35,7 @@ import { ColorScheme } from 'vs/platform/theme/common/theme';
3635
import { Color } from 'vs/base/common/color';
3736
import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
3837
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
39-
import { isNumber } from 'vs/base/common/types';
38+
import { assertIsDefined, isNumber } from 'vs/base/common/types';
4039
import { AccessibilityVoiceSettingId, SpeechTimeoutDefault, accessibilityConfigurationNodeBase } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
4140
import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
4241
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
@@ -64,6 +63,7 @@ const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey<boolean>('voice
6463
const CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS = new RawContextKey<boolean>('voiceChatInEditorInProgress', false, { type: 'boolean', description: localize('voiceChatInEditorInProgress', "True when voice recording from microphone is in progress in the chat editor.") });
6564

6665
const CanVoiceChat = ContextKeyExpr.and(CONTEXT_PROVIDER_EXISTS, HasSpeechProvider);
66+
const FocusInChatInput = assertIsDefined(ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT));
6767

6868
type VoiceChatSessionContext = 'inline' | 'quick' | 'view' | 'editor';
6969

@@ -92,7 +92,6 @@ class VoiceChatSessionControllerFactory {
9292
static create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'focused'): Promise<IVoiceChatSessionController | undefined>;
9393
static async create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'focused'): Promise<IVoiceChatSessionController | undefined> {
9494
const chatWidgetService = accessor.get(IChatWidgetService);
95-
const chatService = accessor.get(IChatService);
9695
const viewsService = accessor.get(IViewsService);
9796
const chatContributionService = accessor.get(IChatContributionService);
9897
const quickChatService = accessor.get(IQuickChatService);
@@ -136,12 +135,9 @@ class VoiceChatSessionControllerFactory {
136135

137136
// View Chat
138137
if (context === 'view' || context === 'focused' /* fallback in case 'focused' was not successful */) {
139-
const provider = firstOrDefault(chatService.getProviderInfos());
140-
if (provider) {
141-
const chatView = await chatWidgetService.revealViewForProvider(provider.id);
142-
if (chatView) {
143-
return VoiceChatSessionControllerFactory.doCreateForChatView(chatView, viewsService, chatContributionService);
144-
}
138+
const chatView = await VoiceChatSessionControllerFactory.revealChatView(accessor);
139+
if (chatView) {
140+
return VoiceChatSessionControllerFactory.doCreateForChatView(chatView, viewsService, chatContributionService);
145141
}
146142
}
147143

@@ -169,6 +165,18 @@ class VoiceChatSessionControllerFactory {
169165
return undefined;
170166
}
171167

168+
static async revealChatView(accessor: ServicesAccessor): Promise<IChatWidget | undefined> {
169+
const chatWidgetService = accessor.get(IChatWidgetService);
170+
const chatService = accessor.get(IChatService);
171+
172+
const provider = firstOrDefault(chatService.getProviderInfos());
173+
if (provider) {
174+
return chatWidgetService.revealViewForProvider(provider.id);
175+
}
176+
177+
return undefined;
178+
}
179+
172180
private static doCreateForChatView(chatView: IChatWidget, viewsService: IViewsService, chatContributionService: IChatContributionService): IVoiceChatSessionController {
173181
return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('view', chatView, viewsService, chatContributionService);
174182
}
@@ -414,7 +422,7 @@ async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor
414422
let acceptVoice = false;
415423
const handle = disposableTimeout(() => {
416424
acceptVoice = true;
417-
session.setTimeoutDisabled(true); // disable accept on timeout when hold mode runs for 250ms
425+
session.setTimeoutDisabled(true); // disable accept on timeout when hold mode runs for VOICE_KEY_HOLD_THRESHOLD
418426
}, VOICE_KEY_HOLD_THRESHOLD);
419427

420428
const controller = await VoiceChatSessionControllerFactory.create(accessor, target);
@@ -459,6 +467,57 @@ export class VoiceChatInChatViewAction extends VoiceChatWithHoldModeAction {
459467
}
460468
}
461469

470+
export class HoldToVoiceChatInChatViewAction extends Action2 {
471+
472+
static readonly ID = 'workbench.action.chat.holdToVoiceChatInChatView';
473+
474+
constructor() {
475+
super({
476+
id: HoldToVoiceChatInChatViewAction.ID,
477+
title: localize2('workbench.action.chat.holdToVoiceChatInChatView.label', "Hold to Voice Chat in View"),
478+
keybinding: {
479+
weight: KeybindingWeight.WorkbenchContrib,
480+
when: ContextKeyExpr.and(
481+
CanVoiceChat,
482+
FocusInChatInput.negate(), // when already in chat input, disable this action and prefer to start voice chat directly
483+
EditorContextKeys.focus.negate() // do not steal the inline-chat keybinding
484+
),
485+
primary: KeyMod.CtrlCmd | KeyCode.KeyI
486+
}
487+
});
488+
}
489+
490+
override async run(accessor: ServicesAccessor, context?: IChatExecuteActionContext): Promise<void> {
491+
492+
// The intent of this action is to provide 2 modes to align with what `Ctrlcmd+I` in inline chat:
493+
// - if the user press and holds, we start voice chat in the chat view
494+
// - if the user press and releases quickly enough, we just open the chat view without voice chat
495+
496+
const instantiationService = accessor.get(IInstantiationService);
497+
const keybindingService = accessor.get(IKeybindingService);
498+
499+
const holdMode = keybindingService.enableKeybindingHoldMode(HoldToVoiceChatInChatViewAction.ID);
500+
501+
let session: IVoiceChatSession | undefined;
502+
const handle = disposableTimeout(async () => {
503+
const controller = await VoiceChatSessionControllerFactory.create(accessor, 'view');
504+
if (controller) {
505+
session = VoiceChatSessions.getInstance(instantiationService).start(controller, context);
506+
session.setTimeoutDisabled(true);
507+
}
508+
}, VOICE_KEY_HOLD_THRESHOLD);
509+
510+
(await VoiceChatSessionControllerFactory.revealChatView(accessor))?.focusInput();
511+
512+
await holdMode;
513+
handle.dispose();
514+
515+
if (session) {
516+
session.accept();
517+
}
518+
}
519+
}
520+
462521
export class InlineVoiceChatAction extends VoiceChatWithHoldModeAction {
463522

464523
static readonly ID = 'workbench.action.chat.inlineVoiceChat';
@@ -502,11 +561,8 @@ export class StartVoiceChatAction extends Action2 {
502561
keybinding: {
503562
weight: KeybindingWeight.WorkbenchContrib,
504563
when: ContextKeyExpr.and(
505-
CanVoiceChat,
506-
EditorContextKeys.focus.toNegated(), // do not steal the inline-chat keybinding
507-
CONTEXT_VOICE_CHAT_GETTING_READY.negate(),
508-
CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(),
509-
CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.negate(),
564+
FocusInChatInput, // scope this action to chat input fields only
565+
EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding
510566
CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(),
511567
CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(),
512568
CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(),
@@ -629,7 +685,6 @@ class BaseStopListeningAction extends Action2 {
629685
category: CHAT_CATEGORY,
630686
keybinding: {
631687
weight: KeybindingWeight.WorkbenchContrib + 100,
632-
when: ContextKeyExpr.and(CanVoiceChat, context),
633688
primary: KeyCode.Escape
634689
},
635690
precondition: ContextKeyExpr.and(CanVoiceChat, context),
@@ -704,11 +759,7 @@ export class StopListeningAndSubmitAction extends Action2 {
704759
f1: true,
705760
keybinding: {
706761
weight: KeybindingWeight.WorkbenchContrib,
707-
when: ContextKeyExpr.and(
708-
CanVoiceChat,
709-
ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT),
710-
CONTEXT_VOICE_CHAT_IN_PROGRESS
711-
),
762+
when: FocusInChatInput,
712763
primary: KeyMod.CtrlCmd | KeyCode.KeyI
713764
},
714765
precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_IN_PROGRESS)

src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInInlineChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions';
6+
import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInInlineChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, HoldToVoiceChatInChatViewAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions';
77
import { registerAction2 } from 'vs/platform/actions/common/actions';
88
import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions';
99

1010
registerAction2(StartVoiceChatAction);
1111
registerAction2(InstallVoiceChatAction);
1212

1313
registerAction2(VoiceChatInChatViewAction);
14+
registerAction2(HoldToVoiceChatInChatViewAction);
1415
registerAction2(QuickVoiceChatAction);
1516
registerAction2(InlineVoiceChatAction);
1617

0 commit comments

Comments
 (0)