Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts
3296 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 product from '../../../../platform/product/common/product.js';
7
import { Barrier } from '../../../../base/common/async.js';
8
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
9
import { Emitter, Event } from '../../../../base/common/event.js';
10
import { Lazy } from '../../../../base/common/lazy.js';
11
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
12
import { IRequestContext } from '../../../../base/parts/request/common/request.js';
13
import { localize } from '../../../../nls.js';
14
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
15
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
16
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
17
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
18
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
19
import { ILogService } from '../../../../platform/log/common/log.js';
20
import { IProductService } from '../../../../platform/product/common/productService.js';
21
import { asText, IRequestService } from '../../../../platform/request/common/request.js';
22
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
23
import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js';
24
import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js';
25
import { EnablementState, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js';
26
import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
27
import { ChatContextKeys } from './chatContextKeys.js';
28
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
29
import { URI } from '../../../../base/common/uri.js';
30
import Severity from '../../../../base/common/severity.js';
31
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
32
import { isWeb } from '../../../../base/common/platform.js';
33
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
34
import { Mutable } from '../../../../base/common/types.js';
35
import { distinct } from '../../../../base/common/arrays.js';
36
37
export const IChatEntitlementService = createDecorator<IChatEntitlementService>('chatEntitlementService');
38
39
export enum ChatEntitlement {
40
/** Signed out */
41
Unknown = 1,
42
/** Signed in but not yet resolved */
43
Unresolved,
44
/** Signed in and entitled to Free */
45
Available,
46
/** Signed in but not entitled to Free */
47
Unavailable,
48
/** Signed-up to Free */
49
Free,
50
/** Signed-up to Pro */
51
Pro,
52
/** Signed-up to Pro Plus */
53
ProPlus,
54
/** Signed-up to Business */
55
Business,
56
/** Signed-up to Enterprise */
57
Enterprise
58
}
59
60
export interface IChatSentiment {
61
62
/**
63
* User has Chat installed.
64
*/
65
installed?: boolean;
66
67
/**
68
* User signals no intent in using Chat.
69
*
70
* Note: in contrast to `disabled`, this should not only disable
71
* Chat but also hide all of its UI.
72
*/
73
hidden?: boolean;
74
75
/**
76
* User signals intent to disable Chat.
77
*
78
* Note: in contrast to `hidden`, this should not hide
79
* Chat but but disable its functionality.
80
*/
81
disabled?: boolean;
82
83
/**
84
* Chat is disabled due to missing workspace trust.
85
*
86
* Note: even though this disables Chat, we want to treat it
87
* different from the `disabled` state that is by explicit
88
* user choice.
89
*/
90
untrusted?: boolean;
91
92
/**
93
* User signals intent to use Chat later.
94
*/
95
later?: boolean;
96
}
97
98
export interface IChatEntitlementService {
99
100
_serviceBrand: undefined;
101
102
readonly onDidChangeEntitlement: Event<void>;
103
104
readonly entitlement: ChatEntitlement;
105
106
readonly organisations: string[] | undefined;
107
readonly isInternal: boolean;
108
readonly sku: string | undefined;
109
110
readonly onDidChangeQuotaExceeded: Event<void>;
111
readonly onDidChangeQuotaRemaining: Event<void>;
112
113
readonly quotas: IQuotas;
114
115
update(token: CancellationToken): Promise<void>;
116
117
readonly onDidChangeSentiment: Event<void>;
118
119
readonly sentiment: IChatSentiment;
120
}
121
122
//#region Helper Functions
123
124
/**
125
* Checks the chat entitlements to see if the user falls into the paid category
126
* @param chatEntitlement The chat entitlement to check
127
* @returns Whether or not they are a paid user
128
*/
129
export function isProUser(chatEntitlement: ChatEntitlement): boolean {
130
return chatEntitlement === ChatEntitlement.Pro ||
131
chatEntitlement === ChatEntitlement.ProPlus ||
132
chatEntitlement === ChatEntitlement.Business ||
133
chatEntitlement === ChatEntitlement.Enterprise;
134
}
135
136
//#region Service Implementation
137
138
const defaultChat = {
139
extensionId: product.defaultChatAgent?.extensionId ?? '',
140
chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '',
141
upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',
142
provider: product.defaultChatAgent?.provider ?? { default: { id: '' }, enterprise: { id: '' } },
143
providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '',
144
providerScopes: product.defaultChatAgent?.providerScopes ?? [[]],
145
entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '',
146
entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '',
147
completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '',
148
chatQuotaExceededContext: product.defaultChatAgent?.chatQuotaExceededContext ?? '',
149
completionsQuotaExceededContext: product.defaultChatAgent?.completionsQuotaExceededContext ?? ''
150
};
151
152
interface IChatQuotasAccessor {
153
clearQuotas(): void;
154
acceptQuotas(quotas: IQuotas): void;
155
}
156
157
export class ChatEntitlementService extends Disposable implements IChatEntitlementService {
158
159
declare _serviceBrand: undefined;
160
161
readonly context: Lazy<ChatEntitlementContext> | undefined;
162
readonly requests: Lazy<ChatEntitlementRequests> | undefined;
163
164
constructor(
165
@IInstantiationService instantiationService: IInstantiationService,
166
@IProductService productService: IProductService,
167
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
168
@IContextKeyService private readonly contextKeyService: IContextKeyService,
169
@IConfigurationService configurationService: IConfigurationService
170
) {
171
super();
172
173
this.chatQuotaExceededContextKey = ChatContextKeys.chatQuotaExceeded.bindTo(this.contextKeyService);
174
this.completionsQuotaExceededContextKey = ChatContextKeys.completionsQuotaExceeded.bindTo(this.contextKeyService);
175
176
this.onDidChangeEntitlement = Event.map(
177
Event.filter(
178
this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([
179
ChatContextKeys.Entitlement.planPro.key,
180
ChatContextKeys.Entitlement.planBusiness.key,
181
ChatContextKeys.Entitlement.planEnterprise.key,
182
ChatContextKeys.Entitlement.planProPlus.key,
183
ChatContextKeys.Entitlement.planFree.key,
184
ChatContextKeys.Entitlement.canSignUp.key,
185
ChatContextKeys.Entitlement.signedOut.key,
186
ChatContextKeys.Entitlement.organisations.key,
187
ChatContextKeys.Entitlement.internal.key,
188
ChatContextKeys.Entitlement.sku.key
189
])), this._store
190
), () => { }, this._store
191
);
192
193
this.onDidChangeSentiment = Event.map(
194
Event.filter(
195
this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([
196
ChatContextKeys.Setup.hidden.key,
197
ChatContextKeys.Setup.disabled.key,
198
ChatContextKeys.Setup.untrusted.key,
199
ChatContextKeys.Setup.installed.key,
200
ChatContextKeys.Setup.later.key
201
])), this._store
202
), () => { }, this._store
203
);
204
205
if (
206
!productService.defaultChatAgent || // needs product config
207
(
208
// TODO@bpasero remove this condition and 'serverlessWebEnabled' once Chat web support lands
209
isWeb &&
210
!environmentService.remoteAuthority &&
211
!configurationService.getValue('chat.experimental.serverlessWebEnabled')
212
)
213
) {
214
ChatContextKeys.Setup.hidden.bindTo(this.contextKeyService).set(true); // hide copilot UI
215
return;
216
}
217
218
const context = this.context = new Lazy(() => this._register(instantiationService.createInstance(ChatEntitlementContext)));
219
this.requests = new Lazy(() => this._register(instantiationService.createInstance(ChatEntitlementRequests, context.value, {
220
clearQuotas: () => this.clearQuotas(),
221
acceptQuotas: quotas => this.acceptQuotas(quotas)
222
})));
223
224
this.registerListeners();
225
}
226
227
//#region --- Entitlements
228
229
readonly onDidChangeEntitlement: Event<void>;
230
231
get entitlement(): ChatEntitlement {
232
if (this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.planPro.key) === true) {
233
return ChatEntitlement.Pro;
234
} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.planBusiness.key) === true) {
235
return ChatEntitlement.Business;
236
} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.planEnterprise.key) === true) {
237
return ChatEntitlement.Enterprise;
238
} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.planProPlus.key) === true) {
239
return ChatEntitlement.ProPlus;
240
} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.planFree.key) === true) {
241
return ChatEntitlement.Free;
242
} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.canSignUp.key) === true) {
243
return ChatEntitlement.Available;
244
} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.signedOut.key) === true) {
245
return ChatEntitlement.Unknown;
246
}
247
248
return ChatEntitlement.Unresolved;
249
}
250
251
get isInternal(): boolean {
252
return this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.internal.key) === true;
253
}
254
255
get organisations(): string[] | undefined {
256
return this.contextKeyService.getContextKeyValue<string[]>(ChatContextKeys.Entitlement.organisations.key);
257
}
258
259
get sku(): string | undefined {
260
return this.contextKeyService.getContextKeyValue<string>(ChatContextKeys.Entitlement.sku.key);
261
}
262
263
//#endregion
264
265
//#region --- Quotas
266
267
private readonly _onDidChangeQuotaExceeded = this._register(new Emitter<void>());
268
readonly onDidChangeQuotaExceeded = this._onDidChangeQuotaExceeded.event;
269
270
private readonly _onDidChangeQuotaRemaining = this._register(new Emitter<void>());
271
readonly onDidChangeQuotaRemaining = this._onDidChangeQuotaRemaining.event;
272
273
private _quotas: IQuotas = {};
274
get quotas() { return this._quotas; }
275
276
private readonly chatQuotaExceededContextKey: IContextKey<boolean>;
277
private readonly completionsQuotaExceededContextKey: IContextKey<boolean>;
278
279
private ExtensionQuotaContextKeys = {
280
chatQuotaExceeded: defaultChat.chatQuotaExceededContext,
281
completionsQuotaExceeded: defaultChat.completionsQuotaExceededContext,
282
};
283
284
private registerListeners(): void {
285
const quotaExceededSet = new Set([this.ExtensionQuotaContextKeys.chatQuotaExceeded, this.ExtensionQuotaContextKeys.completionsQuotaExceeded]);
286
287
const cts = this._register(new MutableDisposable<CancellationTokenSource>());
288
this._register(this.contextKeyService.onDidChangeContext(e => {
289
if (e.affectsSome(quotaExceededSet)) {
290
if (cts.value) {
291
cts.value.cancel();
292
}
293
cts.value = new CancellationTokenSource();
294
this.update(cts.value.token);
295
}
296
}));
297
}
298
299
acceptQuotas(quotas: IQuotas): void {
300
const oldQuota = this._quotas;
301
this._quotas = quotas;
302
this.updateContextKeys();
303
304
const { changed: chatChanged } = this.compareQuotas(oldQuota.chat, quotas.chat);
305
const { changed: completionsChanged } = this.compareQuotas(oldQuota.completions, quotas.completions);
306
const { changed: premiumChatChanged } = this.compareQuotas(oldQuota.premiumChat, quotas.premiumChat);
307
308
if (chatChanged.exceeded || completionsChanged.exceeded || premiumChatChanged.exceeded) {
309
this._onDidChangeQuotaExceeded.fire();
310
}
311
312
if (chatChanged.remaining || completionsChanged.remaining || premiumChatChanged.remaining) {
313
this._onDidChangeQuotaRemaining.fire();
314
}
315
}
316
317
private compareQuotas(oldQuota: IQuotaSnapshot | undefined, newQuota: IQuotaSnapshot | undefined): { changed: { exceeded: boolean; remaining: boolean } } {
318
return {
319
changed: {
320
exceeded: (oldQuota?.percentRemaining === 0) !== (newQuota?.percentRemaining === 0),
321
remaining: oldQuota?.percentRemaining !== newQuota?.percentRemaining
322
}
323
};
324
}
325
326
clearQuotas(): void {
327
this.acceptQuotas({});
328
}
329
330
private updateContextKeys(): void {
331
this.chatQuotaExceededContextKey.set(this._quotas.chat?.percentRemaining === 0);
332
this.completionsQuotaExceededContextKey.set(this._quotas.completions?.percentRemaining === 0);
333
}
334
335
//#endregion
336
337
//#region --- Sentiment
338
339
readonly onDidChangeSentiment: Event<void>;
340
341
get sentiment(): IChatSentiment {
342
return {
343
installed: this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Setup.installed.key) === true,
344
hidden: this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Setup.hidden.key) === true,
345
disabled: this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Setup.disabled.key) === true,
346
untrusted: this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Setup.untrusted.key) === true,
347
later: this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Setup.later.key) === true
348
};
349
}
350
351
//#endregion
352
353
async update(token: CancellationToken): Promise<void> {
354
await this.requests?.value.forceResolveEntitlement(undefined, token);
355
}
356
}
357
358
//#endregion
359
360
//#region Chat Entitlement Request Service
361
362
type EntitlementClassification = {
363
tid: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; comment: 'The anonymized analytics id returned by the service'; endpoint: 'GoogleAnalyticsId' };
364
entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' };
365
quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat requests available to the user' };
366
quotaPremiumChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of premium chat requests available to the user' };
367
quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of code completions available to the user' };
368
quotaResetDate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The date the quota will reset' };
369
owner: 'bpasero';
370
comment: 'Reporting chat entitlements';
371
};
372
373
type EntitlementEvent = {
374
entitlement: ChatEntitlement;
375
tid: string;
376
quotaChat: number | undefined;
377
quotaPremiumChat: number | undefined;
378
quotaCompletions: number | undefined;
379
quotaResetDate: string | undefined;
380
};
381
382
interface IQuotaSnapshotResponse {
383
readonly entitlement: number;
384
readonly overage_count: number;
385
readonly overage_permitted: boolean;
386
readonly percent_remaining: number;
387
readonly remaining: number;
388
readonly unlimited: boolean;
389
}
390
391
interface ILegacyQuotaSnapshotResponse {
392
readonly limited_user_quotas?: {
393
readonly chat: number;
394
readonly completions: number;
395
};
396
readonly monthly_quotas?: {
397
readonly chat: number;
398
readonly completions: number;
399
};
400
}
401
402
interface IEntitlementsResponse extends ILegacyQuotaSnapshotResponse {
403
readonly access_type_sku: string;
404
readonly assigned_date: string;
405
readonly can_signup_for_limited: boolean;
406
readonly chat_enabled: boolean;
407
readonly copilot_plan: string;
408
readonly organization_login_list: string[];
409
readonly analytics_tracking_id: string;
410
readonly limited_user_reset_date?: string; // for Copilot Free
411
readonly quota_reset_date?: string; // for all other Copilot SKUs
412
readonly quota_reset_date_utc?: string; // for all other Copilot SKUs (includes time)
413
readonly quota_snapshots?: {
414
chat?: IQuotaSnapshotResponse;
415
completions?: IQuotaSnapshotResponse;
416
premium_interactions?: IQuotaSnapshotResponse;
417
};
418
}
419
420
interface IEntitlements {
421
readonly entitlement: ChatEntitlement;
422
readonly organisations?: string[];
423
readonly sku?: string;
424
readonly quotas?: IQuotas;
425
}
426
427
export interface IQuotaSnapshot {
428
readonly total: number;
429
readonly percentRemaining: number;
430
431
readonly overageEnabled: boolean;
432
readonly overageCount: number;
433
434
readonly unlimited: boolean;
435
}
436
437
interface IQuotas {
438
readonly resetDate?: string;
439
readonly resetDateHasTime?: boolean;
440
441
readonly chat?: IQuotaSnapshot;
442
readonly completions?: IQuotaSnapshot;
443
readonly premiumChat?: IQuotaSnapshot;
444
}
445
446
export class ChatEntitlementRequests extends Disposable {
447
448
static providerId(configurationService: IConfigurationService): string {
449
if (configurationService.getValue<string | undefined>(`${defaultChat.completionsAdvancedSetting}.authProvider`) === defaultChat.provider.enterprise.id) {
450
return defaultChat.provider.enterprise.id;
451
}
452
453
return defaultChat.provider.default.id;
454
}
455
456
private state: IEntitlements;
457
458
private pendingResolveCts = new CancellationTokenSource();
459
private didResolveEntitlements = false;
460
461
constructor(
462
private readonly context: ChatEntitlementContext,
463
private readonly chatQuotasAccessor: IChatQuotasAccessor,
464
@ITelemetryService private readonly telemetryService: ITelemetryService,
465
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
466
@ILogService private readonly logService: ILogService,
467
@IRequestService private readonly requestService: IRequestService,
468
@IDialogService private readonly dialogService: IDialogService,
469
@IOpenerService private readonly openerService: IOpenerService,
470
@IConfigurationService private readonly configurationService: IConfigurationService,
471
@IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService,
472
@ILifecycleService private readonly lifecycleService: ILifecycleService,
473
) {
474
super();
475
476
this.state = { entitlement: this.context.state.entitlement };
477
478
this.registerListeners();
479
480
this.resolve();
481
}
482
483
private registerListeners(): void {
484
this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.resolve()));
485
486
this._register(this.authenticationService.onDidChangeSessions(e => {
487
if (e.providerId === ChatEntitlementRequests.providerId(this.configurationService)) {
488
this.resolve();
489
}
490
}));
491
492
this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => {
493
if (e.id === ChatEntitlementRequests.providerId(this.configurationService)) {
494
this.resolve();
495
}
496
}));
497
498
this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => {
499
if (e.id === ChatEntitlementRequests.providerId(this.configurationService)) {
500
this.resolve();
501
}
502
}));
503
504
this._register(this.context.onDidChange(() => {
505
if (!this.context.state.installed || this.context.state.disabled || this.context.state.entitlement === ChatEntitlement.Unknown) {
506
// When the extension is not installed, disabled or the user is not entitled
507
// make sure to clear quotas so that any indicators are also gone
508
this.state = { entitlement: this.state.entitlement, quotas: undefined };
509
this.chatQuotasAccessor.clearQuotas();
510
}
511
}));
512
}
513
514
private async resolve(): Promise<void> {
515
this.pendingResolveCts.dispose(true);
516
const cts = this.pendingResolveCts = new CancellationTokenSource();
517
518
const session = await this.findMatchingProviderSession(cts.token);
519
if (cts.token.isCancellationRequested) {
520
return;
521
}
522
523
// Immediately signal whether we have a session or not
524
let state: IEntitlements | undefined = undefined;
525
if (session) {
526
// Do not overwrite any state we have already
527
if (this.state.entitlement === ChatEntitlement.Unknown) {
528
state = { entitlement: ChatEntitlement.Unresolved };
529
}
530
} else {
531
this.didResolveEntitlements = false; // reset so that we resolve entitlements fresh when signed in again
532
state = { entitlement: ChatEntitlement.Unknown };
533
}
534
if (state) {
535
this.update(state);
536
}
537
538
if (session && !this.didResolveEntitlements) {
539
// Afterwards resolve entitlement with a network request
540
// but only unless it was not already resolved before.
541
await this.resolveEntitlement(session, cts.token);
542
}
543
}
544
545
private async findMatchingProviderSession(token: CancellationToken): Promise<AuthenticationSession[] | undefined> {
546
const sessions = await this.doGetSessions(ChatEntitlementRequests.providerId(this.configurationService));
547
if (token.isCancellationRequested) {
548
return undefined;
549
}
550
551
const matchingSessions = new Set<AuthenticationSession>();
552
for (const session of sessions) {
553
for (const scopes of defaultChat.providerScopes) {
554
if (this.includesScopes(session.scopes, scopes)) {
555
matchingSessions.add(session);
556
}
557
}
558
}
559
560
// We intentionally want to return an array of matching sessions and
561
// not just the first, because it is possible that a matching session
562
// has an expired token. As such, we want to try them all until we
563
// succeeded with the request.
564
return matchingSessions.size > 0 ? Array.from(matchingSessions) : undefined;
565
}
566
567
private async doGetSessions(providerId: string): Promise<readonly AuthenticationSession[]> {
568
const preferredAccountName = this.authenticationExtensionsService.getAccountPreference(defaultChat.chatExtensionId, providerId) ?? this.authenticationExtensionsService.getAccountPreference(defaultChat.extensionId, providerId);
569
let preferredAccount: AuthenticationSessionAccount | undefined;
570
for (const account of await this.authenticationService.getAccounts(providerId)) {
571
if (account.label === preferredAccountName) {
572
preferredAccount = account;
573
break;
574
}
575
}
576
577
try {
578
return await this.authenticationService.getSessions(providerId, undefined, { account: preferredAccount });
579
} catch (error) {
580
// ignore - errors can throw if a provider is not registered
581
}
582
583
return [];
584
}
585
586
private includesScopes(scopes: ReadonlyArray<string>, expectedScopes: string[]): boolean {
587
return expectedScopes.every(scope => scopes.includes(scope));
588
}
589
590
private async resolveEntitlement(sessions: AuthenticationSession[], token: CancellationToken): Promise<IEntitlements | undefined> {
591
const entitlements = await this.doResolveEntitlement(sessions, token);
592
if (typeof entitlements?.entitlement === 'number' && !token.isCancellationRequested) {
593
this.didResolveEntitlements = true;
594
this.update(entitlements);
595
}
596
597
return entitlements;
598
}
599
600
private async doResolveEntitlement(sessions: AuthenticationSession[], token: CancellationToken): Promise<IEntitlements | undefined> {
601
if (token.isCancellationRequested) {
602
return undefined;
603
}
604
605
const response = await this.request(this.getEntitlementUrl(), 'GET', undefined, sessions, token);
606
if (token.isCancellationRequested) {
607
return undefined;
608
}
609
610
if (!response) {
611
this.logService.trace('[chat entitlement]: no response');
612
return { entitlement: ChatEntitlement.Unresolved };
613
}
614
615
if (response.res.statusCode && response.res.statusCode !== 200) {
616
this.logService.trace(`[chat entitlement]: unexpected status code ${response.res.statusCode}`);
617
return (
618
response.res.statusCode === 401 || // oauth token being unavailable (expired/revoked)
619
response.res.statusCode === 404 // missing scopes/permissions, service pretends the endpoint doesn't exist
620
) ? { entitlement: ChatEntitlement.Unknown /* treat as signed out */ } : { entitlement: ChatEntitlement.Unresolved };
621
}
622
623
let responseText: string | null = null;
624
try {
625
responseText = await asText(response);
626
} catch (error) {
627
// ignore - handled below
628
}
629
if (token.isCancellationRequested) {
630
return undefined;
631
}
632
633
if (!responseText) {
634
this.logService.trace('[chat entitlement]: response has no content');
635
return { entitlement: ChatEntitlement.Unresolved };
636
}
637
638
let entitlementsResponse: IEntitlementsResponse;
639
try {
640
entitlementsResponse = JSON.parse(responseText);
641
this.logService.trace(`[chat entitlement]: parsed result is ${JSON.stringify(entitlementsResponse)}`);
642
} catch (err) {
643
this.logService.trace(`[chat entitlement]: error parsing response (${err})`);
644
return { entitlement: ChatEntitlement.Unresolved };
645
}
646
647
let entitlement: ChatEntitlement;
648
if (entitlementsResponse.access_type_sku === 'free_limited_copilot') {
649
entitlement = ChatEntitlement.Free;
650
} else if (entitlementsResponse.can_signup_for_limited) {
651
entitlement = ChatEntitlement.Available;
652
} else if (entitlementsResponse.copilot_plan === 'individual') {
653
entitlement = ChatEntitlement.Pro;
654
} else if (entitlementsResponse.copilot_plan === 'individual_pro') {
655
entitlement = ChatEntitlement.ProPlus;
656
} else if (entitlementsResponse.copilot_plan === 'business') {
657
entitlement = ChatEntitlement.Business;
658
} else if (entitlementsResponse.copilot_plan === 'enterprise') {
659
entitlement = ChatEntitlement.Enterprise;
660
} else if (entitlementsResponse.chat_enabled) {
661
// This should never happen as we exhaustively list the plans above. But if a new plan is added in the future older clients won't break
662
entitlement = ChatEntitlement.Pro;
663
} else {
664
entitlement = ChatEntitlement.Unavailable;
665
}
666
667
const entitlements: IEntitlements = {
668
entitlement,
669
organisations: entitlementsResponse.organization_login_list,
670
quotas: this.toQuotas(entitlementsResponse),
671
sku: entitlementsResponse.access_type_sku
672
};
673
674
this.logService.trace(`[chat entitlement]: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`);
675
this.telemetryService.publicLog2<EntitlementEvent, EntitlementClassification>('chatInstallEntitlement', {
676
entitlement: entitlements.entitlement,
677
tid: entitlementsResponse.analytics_tracking_id,
678
quotaChat: entitlementsResponse?.quota_snapshots?.chat?.remaining,
679
quotaPremiumChat: entitlementsResponse?.quota_snapshots?.premium_interactions?.remaining,
680
quotaCompletions: entitlementsResponse?.quota_snapshots?.completions?.remaining,
681
quotaResetDate: entitlementsResponse.quota_reset_date_utc ?? entitlementsResponse.quota_reset_date ?? entitlementsResponse.limited_user_reset_date
682
});
683
684
return entitlements;
685
}
686
687
private getEntitlementUrl(): string {
688
if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id) {
689
try {
690
const enterpriseUrl = new URL(this.configurationService.getValue(defaultChat.providerUriSetting));
691
return `${enterpriseUrl.protocol}//api.${enterpriseUrl.hostname}${enterpriseUrl.port ? ':' + enterpriseUrl.port : ''}/copilot_internal/user`;
692
} catch (error) {
693
this.logService.error(error);
694
}
695
}
696
697
return defaultChat.entitlementUrl;
698
}
699
700
private toQuotas(response: IEntitlementsResponse): IQuotas {
701
const quotas: Mutable<IQuotas> = {
702
resetDate: response.quota_reset_date_utc ?? response.quota_reset_date ?? response.limited_user_reset_date,
703
resetDateHasTime: typeof response.quota_reset_date_utc === 'string',
704
};
705
706
// Legacy Free SKU Quota
707
if (response.monthly_quotas?.chat && typeof response.limited_user_quotas?.chat === 'number') {
708
quotas.chat = {
709
total: response.monthly_quotas.chat,
710
percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.chat / response.monthly_quotas.chat) * 100)),
711
overageEnabled: false,
712
overageCount: 0,
713
unlimited: false
714
};
715
}
716
717
if (response.monthly_quotas?.completions && typeof response.limited_user_quotas?.completions === 'number') {
718
quotas.completions = {
719
total: response.monthly_quotas.completions,
720
percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.completions / response.monthly_quotas.completions) * 100)),
721
overageEnabled: false,
722
overageCount: 0,
723
unlimited: false
724
};
725
}
726
727
// New Quota Snapshot
728
if (response.quota_snapshots) {
729
for (const quotaType of ['chat', 'completions', 'premium_interactions'] as const) {
730
const rawQuotaSnapshot = response.quota_snapshots[quotaType];
731
if (!rawQuotaSnapshot) {
732
continue;
733
}
734
const quotaSnapshot: IQuotaSnapshot = {
735
total: rawQuotaSnapshot.entitlement,
736
percentRemaining: Math.min(100, Math.max(0, rawQuotaSnapshot.percent_remaining)),
737
overageEnabled: rawQuotaSnapshot.overage_permitted,
738
overageCount: rawQuotaSnapshot.overage_count,
739
unlimited: rawQuotaSnapshot.unlimited
740
};
741
742
switch (quotaType) {
743
case 'chat':
744
quotas.chat = quotaSnapshot;
745
break;
746
case 'completions':
747
quotas.completions = quotaSnapshot;
748
break;
749
case 'premium_interactions':
750
quotas.premiumChat = quotaSnapshot;
751
break;
752
}
753
}
754
}
755
756
return quotas;
757
}
758
759
private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined>;
760
private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined>;
761
private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined> {
762
let lastRequest: IRequestContext | undefined;
763
764
for (const session of sessions) {
765
if (token.isCancellationRequested) {
766
return lastRequest;
767
}
768
769
try {
770
const response = await this.requestService.request({
771
type,
772
url,
773
data: type === 'POST' ? JSON.stringify(body) : undefined,
774
disableCache: true,
775
headers: {
776
'Authorization': `Bearer ${session.accessToken}`
777
}
778
}, token);
779
780
const status = response.res.statusCode;
781
if (status && status !== 200) {
782
lastRequest = response;
783
continue; // try next session
784
}
785
786
return response;
787
} catch (error) {
788
if (!token.isCancellationRequested) {
789
this.logService.error(`[chat entitlement] request: error ${error}`);
790
}
791
}
792
}
793
794
return lastRequest;
795
}
796
797
private update(state: IEntitlements): void {
798
this.state = state;
799
800
this.context.update({ entitlement: this.state.entitlement, organisations: this.state.organisations, sku: this.state.sku });
801
802
if (state.quotas) {
803
this.chatQuotasAccessor.acceptQuotas(state.quotas);
804
}
805
}
806
807
async forceResolveEntitlement(sessions: AuthenticationSession[] | undefined, token = CancellationToken.None): Promise<IEntitlements | undefined> {
808
if (!sessions) {
809
sessions = await this.findMatchingProviderSession(token);
810
}
811
812
if (!sessions || sessions.length === 0) {
813
return undefined;
814
}
815
816
return this.resolveEntitlement(sessions, token);
817
}
818
819
async signUpFree(sessions: AuthenticationSession[]): Promise<true /* signed up */ | false /* already signed up */ | { errorCode: number } /* error */> {
820
const body = {
821
restricted_telemetry: this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? 'disabled' : 'enabled',
822
public_code_suggestions: 'enabled'
823
};
824
825
const response = await this.request(defaultChat.entitlementSignupLimitedUrl, 'POST', body, sessions, CancellationToken.None);
826
if (!response) {
827
const retry = await this.onUnknownSignUpError(localize('signUpNoResponseError', "No response received."), '[chat entitlement] sign-up: no response');
828
return retry ? this.signUpFree(sessions) : { errorCode: 1 };
829
}
830
831
if (response.res.statusCode && response.res.statusCode !== 200) {
832
if (response.res.statusCode === 422) {
833
try {
834
const responseText = await asText(response);
835
if (responseText) {
836
const responseError: { message: string } = JSON.parse(responseText);
837
if (typeof responseError.message === 'string' && responseError.message) {
838
this.onUnprocessableSignUpError(`[chat entitlement] sign-up: unprocessable entity (${responseError.message})`, responseError.message);
839
return { errorCode: response.res.statusCode };
840
}
841
}
842
} catch (error) {
843
// ignore - handled below
844
}
845
}
846
const retry = await this.onUnknownSignUpError(localize('signUpUnexpectedStatusError', "Unexpected status code {0}.", response.res.statusCode), `[chat entitlement] sign-up: unexpected status code ${response.res.statusCode}`);
847
return retry ? this.signUpFree(sessions) : { errorCode: response.res.statusCode };
848
}
849
850
let responseText: string | null = null;
851
try {
852
responseText = await asText(response);
853
} catch (error) {
854
// ignore - handled below
855
}
856
857
if (!responseText) {
858
const retry = await this.onUnknownSignUpError(localize('signUpNoResponseContentsError', "Response has no contents."), '[chat entitlement] sign-up: response has no content');
859
return retry ? this.signUpFree(sessions) : { errorCode: 2 };
860
}
861
862
let parsedResult: { subscribed: boolean } | undefined = undefined;
863
try {
864
parsedResult = JSON.parse(responseText);
865
this.logService.trace(`[chat entitlement] sign-up: response is ${responseText}`);
866
} catch (err) {
867
const retry = await this.onUnknownSignUpError(localize('signUpInvalidResponseError', "Invalid response contents."), `[chat entitlement] sign-up: error parsing response (${err})`);
868
return retry ? this.signUpFree(sessions) : { errorCode: 3 };
869
}
870
871
// We have made it this far, so the user either did sign-up or was signed-up already.
872
// That is, because the endpoint throws in all other case according to Patrick.
873
this.update({ entitlement: ChatEntitlement.Free });
874
875
return Boolean(parsedResult?.subscribed);
876
}
877
878
private async onUnknownSignUpError(detail: string, logMessage: string): Promise<boolean> {
879
this.logService.error(logMessage);
880
881
if (!this.lifecycleService.willShutdown) {
882
const { confirmed } = await this.dialogService.confirm({
883
type: Severity.Error,
884
message: localize('unknownSignUpError', "An error occurred while signing up for the GitHub Copilot Free plan. Would you like to try again?"),
885
detail,
886
primaryButton: localize('retry', "Retry")
887
});
888
889
return confirmed;
890
}
891
892
return false;
893
}
894
895
private onUnprocessableSignUpError(logMessage: string, logDetails: string): void {
896
this.logService.error(logMessage);
897
898
if (!this.lifecycleService.willShutdown) {
899
this.dialogService.prompt({
900
type: Severity.Error,
901
message: localize('unprocessableSignUpError', "An error occurred while signing up for the GitHub Copilot Free plan."),
902
detail: logDetails,
903
buttons: [
904
{
905
label: localize('ok', "OK"),
906
run: () => { /* noop */ }
907
},
908
{
909
label: localize('learnMore', "Learn More"),
910
run: () => this.openerService.open(URI.parse(defaultChat.upgradePlanUrl))
911
}
912
]
913
});
914
}
915
}
916
917
async signIn(options?: { useSocialProvider?: string; additionalScopes?: readonly string[] }) {
918
const providerId = ChatEntitlementRequests.providerId(this.configurationService);
919
920
const scopes = options?.additionalScopes ? distinct([...defaultChat.providerScopes[0], ...options.additionalScopes]) : defaultChat.providerScopes[0];
921
const session = await this.authenticationService.createSession(
922
providerId,
923
scopes,
924
{
925
extraAuthorizeParameters: { get_started_with: 'copilot-vscode' },
926
provider: options?.useSocialProvider
927
});
928
929
this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account);
930
this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account);
931
932
const entitlements = await this.forceResolveEntitlement([session]);
933
934
return { session, entitlements };
935
}
936
937
override dispose(): void {
938
this.pendingResolveCts.dispose(true);
939
940
super.dispose();
941
}
942
}
943
944
//#endregion
945
946
//#region Context
947
948
export interface IChatEntitlementContextState extends IChatSentiment {
949
950
/**
951
* Users last known or resolved entitlement.
952
*/
953
entitlement: ChatEntitlement;
954
955
/**
956
* User's last known or resolved raw SKU type.
957
*/
958
sku: string | undefined;
959
960
/**
961
* User's last known or resolved organisations.
962
*/
963
organisations: string[] | undefined;
964
965
/**
966
* User is or was a registered Chat user.
967
*/
968
registered?: boolean;
969
}
970
971
type ChatEntitlementClassification = {
972
owner: 'bpasero';
973
comment: 'Provides insight into chat entitlements.';
974
chatHidden: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether chat is hidden or not.' };
975
chatEntitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current chat entitlement of the user.' };
976
};
977
type ChatEntitlementEvent = {
978
chatHidden: boolean;
979
chatEntitlement: ChatEntitlement;
980
};
981
982
export class ChatEntitlementContext extends Disposable {
983
984
private static readonly CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY = 'chat.setupContext';
985
private static readonly CHAT_DISABLED_CONFIGURATION_KEY = 'chat.disableAIFeatures';
986
987
private readonly canSignUpContextKey: IContextKey<boolean>;
988
private readonly signedOutContextKey: IContextKey<boolean>;
989
990
private readonly freeContextKey: IContextKey<boolean>;
991
private readonly proContextKey: IContextKey<boolean>;
992
private readonly proPlusContextKey: IContextKey<boolean>;
993
private readonly businessContextKey: IContextKey<boolean>;
994
private readonly enterpriseContextKey: IContextKey<boolean>;
995
996
private readonly organisationsContextKey: IContextKey<string[] | undefined>;
997
private readonly isInternalContextKey: IContextKey<boolean>;
998
private readonly skuContextKey: IContextKey<string | undefined>;
999
1000
private readonly hiddenContext: IContextKey<boolean>;
1001
private readonly laterContext: IContextKey<boolean>;
1002
private readonly installedContext: IContextKey<boolean>;
1003
private readonly disabledContext: IContextKey<boolean>;
1004
private readonly untrustedContext: IContextKey<boolean>;
1005
1006
private _state: IChatEntitlementContextState;
1007
private suspendedState: IChatEntitlementContextState | undefined = undefined;
1008
get state(): IChatEntitlementContextState { return this.withConfiguration(this.suspendedState ?? this._state); }
1009
1010
private readonly _onDidChange = this._register(new Emitter<void>());
1011
readonly onDidChange = this._onDidChange.event;
1012
1013
private updateBarrier: Barrier | undefined = undefined;
1014
1015
constructor(
1016
@IContextKeyService contextKeyService: IContextKeyService,
1017
@IStorageService private readonly storageService: IStorageService,
1018
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
1019
@ILogService private readonly logService: ILogService,
1020
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
1021
@IConfigurationService private readonly configurationService: IConfigurationService,
1022
@ITelemetryService private readonly telemetryService: ITelemetryService
1023
) {
1024
super();
1025
1026
this.canSignUpContextKey = ChatContextKeys.Entitlement.canSignUp.bindTo(contextKeyService);
1027
this.signedOutContextKey = ChatContextKeys.Entitlement.signedOut.bindTo(contextKeyService);
1028
this.freeContextKey = ChatContextKeys.Entitlement.planFree.bindTo(contextKeyService);
1029
this.proContextKey = ChatContextKeys.Entitlement.planPro.bindTo(contextKeyService);
1030
this.proPlusContextKey = ChatContextKeys.Entitlement.planProPlus.bindTo(contextKeyService);
1031
this.businessContextKey = ChatContextKeys.Entitlement.planBusiness.bindTo(contextKeyService);
1032
this.enterpriseContextKey = ChatContextKeys.Entitlement.planEnterprise.bindTo(contextKeyService);
1033
this.organisationsContextKey = ChatContextKeys.Entitlement.organisations.bindTo(contextKeyService);
1034
this.isInternalContextKey = ChatContextKeys.Entitlement.internal.bindTo(contextKeyService);
1035
this.skuContextKey = ChatContextKeys.Entitlement.sku.bindTo(contextKeyService);
1036
this.hiddenContext = ChatContextKeys.Setup.hidden.bindTo(contextKeyService);
1037
this.laterContext = ChatContextKeys.Setup.later.bindTo(contextKeyService);
1038
this.installedContext = ChatContextKeys.Setup.installed.bindTo(contextKeyService);
1039
this.disabledContext = ChatContextKeys.Setup.disabled.bindTo(contextKeyService);
1040
this.untrustedContext = ChatContextKeys.Setup.untrusted.bindTo(contextKeyService);
1041
1042
this._state = this.storageService.getObject<IChatEntitlementContextState>(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown, organisations: undefined, sku: undefined };
1043
1044
this.checkExtensionInstallation();
1045
this.updateContextSync();
1046
1047
this.registerListeners();
1048
}
1049
1050
private registerListeners(): void {
1051
this._register(this.configurationService.onDidChangeConfiguration(e => {
1052
if (e.affectsConfiguration(ChatEntitlementContext.CHAT_DISABLED_CONFIGURATION_KEY)) {
1053
this.updateContext();
1054
}
1055
}));
1056
}
1057
1058
private withConfiguration(state: IChatEntitlementContextState): IChatEntitlementContextState {
1059
if (this.configurationService.getValue(ChatEntitlementContext.CHAT_DISABLED_CONFIGURATION_KEY) === true) {
1060
// Setting always wins: if AI is disabled, set `hidden: true`
1061
return { ...state, hidden: true };
1062
}
1063
1064
return state;
1065
}
1066
1067
private async checkExtensionInstallation(): Promise<void> {
1068
1069
// Await extensions to be ready to be queried
1070
await this.extensionsWorkbenchService.queryLocal();
1071
1072
// Listen to extensions change and process extensions once
1073
this._register(Event.runAndSubscribe<IExtension | undefined>(this.extensionsWorkbenchService.onChange, e => {
1074
if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.chatExtensionId)) {
1075
return; // unrelated event
1076
}
1077
1078
const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.chatExtensionId));
1079
const installed = !!defaultChatExtension?.local;
1080
1081
let disabled: boolean;
1082
let untrusted = false;
1083
if (installed) {
1084
disabled = !this.extensionEnablementService.isEnabled(defaultChatExtension.local);
1085
if (disabled) {
1086
const state = this.extensionEnablementService.getEnablementState(defaultChatExtension.local);
1087
if (state === EnablementState.DisabledByTrustRequirement) {
1088
disabled = false; // not disabled by user choice but
1089
untrusted = true; // by missing workspace trust
1090
}
1091
}
1092
} else {
1093
disabled = false;
1094
}
1095
1096
this.update({ installed, disabled, untrusted });
1097
}));
1098
}
1099
1100
update(context: { installed: boolean; disabled: boolean; untrusted: boolean }): Promise<void>;
1101
update(context: { hidden: false }): Promise<void>; // legacy UI state from before we had a setting to hide, keep around to still support users who used this
1102
update(context: { later: boolean }): Promise<void>;
1103
update(context: { entitlement: ChatEntitlement; organisations: string[] | undefined; sku: string | undefined }): Promise<void>;
1104
async update(context: { installed?: boolean; disabled?: boolean; untrusted?: boolean; hidden?: false; later?: boolean; entitlement?: ChatEntitlement; organisations?: string[]; sku?: string }): Promise<void> {
1105
this.logService.trace(`[chat entitlement context] update(): ${JSON.stringify(context)}`);
1106
1107
const oldState = JSON.stringify(this._state);
1108
1109
if (typeof context.installed === 'boolean' && typeof context.disabled === 'boolean' && typeof context.untrusted === 'boolean') {
1110
this._state.installed = context.installed;
1111
this._state.disabled = context.disabled;
1112
this._state.untrusted = context.untrusted;
1113
1114
if (context.installed && !context.disabled) {
1115
context.hidden = false; // treat this as a sign to make Chat visible again in case it is hidden
1116
}
1117
}
1118
1119
if (typeof context.hidden === 'boolean') {
1120
this._state.hidden = context.hidden;
1121
}
1122
1123
if (typeof context.later === 'boolean') {
1124
this._state.later = context.later;
1125
}
1126
1127
if (typeof context.entitlement === 'number') {
1128
this._state.entitlement = context.entitlement;
1129
this._state.organisations = context.organisations;
1130
this._state.sku = context.sku;
1131
1132
if (this._state.entitlement === ChatEntitlement.Free || isProUser(this._state.entitlement)) {
1133
this._state.registered = true;
1134
} else if (this._state.entitlement === ChatEntitlement.Available) {
1135
this._state.registered = false; // only reset when signed-in user can sign-up for free
1136
}
1137
}
1138
1139
if (oldState === JSON.stringify(this._state)) {
1140
return; // state did not change
1141
}
1142
1143
this.storageService.store(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, {
1144
...this._state,
1145
later: undefined // do not persist this across restarts for now
1146
}, StorageScope.PROFILE, StorageTarget.MACHINE);
1147
1148
return this.updateContext();
1149
}
1150
1151
private async updateContext(): Promise<void> {
1152
await this.updateBarrier?.wait();
1153
1154
this.updateContextSync();
1155
}
1156
1157
private updateContextSync(): void {
1158
const state = this.withConfiguration(this._state);
1159
1160
this.signedOutContextKey.set(state.entitlement === ChatEntitlement.Unknown);
1161
this.canSignUpContextKey.set(state.entitlement === ChatEntitlement.Available);
1162
1163
this.freeContextKey.set(state.entitlement === ChatEntitlement.Free);
1164
this.proContextKey.set(state.entitlement === ChatEntitlement.Pro);
1165
this.proPlusContextKey.set(state.entitlement === ChatEntitlement.ProPlus);
1166
this.businessContextKey.set(state.entitlement === ChatEntitlement.Business);
1167
this.enterpriseContextKey.set(state.entitlement === ChatEntitlement.Enterprise);
1168
1169
this.organisationsContextKey.set(state.organisations);
1170
this.isInternalContextKey.set(Boolean(state.organisations?.some(org => org === 'github' || org === 'microsoft')));
1171
this.skuContextKey.set(state.sku);
1172
1173
this.hiddenContext.set(!!state.hidden);
1174
this.laterContext.set(!!state.later);
1175
this.installedContext.set(!!state.installed);
1176
this.disabledContext.set(!!state.disabled);
1177
this.untrustedContext.set(!!state.untrusted);
1178
1179
this.logService.trace(`[chat entitlement context] updateContext(): ${JSON.stringify(state)}`);
1180
this.telemetryService.publicLog2<ChatEntitlementEvent, ChatEntitlementClassification>('chatEntitlements', {
1181
chatHidden: Boolean(state.hidden),
1182
chatEntitlement: state.entitlement
1183
});
1184
1185
this._onDidChange.fire();
1186
}
1187
1188
suspend(): void {
1189
this.suspendedState = { ...this._state };
1190
this.updateBarrier = new Barrier();
1191
}
1192
1193
resume(): void {
1194
this.suspendedState = undefined;
1195
this.updateBarrier?.open();
1196
this.updateBarrier = undefined;
1197
}
1198
}
1199
1200
//#endregion
1201
1202
1203