@@ -9,11 +9,10 @@ import { firstOrDefault } from 'vs/base/common/arrays';
9
9
import { CancellationToken , CancellationTokenSource } from 'vs/base/common/cancellation' ;
10
10
import { Codicon } from 'vs/base/common/codicons' ;
11
11
import { Disposable , DisposableStore , MutableDisposable , toDisposable } from 'vs/base/common/lifecycle' ;
12
- import { ServicesAccessor } from 'vs/editor/browser/editorExtensions' ;
13
12
import { localize , localize2 } from 'vs/nls' ;
14
13
import { Action2 , IAction2Options , MenuId } from 'vs/platform/actions/common/actions' ;
15
14
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' ;
17
16
import { spinningLoading } from 'vs/platform/theme/common/iconRegistry' ;
18
17
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions' ;
19
18
import { IChatWidget , IChatWidgetService , IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat' ;
@@ -36,7 +35,7 @@ import { ColorScheme } from 'vs/platform/theme/common/theme';
36
35
import { Color } from 'vs/base/common/color' ;
37
36
import { contrastBorder , focusBorder } from 'vs/platform/theme/common/colorRegistry' ;
38
37
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' ;
40
39
import { AccessibilityVoiceSettingId , SpeechTimeoutDefault , accessibilityConfigurationNodeBase } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration' ;
41
40
import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions' ;
42
41
import { IWorkbenchContribution } from 'vs/workbench/common/contributions' ;
@@ -64,6 +63,7 @@ const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey<boolean>('voice
64
63
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." ) } ) ;
65
64
66
65
const CanVoiceChat = ContextKeyExpr . and ( CONTEXT_PROVIDER_EXISTS , HasSpeechProvider ) ;
66
+ const FocusInChatInput = assertIsDefined ( ContextKeyExpr . or ( CTX_INLINE_CHAT_FOCUSED , CONTEXT_IN_CHAT_INPUT ) ) ;
67
67
68
68
type VoiceChatSessionContext = 'inline' | 'quick' | 'view' | 'editor' ;
69
69
@@ -92,7 +92,6 @@ class VoiceChatSessionControllerFactory {
92
92
static create ( accessor : ServicesAccessor , context : 'inline' | 'quick' | 'view' | 'focused' ) : Promise < IVoiceChatSessionController | undefined > ;
93
93
static async create ( accessor : ServicesAccessor , context : 'inline' | 'quick' | 'view' | 'focused' ) : Promise < IVoiceChatSessionController | undefined > {
94
94
const chatWidgetService = accessor . get ( IChatWidgetService ) ;
95
- const chatService = accessor . get ( IChatService ) ;
96
95
const viewsService = accessor . get ( IViewsService ) ;
97
96
const chatContributionService = accessor . get ( IChatContributionService ) ;
98
97
const quickChatService = accessor . get ( IQuickChatService ) ;
@@ -136,12 +135,9 @@ class VoiceChatSessionControllerFactory {
136
135
137
136
// View Chat
138
137
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 ) ;
145
141
}
146
142
}
147
143
@@ -169,6 +165,18 @@ class VoiceChatSessionControllerFactory {
169
165
return undefined ;
170
166
}
171
167
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
+
172
180
private static doCreateForChatView ( chatView : IChatWidget , viewsService : IViewsService , chatContributionService : IChatContributionService ) : IVoiceChatSessionController {
173
181
return VoiceChatSessionControllerFactory . doCreateForChatViewOrEditor ( 'view' , chatView , viewsService , chatContributionService ) ;
174
182
}
@@ -414,7 +422,7 @@ async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor
414
422
let acceptVoice = false ;
415
423
const handle = disposableTimeout ( ( ) => {
416
424
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
418
426
} , VOICE_KEY_HOLD_THRESHOLD ) ;
419
427
420
428
const controller = await VoiceChatSessionControllerFactory . create ( accessor , target ) ;
@@ -459,6 +467,57 @@ export class VoiceChatInChatViewAction extends VoiceChatWithHoldModeAction {
459
467
}
460
468
}
461
469
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
+
462
521
export class InlineVoiceChatAction extends VoiceChatWithHoldModeAction {
463
522
464
523
static readonly ID = 'workbench.action.chat.inlineVoiceChat' ;
@@ -502,11 +561,8 @@ export class StartVoiceChatAction extends Action2 {
502
561
keybinding : {
503
562
weight : KeybindingWeight . WorkbenchContrib ,
504
563
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
510
566
CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS . negate ( ) ,
511
567
CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS . negate ( ) ,
512
568
CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS . negate ( ) ,
@@ -629,7 +685,6 @@ class BaseStopListeningAction extends Action2 {
629
685
category : CHAT_CATEGORY ,
630
686
keybinding : {
631
687
weight : KeybindingWeight . WorkbenchContrib + 100 ,
632
- when : ContextKeyExpr . and ( CanVoiceChat , context ) ,
633
688
primary : KeyCode . Escape
634
689
} ,
635
690
precondition : ContextKeyExpr . and ( CanVoiceChat , context ) ,
@@ -704,11 +759,7 @@ export class StopListeningAndSubmitAction extends Action2 {
704
759
f1 : true ,
705
760
keybinding : {
706
761
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 ,
712
763
primary : KeyMod . CtrlCmd | KeyCode . KeyI
713
764
} ,
714
765
precondition : ContextKeyExpr . and ( CanVoiceChat , CONTEXT_VOICE_CHAT_IN_PROGRESS )
0 commit comments