@@ -9,11 +9,10 @@ import { firstOrDefault } from 'vs/base/common/arrays';
99import { CancellationToken , CancellationTokenSource } from 'vs/base/common/cancellation' ;
1010import { Codicon } from 'vs/base/common/codicons' ;
1111import { Disposable , DisposableStore , MutableDisposable , toDisposable } from 'vs/base/common/lifecycle' ;
12- import { ServicesAccessor } from 'vs/editor/browser/editorExtensions' ;
1312import { localize , localize2 } from 'vs/nls' ;
1413import { Action2 , IAction2Options , MenuId } from 'vs/platform/actions/common/actions' ;
1514import { 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' ;
1716import { spinningLoading } from 'vs/platform/theme/common/iconRegistry' ;
1817import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions' ;
1918import { IChatWidget , IChatWidgetService , IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat' ;
@@ -36,7 +35,7 @@ import { ColorScheme } from 'vs/platform/theme/common/theme';
3635import { Color } from 'vs/base/common/color' ;
3736import { contrastBorder , focusBorder } from 'vs/platform/theme/common/colorRegistry' ;
3837import { IConfigurationService } from 'vs/platform/configuration/common/configuration' ;
39- import { isNumber } from 'vs/base/common/types' ;
38+ import { assertIsDefined , isNumber } from 'vs/base/common/types' ;
4039import { AccessibilityVoiceSettingId , SpeechTimeoutDefault , accessibilityConfigurationNodeBase } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration' ;
4140import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions' ;
4241import { IWorkbenchContribution } from 'vs/workbench/common/contributions' ;
@@ -64,6 +63,7 @@ const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey<boolean>('voice
6463const 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
6665const CanVoiceChat = ContextKeyExpr . and ( CONTEXT_PROVIDER_EXISTS , HasSpeechProvider ) ;
66+ const FocusInChatInput = assertIsDefined ( ContextKeyExpr . or ( CTX_INLINE_CHAT_FOCUSED , CONTEXT_IN_CHAT_INPUT ) ) ;
6767
6868type 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+
462521export 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 )
0 commit comments