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