Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js';
7
import { Event } from '../../../../../base/common/event.js';
8
import { Lazy } from '../../../../../base/common/lazy.js';
9
import { Disposable, DisposableStore, markAsSingleton, MutableDisposable } from '../../../../../base/common/lifecycle.js';
10
import Severity from '../../../../../base/common/severity.js';
11
import { equalsIgnoreCase } from '../../../../../base/common/strings.js';
12
import { URI } from '../../../../../base/common/uri.js';
13
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
14
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
15
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
16
import { localize, localize2 } from '../../../../../nls.js';
17
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
18
import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';
19
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
20
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
21
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
22
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
23
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
24
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
25
import { ILogService } from '../../../../../platform/log/common/log.js';
26
import { IMarkerService } from '../../../../../platform/markers/common/markers.js';
27
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
28
import product from '../../../../../platform/product/common/product.js';
29
import { IProductService } from '../../../../../platform/product/common/productService.js';
30
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
31
import { IWorkbenchContribution } from '../../../../common/contributions.js';
32
import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';
33
import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService, isProUser } from '../../../../services/chat/common/chatEntitlementService.js';
34
import { EnablementState, IWorkbenchExtensionEnablementService } from '../../../../services/extensionManagement/common/extensionManagement.js';
35
import { ExtensionUrlHandlerOverrideRegistry, IExtensionUrlHandlerOverride } from '../../../../services/extensions/browser/extensionUrlHandler.js';
36
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
37
import { IHostService } from '../../../../services/host/browser/host.js';
38
import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/browser/layoutService.js';
39
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
40
import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';
41
import { IExtension, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
42
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
43
import { IChatModeService } from '../../common/chatModes.js';
44
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
45
import { CHAT_CATEGORY, CHAT_SETUP_ACTION_ID, CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../actions/chatActions.js';
46
import { ChatViewContainerId, IChatWidgetService } from '../chat.js';
47
import { chatViewsWelcomeRegistry } from '../viewsWelcome/chatViewsWelcome.js';
48
import { ChatSetupAnonymous } from './chatSetup.js';
49
import { ChatSetupController } from './chatSetupController.js';
50
import { AICodeActionsHelper, AINewSymbolNamesProvider, ChatCodeActionsProvider, SetupAgent } from './chatSetupProviders.js';
51
import { ChatSetup } from './chatSetupRunner.js';
52
53
const defaultChat = {
54
chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '',
55
manageOveragesUrl: product.defaultChatAgent?.manageOverageUrl ?? '',
56
upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',
57
completionsRefreshTokenCommand: product.defaultChatAgent?.completionsRefreshTokenCommand ?? '',
58
chatRefreshTokenCommand: product.defaultChatAgent?.chatRefreshTokenCommand ?? '',
59
};
60
61
export class ChatSetupContribution extends Disposable implements IWorkbenchContribution {
62
63
static readonly ID = 'workbench.contrib.chatSetup';
64
65
constructor(
66
@IInstantiationService private readonly instantiationService: IInstantiationService,
67
@IChatEntitlementService chatEntitlementService: ChatEntitlementService,
68
@ILogService private readonly logService: ILogService,
69
@IContextKeyService private readonly contextKeyService: IContextKeyService,
70
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
71
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
72
@IExtensionService private readonly extensionService: IExtensionService,
73
@IEnvironmentService private readonly environmentService: IEnvironmentService,
74
) {
75
super();
76
77
const context = chatEntitlementService.context?.value;
78
const requests = chatEntitlementService.requests?.value;
79
if (!context || !requests) {
80
return; // disabled
81
}
82
83
const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests)));
84
85
this.registerSetupAgents(context, controller);
86
this.registerActions(context, requests, controller);
87
this.registerUrlLinkHandler();
88
this.checkExtensionInstallation(context);
89
}
90
91
private registerSetupAgents(context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): void {
92
const defaultAgentDisposables = markAsSingleton(new MutableDisposable()); // prevents flicker on window reload
93
const vscodeAgentDisposables = markAsSingleton(new MutableDisposable());
94
95
const renameProviderDisposables = markAsSingleton(new MutableDisposable());
96
const codeActionsProviderDisposables = markAsSingleton(new MutableDisposable());
97
98
const updateRegistration = () => {
99
100
// Agent + Tools
101
{
102
if (!context.state.hidden && !context.state.disabled) {
103
104
// Default Agents (always, even if installed to allow for speedy requests right on startup)
105
if (!defaultAgentDisposables.value) {
106
const disposables = defaultAgentDisposables.value = new DisposableStore();
107
108
// Panel Agents
109
const panelAgentDisposables = disposables.add(new DisposableStore());
110
for (const mode of [ChatModeKind.Ask, ChatModeKind.Edit, ChatModeKind.Agent]) {
111
const { agent, disposable } = SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Chat, mode, context, controller);
112
panelAgentDisposables.add(disposable);
113
panelAgentDisposables.add(agent.onUnresolvableError(() => {
114
const panelAgentHasGuidance = chatViewsWelcomeRegistry.get().some(descriptor => this.contextKeyService.contextMatchesRules(descriptor.when));
115
if (panelAgentHasGuidance) {
116
// An unresolvable error from our agent registrations means that
117
// Chat is unhealthy for some reason. We clear our panel
118
// registration to give Chat a chance to show a custom message
119
// to the user from the views and stop pretending as if there was
120
// a functional agent.
121
this.logService.error('[chat setup] Unresolvable error from Chat agent registration, clearing registration.');
122
panelAgentDisposables.dispose();
123
}
124
}));
125
}
126
127
// Inline Agents
128
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Terminal, undefined, context, controller).disposable);
129
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Notebook, undefined, context, controller).disposable);
130
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.EditorInline, undefined, context, controller).disposable);
131
}
132
133
// Built-In Agent + Tool (unless installed, signed-in and enabled)
134
if ((!context.state.installed || context.state.entitlement === ChatEntitlement.Unknown || context.state.entitlement === ChatEntitlement.Unresolved) && !vscodeAgentDisposables.value) {
135
const disposables = vscodeAgentDisposables.value = new DisposableStore();
136
disposables.add(SetupAgent.registerBuiltInAgents(this.instantiationService, context, controller));
137
}
138
} else {
139
defaultAgentDisposables.clear();
140
vscodeAgentDisposables.clear();
141
}
142
143
if (context.state.installed && !context.state.disabled) {
144
vscodeAgentDisposables.clear(); // we need to do this to prevent showing duplicate agent/tool entries in the list
145
}
146
}
147
148
// Rename Provider
149
{
150
if (!context.state.installed && !context.state.hidden && !context.state.disabled) {
151
if (!renameProviderDisposables.value) {
152
renameProviderDisposables.value = AINewSymbolNamesProvider.registerProvider(this.instantiationService, context, controller);
153
}
154
} else {
155
renameProviderDisposables.clear();
156
}
157
}
158
159
// Code Actions Provider
160
{
161
if (!context.state.installed && !context.state.hidden && !context.state.disabled) {
162
if (!codeActionsProviderDisposables.value) {
163
codeActionsProviderDisposables.value = ChatCodeActionsProvider.registerProvider(this.instantiationService);
164
}
165
} else {
166
codeActionsProviderDisposables.clear();
167
}
168
}
169
};
170
171
this._register(Event.runAndSubscribe(context.onDidChange, () => updateRegistration()));
172
}
173
174
private registerActions(context: ChatEntitlementContext, requests: ChatEntitlementRequests, controller: Lazy<ChatSetupController>): void {
175
176
//#region Global Chat Setup Actions
177
178
class ChatSetupTriggerAction extends Action2 {
179
180
static CHAT_SETUP_ACTION_LABEL = localize2('triggerChatSetup', "Use AI Features with Copilot for free...");
181
182
constructor() {
183
super({
184
id: CHAT_SETUP_ACTION_ID,
185
title: ChatSetupTriggerAction.CHAT_SETUP_ACTION_LABEL,
186
category: CHAT_CATEGORY,
187
f1: true,
188
precondition: ContextKeyExpr.or(
189
ChatContextKeys.Setup.hidden,
190
ChatContextKeys.Setup.disabled,
191
ChatContextKeys.Setup.untrusted,
192
ChatContextKeys.Setup.installed.negate(),
193
ChatContextKeys.Entitlement.canSignUp
194
)
195
});
196
}
197
198
override async run(accessor: ServicesAccessor, mode?: ChatModeKind | string, options?: { forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous; inputValue?: string }): Promise<boolean> {
199
const widgetService = accessor.get(IChatWidgetService);
200
const instantiationService = accessor.get(IInstantiationService);
201
const dialogService = accessor.get(IDialogService);
202
const commandService = accessor.get(ICommandService);
203
const lifecycleService = accessor.get(ILifecycleService);
204
const configurationService = accessor.get(IConfigurationService);
205
206
await context.update({ hidden: false });
207
configurationService.updateValue(ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY, false);
208
209
if (mode) {
210
const chatWidget = await widgetService.revealWidget();
211
chatWidget?.input.setChatMode(mode);
212
}
213
214
if (options?.inputValue) {
215
const chatWidget = await widgetService.revealWidget();
216
chatWidget?.setInput(options.inputValue);
217
}
218
219
const setup = ChatSetup.getInstance(instantiationService, context, controller);
220
const { success } = await setup.run(options);
221
if (success === false && !lifecycleService.willShutdown) {
222
const { confirmed } = await dialogService.confirm({
223
type: Severity.Error,
224
message: localize('setupErrorDialog', "Chat setup failed. Would you like to try again?"),
225
primaryButton: localize('retry', "Retry"),
226
});
227
228
if (confirmed) {
229
return Boolean(await commandService.executeCommand(CHAT_SETUP_ACTION_ID, mode, options));
230
}
231
}
232
233
return Boolean(success);
234
}
235
}
236
237
class ChatSetupTriggerSupportAnonymousAction extends Action2 {
238
239
constructor() {
240
super({
241
id: CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID,
242
title: ChatSetupTriggerAction.CHAT_SETUP_ACTION_LABEL
243
});
244
}
245
246
override async run(accessor: ServicesAccessor): Promise<unknown> {
247
const commandService = accessor.get(ICommandService);
248
const telemetryService = accessor.get(ITelemetryService);
249
const chatEntitlementService = accessor.get(IChatEntitlementService);
250
251
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'api' });
252
253
return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, {
254
forceAnonymous: chatEntitlementService.anonymous ? ChatSetupAnonymous.EnabledWithDialog : undefined
255
});
256
}
257
}
258
259
class ChatSetupTriggerForceSignInDialogAction extends Action2 {
260
261
constructor() {
262
super({
263
id: 'workbench.action.chat.triggerSetupForceSignIn',
264
title: localize2('forceSignIn', "Sign in to use AI features")
265
});
266
}
267
268
override async run(accessor: ServicesAccessor): Promise<unknown> {
269
const commandService = accessor.get(ICommandService);
270
const telemetryService = accessor.get(ITelemetryService);
271
272
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'api' });
273
274
return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, { forceSignInDialog: true });
275
}
276
}
277
278
class ChatSetupTriggerAnonymousWithoutDialogAction extends Action2 {
279
280
constructor() {
281
super({
282
id: 'workbench.action.chat.triggerSetupAnonymousWithoutDialog',
283
title: ChatSetupTriggerAction.CHAT_SETUP_ACTION_LABEL
284
});
285
}
286
287
override async run(accessor: ServicesAccessor): Promise<unknown> {
288
const commandService = accessor.get(ICommandService);
289
const telemetryService = accessor.get(ITelemetryService);
290
291
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'api' });
292
293
return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, { forceAnonymous: ChatSetupAnonymous.EnabledWithoutDialog });
294
}
295
}
296
297
class ChatSetupFromAccountsAction extends Action2 {
298
299
constructor() {
300
super({
301
id: 'workbench.action.chat.triggerSetupFromAccounts',
302
title: localize2('triggerChatSetupFromAccounts', "Sign in to use AI features..."),
303
menu: {
304
id: MenuId.AccountsContext,
305
group: '2_copilot',
306
when: ContextKeyExpr.and(
307
ChatContextKeys.Setup.hidden.negate(),
308
ChatContextKeys.Setup.installed.negate(),
309
ChatContextKeys.Entitlement.signedOut
310
)
311
}
312
});
313
}
314
315
override async run(accessor: ServicesAccessor): Promise<void> {
316
const commandService = accessor.get(ICommandService);
317
const telemetryService = accessor.get(ITelemetryService);
318
319
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'accounts' });
320
321
return commandService.executeCommand(CHAT_SETUP_ACTION_ID);
322
}
323
}
324
325
const windowFocusListener = this._register(new MutableDisposable());
326
class UpgradePlanAction extends Action2 {
327
constructor() {
328
super({
329
id: 'workbench.action.chat.upgradePlan',
330
title: localize2('managePlan', "Upgrade to GitHub Copilot Pro"),
331
category: localize2('chat.category', 'Chat'),
332
f1: true,
333
precondition: ContextKeyExpr.and(
334
ChatContextKeys.Setup.hidden.negate(),
335
ContextKeyExpr.or(
336
ChatContextKeys.Entitlement.canSignUp,
337
ChatContextKeys.Entitlement.planFree
338
)
339
),
340
menu: {
341
id: MenuId.ChatTitleBarMenu,
342
group: 'a_first',
343
order: 1,
344
when: ContextKeyExpr.and(
345
ChatContextKeys.Entitlement.planFree,
346
ContextKeyExpr.or(
347
ChatContextKeys.chatQuotaExceeded,
348
ChatContextKeys.completionsQuotaExceeded
349
)
350
)
351
}
352
});
353
}
354
355
override async run(accessor: ServicesAccessor): Promise<void> {
356
const openerService = accessor.get(IOpenerService);
357
const hostService = accessor.get(IHostService);
358
const commandService = accessor.get(ICommandService);
359
360
openerService.open(URI.parse(defaultChat.upgradePlanUrl));
361
362
const entitlement = context.state.entitlement;
363
if (!isProUser(entitlement)) {
364
// If the user is not yet Pro, we listen to window focus to refresh the token
365
// when the user has come back to the window assuming the user signed up.
366
windowFocusListener.value = hostService.onDidChangeFocus(focus => this.onWindowFocus(focus, commandService));
367
}
368
}
369
370
private async onWindowFocus(focus: boolean, commandService: ICommandService): Promise<void> {
371
if (focus) {
372
windowFocusListener.clear();
373
374
const entitlements = await requests.forceResolveEntitlement(undefined);
375
if (entitlements?.entitlement && isProUser(entitlements?.entitlement)) {
376
refreshTokens(commandService);
377
}
378
}
379
}
380
}
381
382
class EnableOveragesAction extends Action2 {
383
constructor() {
384
super({
385
id: 'workbench.action.chat.manageOverages',
386
title: localize2('manageOverages', "Manage GitHub Copilot Overages"),
387
category: localize2('chat.category', 'Chat'),
388
f1: true,
389
precondition: ContextKeyExpr.and(
390
ChatContextKeys.Setup.hidden.negate(),
391
ContextKeyExpr.or(
392
ChatContextKeys.Entitlement.planPro,
393
ChatContextKeys.Entitlement.planProPlus,
394
)
395
),
396
menu: {
397
id: MenuId.ChatTitleBarMenu,
398
group: 'a_first',
399
order: 1,
400
when: ContextKeyExpr.and(
401
ContextKeyExpr.or(
402
ChatContextKeys.Entitlement.planPro,
403
ChatContextKeys.Entitlement.planProPlus,
404
),
405
ContextKeyExpr.or(
406
ChatContextKeys.chatQuotaExceeded,
407
ChatContextKeys.completionsQuotaExceeded
408
)
409
)
410
}
411
});
412
}
413
414
override async run(accessor: ServicesAccessor): Promise<void> {
415
const openerService = accessor.get(IOpenerService);
416
openerService.open(URI.parse(defaultChat.manageOveragesUrl));
417
}
418
}
419
420
registerAction2(ChatSetupTriggerAction);
421
registerAction2(ChatSetupTriggerForceSignInDialogAction);
422
registerAction2(ChatSetupFromAccountsAction);
423
registerAction2(ChatSetupTriggerAnonymousWithoutDialogAction);
424
registerAction2(ChatSetupTriggerSupportAnonymousAction);
425
registerAction2(UpgradePlanAction);
426
registerAction2(EnableOveragesAction);
427
428
//#endregion
429
430
//#region Editor Context Menu
431
432
function registerGenerateCodeCommand(coreCommand: 'chat.internal.explain' | 'chat.internal.fix' | 'chat.internal.review' | 'chat.internal.generateDocs' | 'chat.internal.generateTests', actualCommand: string): void {
433
434
CommandsRegistry.registerCommand(coreCommand, async accessor => {
435
const commandService = accessor.get(ICommandService);
436
const codeEditorService = accessor.get(ICodeEditorService);
437
const markerService = accessor.get(IMarkerService);
438
439
switch (coreCommand) {
440
case 'chat.internal.explain':
441
case 'chat.internal.fix': {
442
const textEditor = codeEditorService.getActiveCodeEditor();
443
const uri = textEditor?.getModel()?.uri;
444
const range = textEditor?.getSelection();
445
if (!uri || !range) {
446
return;
447
}
448
449
const markers = AICodeActionsHelper.warningOrErrorMarkersAtRange(markerService, uri, range);
450
451
const actualCommand = coreCommand === 'chat.internal.explain'
452
? AICodeActionsHelper.explainMarkers(markers)
453
: AICodeActionsHelper.fixMarkers(markers, range);
454
455
await commandService.executeCommand(actualCommand.id, ...(actualCommand.arguments ?? []));
456
457
break;
458
}
459
case 'chat.internal.review':
460
case 'chat.internal.generateDocs':
461
case 'chat.internal.generateTests': {
462
const result = await commandService.executeCommand(CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID);
463
if (result) {
464
await commandService.executeCommand(actualCommand);
465
}
466
}
467
}
468
});
469
}
470
registerGenerateCodeCommand('chat.internal.explain', 'github.copilot.chat.explain');
471
registerGenerateCodeCommand('chat.internal.fix', 'github.copilot.chat.fix');
472
registerGenerateCodeCommand('chat.internal.review', 'github.copilot.chat.review');
473
registerGenerateCodeCommand('chat.internal.generateDocs', 'github.copilot.chat.generateDocs');
474
registerGenerateCodeCommand('chat.internal.generateTests', 'github.copilot.chat.generateTests');
475
476
const internalGenerateCodeContext = ContextKeyExpr.and(
477
ChatContextKeys.Setup.hidden.negate(),
478
ChatContextKeys.Setup.disabled.negate(),
479
ChatContextKeys.Setup.installed.negate(),
480
);
481
482
MenuRegistry.appendMenuItem(MenuId.EditorContext, {
483
command: {
484
id: 'chat.internal.explain',
485
title: localize('explain', "Explain"),
486
},
487
group: '1_chat',
488
order: 4,
489
when: internalGenerateCodeContext
490
});
491
492
MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {
493
command: {
494
id: 'chat.internal.fix',
495
title: localize('fix', "Fix"),
496
},
497
group: '1_action',
498
order: 1,
499
when: ContextKeyExpr.and(
500
internalGenerateCodeContext,
501
EditorContextKeys.readOnly.negate()
502
)
503
});
504
505
MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {
506
command: {
507
id: 'chat.internal.review',
508
title: localize('review', "Code Review"),
509
},
510
group: '1_action',
511
order: 2,
512
when: internalGenerateCodeContext
513
});
514
515
MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {
516
command: {
517
id: 'chat.internal.generateDocs',
518
title: localize('generateDocs', "Generate Docs"),
519
},
520
group: '2_generate',
521
order: 1,
522
when: ContextKeyExpr.and(
523
internalGenerateCodeContext,
524
EditorContextKeys.readOnly.negate()
525
)
526
});
527
528
MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {
529
command: {
530
id: 'chat.internal.generateTests',
531
title: localize('generateTests', "Generate Tests"),
532
},
533
group: '2_generate',
534
order: 2,
535
when: ContextKeyExpr.and(
536
internalGenerateCodeContext,
537
EditorContextKeys.readOnly.negate()
538
)
539
});
540
}
541
542
private registerUrlLinkHandler(): void {
543
this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler(this.instantiationService.createInstance(ChatSetupExtensionUrlHandler)));
544
}
545
546
private async checkExtensionInstallation(context: ChatEntitlementContext): Promise<void> {
547
548
// When developing extensions, await registration and then check
549
if (this.environmentService.isExtensionDevelopment) {
550
await this.extensionService.whenInstalledExtensionsRegistered();
551
if (this.extensionService.extensions.find(ext => ExtensionIdentifier.equals(ext.identifier, defaultChat.chatExtensionId))) {
552
context.update({ installed: true, disabled: false, untrusted: false });
553
return;
554
}
555
}
556
557
// Await extensions to be ready to be queried
558
await this.extensionsWorkbenchService.queryLocal();
559
560
// Listen to extensions change and process extensions once
561
this._register(Event.runAndSubscribe<IExtension | undefined>(this.extensionsWorkbenchService.onChange, e => {
562
if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.chatExtensionId)) {
563
return; // unrelated event
564
}
565
566
const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.chatExtensionId));
567
const installed = !!defaultChatExtension?.local;
568
569
let disabled: boolean;
570
let untrusted = false;
571
if (installed) {
572
disabled = !this.extensionEnablementService.isEnabled(defaultChatExtension.local);
573
if (disabled) {
574
const state = this.extensionEnablementService.getEnablementState(defaultChatExtension.local);
575
if (state === EnablementState.DisabledByTrustRequirement) {
576
disabled = false; // not disabled by user choice but
577
untrusted = true; // by missing workspace trust
578
}
579
}
580
} else {
581
disabled = false;
582
}
583
584
context.update({ installed, disabled, untrusted });
585
}));
586
}
587
}
588
589
class ChatSetupExtensionUrlHandler implements IExtensionUrlHandlerOverride {
590
constructor(
591
@IProductService private readonly productService: IProductService,
592
@ICommandService private readonly commandService: ICommandService,
593
@ITelemetryService private readonly telemetryService: ITelemetryService,
594
@IChatModeService private readonly chatModeService: IChatModeService,
595
) { }
596
597
canHandleURL(url: URI): boolean {
598
return url.scheme === this.productService.urlProtocol && equalsIgnoreCase(url.authority, defaultChat.chatExtensionId);
599
}
600
601
async handleURL(url: URI): Promise<boolean> {
602
const params = new URLSearchParams(url.query);
603
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'url', detail: params.get('referrer') ?? undefined });
604
605
const agentParam = params.get('agent') ?? params.get('mode');
606
const inputParam = params.get('prompt');
607
if (!agentParam && !inputParam) {
608
return false;
609
}
610
611
const agentId = agentParam ? this.resolveAgentId(agentParam) : undefined;
612
await this.commandService.executeCommand(CHAT_SETUP_ACTION_ID, agentId, inputParam ? { inputValue: inputParam } : undefined);
613
return true;
614
}
615
616
private resolveAgentId(agentParam: string): string | undefined {
617
const agents = this.chatModeService.getModes();
618
const allAgents = [...agents.builtin, ...agents.custom];
619
620
const foundAgent = allAgents.find(agent => agent.id === agentParam);
621
if (foundAgent) {
622
return foundAgent.id;
623
}
624
625
const nameLower = agentParam.toLowerCase();
626
const agentByName = allAgents.find(agent => agent.name.get().toLowerCase() === nameLower);
627
return agentByName?.id;
628
}
629
}
630
631
export class ChatTeardownContribution extends Disposable implements IWorkbenchContribution {
632
633
static readonly ID = 'workbench.contrib.chatTeardown';
634
635
static readonly CHAT_DISABLED_CONFIGURATION_KEY = 'chat.disableAIFeatures';
636
637
constructor(
638
@IChatEntitlementService chatEntitlementService: ChatEntitlementService,
639
@IConfigurationService private readonly configurationService: IConfigurationService,
640
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
641
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
642
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
643
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService
644
) {
645
super();
646
647
const context = chatEntitlementService.context?.value;
648
if (!context) {
649
return; // disabled
650
}
651
652
this.registerListeners();
653
this.registerActions();
654
655
this.handleChatDisabled(false);
656
}
657
658
private handleChatDisabled(fromEvent: boolean): void {
659
const chatDisabled = this.configurationService.inspect(ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY);
660
if (chatDisabled.value === true) {
661
this.maybeEnableOrDisableExtension(typeof chatDisabled.workspaceValue === 'boolean' ? EnablementState.DisabledWorkspace : EnablementState.DisabledGlobally);
662
if (fromEvent) {
663
this.maybeHideAuxiliaryBar();
664
}
665
} else if (chatDisabled.value === false && fromEvent /* do not enable extensions unless its an explicit settings change */) {
666
this.maybeEnableOrDisableExtension(typeof chatDisabled.workspaceValue === 'boolean' ? EnablementState.EnabledWorkspace : EnablementState.EnabledGlobally);
667
}
668
}
669
670
private async registerListeners(): Promise<void> {
671
672
// Configuration changes
673
this._register(this.configurationService.onDidChangeConfiguration(e => {
674
if (!e.affectsConfiguration(ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY)) {
675
return;
676
}
677
678
this.handleChatDisabled(true);
679
}));
680
681
// Extension installation
682
await this.extensionsWorkbenchService.queryLocal();
683
this._register(this.extensionsWorkbenchService.onChange(e => {
684
if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.chatExtensionId)) {
685
return; // unrelated event
686
}
687
688
const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.chatExtensionId));
689
if (defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local)) {
690
this.configurationService.updateValue(ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY, false);
691
}
692
}));
693
}
694
695
private async maybeEnableOrDisableExtension(state: EnablementState.EnabledGlobally | EnablementState.EnabledWorkspace | EnablementState.DisabledGlobally | EnablementState.DisabledWorkspace): Promise<void> {
696
const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.chatExtensionId));
697
if (!defaultChatExtension) {
698
return;
699
}
700
701
await this.extensionsWorkbenchService.setEnablement([defaultChatExtension], state);
702
await this.extensionsWorkbenchService.updateRunningExtensions(state === EnablementState.EnabledGlobally || state === EnablementState.EnabledWorkspace ? localize('restartExtensionHost.reason.enable', "Enabling AI features") : localize('restartExtensionHost.reason.disable', "Disabling AI features"));
703
}
704
705
private maybeHideAuxiliaryBar(): void {
706
const activeContainers = this.viewDescriptorService.getViewContainersByLocation(ViewContainerLocation.AuxiliaryBar).filter(
707
container => this.viewDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0
708
);
709
if (
710
(activeContainers.length === 0) || // chat view is already gone but we know it was there before
711
(activeContainers.length === 1 && activeContainers.at(0)?.id === ChatViewContainerId) // chat view is the only view which is going to go away
712
) {
713
this.layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar
714
}
715
}
716
717
private registerActions(): void {
718
719
class ChatSetupHideAction extends Action2 {
720
721
static readonly ID = 'workbench.action.chat.hideSetup';
722
static readonly TITLE = localize2('hideChatSetup', "Learn How to Hide AI Features");
723
724
constructor() {
725
super({
726
id: ChatSetupHideAction.ID,
727
title: ChatSetupHideAction.TITLE,
728
f1: true,
729
category: CHAT_CATEGORY,
730
precondition: ChatContextKeys.Setup.hidden.negate(),
731
menu: {
732
id: MenuId.ChatTitleBarMenu,
733
group: 'z_hide',
734
order: 1,
735
when: ChatContextKeys.Setup.installed.negate()
736
}
737
});
738
}
739
740
override async run(accessor: ServicesAccessor): Promise<void> {
741
const preferencesService = accessor.get(IPreferencesService);
742
743
preferencesService.openSettings({ jsonEditor: false, query: `@id:${ChatTeardownContribution.CHAT_DISABLED_CONFIGURATION_KEY}` });
744
}
745
}
746
747
registerAction2(ChatSetupHideAction);
748
}
749
}
750
751
//#endregion
752
753
export function refreshTokens(commandService: ICommandService): void {
754
// ugly, but we need to signal to the extension that entitlements changed
755
commandService.executeCommand(defaultChat.completionsRefreshTokenCommand);
756
commandService.executeCommand(defaultChat.chatRefreshTokenCommand);
757
}
758
759