src/vs/workbench/services/chat/common/chatEntitlementService.ts

1533 LOC · 874 covered · 659 uncovered · 103 ranges · 2291 concepts · 14 introducers · 1303 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- chatEntitlementService.ts ×52
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, LogLevel } 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 > disabledInWorkspace: new RawContextKey<boolean>('chatSetupDisabledInWorkspace', false, true), // True when chat is disabled at the workspace level via settings.
43 > untrusted: new RawContextKey<boolean>('chatSetupUntrusted', false, true), // True when the chat extension is disabled due to workspace trust.
44 > later: new RawContextKey<boolean>('chatSetupLater', false, true), // True when the user wants to finish setup later.
45 > registered: new RawContextKey<boolean>('chatSetupRegistered', false, true), // True when the user has registered as Free or Pro user.
46 > completed: new RawContextKey<boolean>('chatSetupCompleted', false, true) // True when the user has completed the setup flow, regardless of the outcome.
47 > };
48 >
49 > export const Entitlement = {
50 > signedOut: new RawContextKey<boolean>('chatEntitlementSignedOut', false, true), // True when user is signed out.
51 > canSignUp: new RawContextKey<boolean>('chatPlanCanSignUp', false, true), // True when user can sign up to be a chat free user.
52 >
53 > planFree: new RawContextKey<boolean>('chatPlanFree', false, true), // True when user is a chat free user.
54 > planPro: new RawContextKey<boolean>('chatPlanPro', false, true), // True when user is a chat pro user.
55 > planEdu: new RawContextKey<boolean>('chatPlanEdu', false, true), // True when user is a chat edu user.
56 > planProPlus: new RawContextKey<boolean>('chatPlanProPlus', false, true), // True when user is a chat pro plus user.
57 > planMax: new RawContextKey<boolean>('chatPlanMax', false, true), // True when user is a chat max user.
58 > planBusiness: new RawContextKey<boolean>('chatPlanBusiness', false, true), // True when user is a chat business user.
59 > planEnterprise: new RawContextKey<boolean>('chatPlanEnterprise', false, true), // True when user is a chat enterprise user.
60 >
61 > organisations: new RawContextKey<string[]>('chatEntitlementOrganisations', undefined, true), // The organizations the user belongs to.
62 > internal: new RawContextKey<boolean>('chatEntitlementInternal', false, true), // True when user belongs to internal organisation.
63 > sku: new RawContextKey<string>('chatEntitlementSku', undefined, true), // The SKU of the user.
64 > };
65 >
66 > export const chatQuotaExceeded = new RawContextKey<boolean>('chatQuotaExceeded', false, true);
67 > export const completionsQuotaExceeded = new RawContextKey<boolean>('completionsQuotaExceeded', false, true);
68 >
69 > export const chatAnonymous = new RawContextKey<boolean>('chatAnonymous', false, true);
70 >
71 > export const clientByokEnabled = new RawContextKey<boolean>('github.copilot.clientByokEnabled', true, true);
72 >
73 > export const hasByokModels = new RawContextKey<boolean>('github.copilot.hasByokModels', false, true);
74 > }
75 >
76 > export const IChatEntitlementService = createDecorator<IChatEntitlementService>('chatEntitlementService');
77 >
78 > export enum ChatEntitlement {
79 > /** Signed out */
80 > Unknown = 1,
81 > /** Signed in but not yet resolved */
82 > Unresolved = 2,
83 > /** Signed in and entitled to Free */
84 > Available = 3,
85 > /** Signed in but not entitled to Free */
86 > Unavailable = 4,
87 > /** Signed-up to Free */
88 > Free = 5,
89 > /** Signed-up to EDU */
90 > EDU = 10,
91 > /** Signed-up to Pro */
92 > Pro = 6,
93 > /** Signed-up to Pro Plus */
94 > ProPlus = 7,
95 > /** Signed-up to Business */
96 > Business = 8,
97 > /** Signed-up to Enterprise */
98 > Enterprise = 9,
99 > /** Signed-up to Max */
100 > Max = 11,
101 > }
102 >
103 > export interface IChatSentiment {
104 >
105 > /**
106 > * Whether the user has completed the setup flow or not, regardless of the outcome
107 > */
108 > completed?: boolean;
109 >
110 > /**
111 > * User has Chat installed.
112 > */
113 > installed?: boolean;
114 >
115 > /**
116 > * User signals no intent in using Chat.
117 > *
118 > * Note: in contrast to `disabled`, this should not only disable
119 > * Chat but also hide all of its UI.
120 > */
121 > hidden?: boolean;
122 >
123 > /**
124 > * User signals intent to disable Chat.
125 > *
126 > * Note: in contrast to `hidden`, this should not hide
127 > * Chat but but disable its functionality.
128 > */
129 > disabled?: boolean;
130 >
131 > /**
132 > * Chat is disabled at the workspace level
133 > *
134 > * Note: in contrast to `hidden` (which hides all UI globally),
135 > * this only disables Chat in the current workspace while
136 > * keeping its UI visible so the user can re-enable it.
137 > */
138 > disabledInWorkspace?: boolean;
139 >
140 > /**
141 > * Chat is disabled due to missing workspace trust.
142 > *
143 > * Note: even though this disables Chat, we want to treat it
144 > * different from the `disabled` state that is by explicit
145 > * user choice.
146 > */
147 > untrusted?: boolean;
148 >
149 > /**
150 > * User signals intent to use Chat later.
151 > */
152 > later?: boolean;
153 >
154 > /**
155 > * User has registered as Free or Pro user.
156 > */
157 > registered?: boolean;
158 > }
159 >
160 > /**
161 > * The inputs needed to decide whether Chat still requires the user to run setup
162 > * (sign in / sign up / trust / enable) before it can service a request.
163 > */
164 > export interface IChatSetupRequirement {
165 > /** Whether the setup flow has been completed (any outcome). */
166 > readonly completed: boolean;
167 > /** Whether the chat extension is disabled for a reason other than trust. */
168 > readonly disabled: boolean;
169 > /** Whether the chat extension is disabled because the workspace is untrusted. */
170 > readonly untrusted: boolean;
171 > /** The user's last known or resolved entitlement. */
172 > readonly entitlement: ChatEntitlement;
173 > /** Whether anonymous (signed-out) Chat access is enabled. */
174 > readonly anonymous: boolean;
175 > /** Whether BYOK models are available. */
176 > readonly hasByokModels: boolean;
177 > }
178 >
179 > /**
180 > * Single source of truth for whether Chat still requires setup before it can
181 > * service a request. Shared by the setup agent (which routes a sent message
182 > * through setup) and the model picker (which surfaces a "Sign in to use Copilot"
183 > * state instead of a misleading lone "Auto"). BYOK models and anonymous access
184 > * intentionally satisfy the entitlement-based checks so those flows keep working.
185 > */
186 > export function chatRequiresSetup(context: IChatSetupRequirement): boolean {
188 > (!context.completed && !context.hasByokModels) || // Setup not completed (unless BYOK models are available)
189 > context.disabled || // Extension disabled: run setup to enable chatEntitlementService.ts ×1
190 > context.untrusted || // Workspace untrusted: run setup to ask for trust chatEntitlementService.ts ×1
191 > context.entitlement === ChatEntitlement.Available || // Entitlement available: run setup to sign up chatEntitlementService.ts ×1
193 > context.entitlement === ChatEntitlement.Unknown && // Entitlement unknown: run setup to sign in / sign up
194 > !context.anonymous && // unless anonymous access is enabled chatEntitlementService.ts ×1
195 > !context.hasByokModels // unless BYOK models are available chatEntitlementService.ts ×1
197 > );
198 > }
200 > export interface IChatEntitlementService {
201 >
202 > _serviceBrand: undefined;
203 >
204 > readonly onDidChangeEntitlement: Event<void>;
205 >
206 > readonly entitlement: ChatEntitlement;
207 > readonly entitlementObs: IObservable<ChatEntitlement>;
208 >
209 > readonly clientByokEnabled: boolean;
210 > readonly hasByokModels: boolean;
211 >
212 > readonly organisations: string[] | undefined;
213 > readonly isInternal: boolean;
214 > readonly sku: string | undefined;
215 > readonly copilotTrackingId: string | undefined;
216 >
217 > readonly onDidChangeQuotaExceeded: Event<void>;
218 > readonly onDidChangeQuotaRemaining: Event<void>;
219 > readonly onDidChangeUsageBasedBilling: Event<void>;
220 >
221 > readonly quotas: IQuotas;
222 >
223 > readonly onDidChangeSentiment: Event<void>;
224 >
225 > readonly sentiment: IChatSentiment;
226 > readonly sentimentObs: IObservable<IChatSentiment>;
227 >
228 > // TODO@bpasero eventually this will become enabled by default
229 > // and in that case we only need to check on entitlements change
230 > // between `unknown` and any other entitlement.
231 > readonly onDidChangeAnonymous: Event<void>;
232 > readonly anonymous: boolean;
233 > readonly anonymousObs: IObservable<boolean>;
234 >
235 > acceptQuotas(quotas: IQuotas): void;
236 >
237 > /**
238 > * Clear all quota state.
239 > */
240 > clearQuotas(): void;
241 >
242 > markAnonymousRateLimited(): void;
243 >
244 > /**
245 > * Mark the chat setup flow as completed.
246 > */
247 > markSetupCompleted(): void;
248 >
249 > /**
250 > * Force the hidden state on or off, overriding the normal entitlement logic.
251 > * Used by the account policy gate to hide all AI features when the gate is
252 > * active and unsatisfied.
253 > */
254 > setForceHidden(hidden: boolean): void;
255 >
256 > update(token: CancellationToken): Promise<void>;
257 > }
258 >
259 > //#region Helper Functions
260 >
261 > /**
262 > * Checks the chat entitlements to see if the user falls into the paid category
263 > * @param chatEntitlement The chat entitlement to check
264 > * @returns Whether or not they are a paid user
265 > */
266 > export function isProUser(chatEntitlement: ChatEntitlement): boolean {
267 return chatEntitlement === ChatEntitlement.EDU ||
268 chatEntitlement === ChatEntitlement.Pro ||
269 chatEntitlement === ChatEntitlement.ProPlus ||
270 chatEntitlement === ChatEntitlement.Max ||
271 chatEntitlement === ChatEntitlement.Business ||
272 chatEntitlement === ChatEntitlement.Enterprise;
273 }
275 > /**
276 > * Gets the full plan name for the given chat entitlement
277 > * @param chatEntitlement The chat entitlement to get the plan name for
278 > * @returns The localized full plan name (e.g., "Copilot Pro", "Copilot Free")
279 > */
280 > export function getChatPlanName(chatEntitlement: ChatEntitlement): string {
281 switch (chatEntitlement) {
282 case ChatEntitlement.EDU:
283 return localize('plan.eduName', 'Copilot Student');
284 case ChatEntitlement.Pro:
285 return localize('plan.proName', 'Copilot Pro');
286 case ChatEntitlement.ProPlus:
287 return localize('plan.proPlusName', 'Copilot Pro+');
288 case ChatEntitlement.Max:
289 return localize('plan.maxName', 'Copilot Max');
290 case ChatEntitlement.Business:
291 return localize('plan.businessName', 'Copilot Business');
292 case ChatEntitlement.Enterprise:
293 return localize('plan.enterpriseName', 'Copilot Enterprise');
294 default:
295 return localize('plan.freeName', 'Copilot Free');
296 }
297 }
299 > //#region Service Implementation
300 >
301 > const defaultChatAgent = {
302 > upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',
303 > providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '',
304 > entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '',
305 > chatQuotaExceededContext: product.defaultChatAgent?.chatQuotaExceededContext ?? '',
306 > completionsQuotaExceededContext: product.defaultChatAgent?.completionsQuotaExceededContext ?? ''
307 > };
308 >
309 > interface IChatQuotasAccessor {
310 > clearQuotas(): void;
311 > acceptQuotas(quotas: IQuotas): void;
312 > }
313 >
314 > const CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY = 'chat.allowAnonymousAccess';
315 >
316 > function isAnonymous(configurationService: IConfigurationService, entitlement: ChatEntitlement, sentiment: IChatSentiment): boolean { chatEntitlementService.ts ×30
317 > if (configurationService.getValue(CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY) !== true) {
318 > return false; // only enabled behind an experimental setting
319 > }
320
321 if (entitlement !== ChatEntitlement.Unknown) {
322 return false; // only consider signed out users
323 }
324
325 > if (sentiment.hidden || sentiment.disabledInWorkspace) { chatEntitlementService.ts ×30
326 return false; // only consider enabled scenarios
327 }
328
329 return true;
330 }
332 > type ChatEntitlementClassification = {
333 > owner: 'bpasero';
334 > comment: 'Provides insight into chat entitlements.';
335 > chatHidden: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether chat is hidden or not.' };
336 > chatEntitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current chat entitlement of the user.' };
337 > chatAnonymous: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is anonymously using chat.' };
338 > chatRegistered: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is registered for chat.' };
339 > chatDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether chat is disabled or not.' };
340 > };
341 > type ChatEntitlementEvent = {
342 > chatHidden: boolean;
343 > chatEntitlement: ChatEntitlement;
344 > chatAnonymous: boolean;
345 > chatRegistered: boolean;
346 > chatDisabled: boolean;
347 > };
348 >
349 function logChatEntitlements(state: IChatEntitlementContextState, configurationService: IConfigurationService, telemetryService: ITelemetryService): void {
350 telemetryService.publicLog2<ChatEntitlementEvent, ChatEntitlementClassification>('chatEntitlements', {
351 chatHidden: Boolean(state.hidden),
352 chatDisabled: Boolean(state.disabled),
353 chatEntitlement: state.entitlement,
354 chatRegistered: Boolean(state.registered),
355 chatAnonymous: isAnonymous(configurationService, state.entitlement, state)
356 });
357 }
359 > type ChatAdditionalSpendConfigurationClassification = {
360 > owner: 'pwang347';
361 > comment: 'Tracks when a user enables or disables additional spend.';
362 > enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether additional spend is now enabled or disabled.' };
363 > entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current chat entitlement of the user.' };
364 > };
365 > type ChatAdditionalSpendConfigurationEvent = {
366 > enabled: boolean;
367 > entitlement: ChatEntitlement;
368 > };
369 >
370 > type ChatAdditionalSpendActiveClassification = {
371 > owner: 'pwang347';
372 > comment: 'Tracks when a user enters additional spend (included quota exhausted while additional spend is enabled).';
373 > entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current chat entitlement of the user.' };
374 > additionalUsageCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of additional spend interactions used so far.' };
375 > };
376 > type ChatAdditionalSpendActiveEvent = {
377 > entitlement: ChatEntitlement;
378 > additionalUsageCount: number;
379 > };
380 >
381 > export class ChatEntitlementService extends Disposable implements IChatEntitlementService {
382 >
383 > declare _serviceBrand: undefined;
384 >
385 > private static readonly CACHED_UBB_STORAGE_KEY = 'chat.usageBasedBilling';
386 >
387 > readonly context: Lazy<ChatEntitlementContext> | undefined;
388 > readonly requests: Lazy<ChatEntitlementRequests> | undefined;
389 >
390 > constructor(
391 > @IInstantiationService instantiationService: IInstantiationService, chatEntitlementService.ts ×30
392 > @IProductService productService: IProductService,
393 > @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
394 > @IContextKeyService private readonly contextKeyService: IContextKeyService,
395 > @IConfigurationService private readonly configurationService: IConfigurationService,
396 > @ITelemetryService private readonly telemetryService: ITelemetryService,
397 > @ILogService private readonly logService: ILogService,
398 > @IStorageService private readonly storageService: IStorageService,
399 > ) {
400 > super();
401 >
402 > const cachedUBB = this.storageService.getBoolean(ChatEntitlementService.CACHED_UBB_STORAGE_KEY, StorageScope.PROFILE);
403 > this._quotas = cachedUBB !== undefined ? { usageBasedBilling: cachedUBB } : {};
404 >
405 > this.chatQuotaExceededContextKey = ChatEntitlementContextKeys.chatQuotaExceeded.bindTo(this.contextKeyService);
406 > this.completionsQuotaExceededContextKey = ChatEntitlementContextKeys.completionsQuotaExceeded.bindTo(this.contextKeyService);
407 >
408 > this.anonymousContextKey = ChatEntitlementContextKeys.chatAnonymous.bindTo(this.contextKeyService);
409 > this.anonymousContextKey.set(this.anonymous);
410 >
411 > // Only apply the workbench-side default if no other source (e.g. the Copilot extension)
412 > // has already set this key; binding would otherwise reset it to the declared default.
413 > if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.clientByokEnabled.key) === undefined) {
414 > ChatEntitlementContextKeys.clientByokEnabled.bindTo(this.contextKeyService);
415 > }
416 >
417 > this.onDidChangeEntitlement = Event.map(
418 > Event.filter(
419 > this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([
420 ChatEntitlementContextKeys.Entitlement.planEdu.key,
421 ChatEntitlementContextKeys.Entitlement.planPro.key,
422 ChatEntitlementContextKeys.Entitlement.planBusiness.key,
423 ChatEntitlementContextKeys.Entitlement.planEnterprise.key,
424 ChatEntitlementContextKeys.Entitlement.planProPlus.key,
425 ChatEntitlementContextKeys.Entitlement.planMax.key,
426 ChatEntitlementContextKeys.Entitlement.planFree.key,
427 ChatEntitlementContextKeys.Entitlement.canSignUp.key,
428 ChatEntitlementContextKeys.Entitlement.signedOut.key,
429 ChatEntitlementContextKeys.Entitlement.organisations.key,
430 ChatEntitlementContextKeys.Entitlement.internal.key,
431 ChatEntitlementContextKeys.Entitlement.sku.key
432 > ])), this._store chatEntitlementService.ts ×30
433 > ), () => { }, this._store
434 > );
435 > this.entitlementObs = observableFromEvent(this.onDidChangeEntitlement, () => this.entitlement);
436 >
437 > this.onDidChangeSentiment = Event.map(
438 > Event.filter(
439 > this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([
440 ChatEntitlementContextKeys.Setup.completed.key,
441 ChatEntitlementContextKeys.Setup.hidden.key,
442 ChatEntitlementContextKeys.Setup.disabled.key,
443 ChatEntitlementContextKeys.Setup.untrusted.key,
444 ChatEntitlementContextKeys.Setup.installed.key,
445 ChatEntitlementContextKeys.Setup.later.key,
446 ChatEntitlementContextKeys.Setup.registered.key
447 > ])), this._store chatEntitlementService.ts ×30
448 > ), () => { }, this._store
449 > );
450 > this.sentimentObs = observableFromEvent(this.onDidChangeSentiment, () => this.sentiment);
451 >
452 > if ((isWeb && !environmentService.remoteAuthority && !environmentService.isSessionsWindow)) {
453 ChatEntitlementContextKeys.Setup.hidden.bindTo(this.contextKeyService).set(true); // hide copilot UI on web if unsupported
454 return;
455 }
457 > if (!productService.defaultChatAgent) {
458 > return; // we need a default chat agent configured going forward from here
459 > }
460
461 const context = this.context = new Lazy(() => this._register(instantiationService.createInstance(ChatEntitlementContext)));
462 this.requests = new Lazy(() => this._register(instantiationService.createInstance(ChatEntitlementRequests, context.value, {
463 clearQuotas: () => this.clearQuotas(),
464 acceptQuotas: quotas => this.acceptQuotas(quotas)
465 })));
466
467 this.registerListeners();
470 > //#region --- Entitlements
471 >
472 > readonly onDidChangeEntitlement: Event<void>;
473 > readonly entitlementObs: IObservable<ChatEntitlement>;
474 >
475 > get entitlement(): ChatEntitlement {
476 > if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planEdu.key) === true) { chatEntitlementService.ts ×30
477 return ChatEntitlement.EDU;
478 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planPro.key) === true) { chatEntitlementService.ts ×30
479 return ChatEntitlement.Pro;
480 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planBusiness.key) === true) { chatEntitlementService.ts ×30
481 return ChatEntitlement.Business;
482 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planEnterprise.key) === true) { chatEntitlementService.ts ×30
483 return ChatEntitlement.Enterprise;
484 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planProPlus.key) === true) { chatEntitlementService.ts ×30
485 return ChatEntitlement.ProPlus;
486 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planMax.key) === true) { chatEntitlementService.ts ×30
487 return ChatEntitlement.Max;
488 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planFree.key) === true) { chatEntitlementService.ts ×30
489 return ChatEntitlement.Free;
490 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.canSignUp.key) === true) { chatEntitlementService.ts ×30
491 return ChatEntitlement.Available;
492 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.signedOut.key) === true) { chatEntitlementService.ts ×30
493 return ChatEntitlement.Unknown;
494 }
496 > return ChatEntitlement.Unresolved;
497 > }
499 > get isInternal(): boolean {
500 return this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.internal.key) === true;
501 }
503 > get organisations(): string[] | undefined {
504 return this.contextKeyService.getContextKeyValue<string[]>(ChatEntitlementContextKeys.Entitlement.organisations.key);
505 }
507 > get sku(): string | undefined {
508 return this.contextKeyService.getContextKeyValue<string>(ChatEntitlementContextKeys.Entitlement.sku.key);
509 }
511 > get copilotTrackingId(): string | undefined {
512 > return this.context?.value.state.copilotTrackingId; chatEntitlementService.ts ×30
513 > }
515 > get clientByokEnabled(): boolean {
516 return this.contextKeyService.getContextKeyValue<boolean>('github.copilot.clientByokEnabled') === true;
517 }
519 > get hasByokModels(): boolean {
520 return this.contextKeyService.getContextKeyValue<boolean>('github.copilot.hasByokModels') === true;
521 }
523 > //#endregion
524 >
525 > //#region --- Quotas
526 >
527 > private readonly _onDidChangeQuotaExceeded = this._register(new Emitter<void>());
528 > readonly onDidChangeQuotaExceeded = this._onDidChangeQuotaExceeded.event;
529 >
530 > private readonly _onDidChangeQuotaRemaining = this._register(new Emitter<void>());
531 > readonly onDidChangeQuotaRemaining = this._onDidChangeQuotaRemaining.event;
532 >
533 > private readonly _onDidChangeUsageBasedBilling = this._register(new Emitter<void>());
534 > readonly onDidChangeUsageBasedBilling = this._onDidChangeUsageBasedBilling.event;
535 >
536 > private _quotas: IQuotas;
537 > private quotaCopilotTrackingId: string | undefined;
538 > get quotas() { return this._quotas; }
539 >
540 > private readonly chatQuotaExceededContextKey: IContextKey<boolean>;
541 > private readonly completionsQuotaExceededContextKey: IContextKey<boolean>;
542 >
543 > private ExtensionQuotaContextKeys = {
544 > chatQuotaExceeded: defaultChatAgent.chatQuotaExceededContext,
545 > completionsQuotaExceeded: defaultChatAgent.completionsQuotaExceededContext,
546 > };
547 >
548 > private registerListeners(): void {
549 const quotaExceededSet = new Set([this.ExtensionQuotaContextKeys.chatQuotaExceeded, this.ExtensionQuotaContextKeys.completionsQuotaExceeded]);
550
551 const cts = this._register(new MutableDisposable<CancellationTokenSource>());
552 this._register(this.contextKeyService.onDidChangeContext(e => {
553 if (e.affectsSome(quotaExceededSet)) {
554 if (cts.value) {
555 cts.value.cancel();
556 }
557 cts.value = new CancellationTokenSource();
558 this.update(cts.value.token);
559 }
560 }));
561
562 let anonymousUsage = this.anonymous;
563
564 const updateAnonymousUsage = () => {
565 const newAnonymousUsage = this.anonymous;
566 if (newAnonymousUsage !== anonymousUsage) {
567 anonymousUsage = newAnonymousUsage;
568 this.anonymousContextKey.set(newAnonymousUsage);
569
570 if (this.context?.hasValue) {
571 logChatEntitlements(this.context.value.state, this.configurationService, this.telemetryService);
572 }
573
574 this._onDidChangeAnonymous.fire();
575 }
576 };
577
578 this._register(this.configurationService.onDidChangeConfiguration(e => {
579 if (e.affectsConfiguration(CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY)) {
580 updateAnonymousUsage();
581 }
582 }));
583
584 this._register(this.onDidChangeEntitlement(() => updateAnonymousUsage()));
585 this._register(this.onDidChangeSentiment(() => updateAnonymousUsage()));
586 }
588 > acceptQuotas(incomingQuotas: IQuotas): void {
589 > const oldQuota = this._quotas; chatEntitlementService.ts ×30
590 > const cachedQuota = this.quotaCopilotTrackingId === this.copilotTrackingId ? oldQuota : {};
591 > const quotas: IQuotas = {
592 > ...incomingQuotas,
593 > chat: incomingQuotas.chat ? mergeDefinedSnapshot(cachedQuota.chat, incomingQuotas.chat) : undefined,
594 > completions: incomingQuotas.completions ? mergeDefinedSnapshot(cachedQuota.completions, incomingQuotas.completions) : undefined,
595 > premiumChat: incomingQuotas.premiumChat ? mergeDefinedSnapshot(cachedQuota.premiumChat, incomingQuotas.premiumChat) : undefined,
596 > sessionRateLimit: incomingQuotas.sessionRateLimit ? mergeDefinedSnapshot(cachedQuota.sessionRateLimit, incomingQuotas.sessionRateLimit) : undefined,
597 > weeklyRateLimit: incomingQuotas.weeklyRateLimit ? mergeDefinedSnapshot(cachedQuota.weeklyRateLimit, incomingQuotas.weeklyRateLimit) : undefined,
598 > };
599 > this.quotaCopilotTrackingId = this.copilotTrackingId;
600 > this._quotas = quotas;
601 > this.updateContextKeys();
602 >
603 > if (oldQuota.usageBasedBilling !== quotas.usageBasedBilling) {
604 if (quotas.usageBasedBilling !== undefined) {
605 this.storageService.store(ChatEntitlementService.CACHED_UBB_STORAGE_KEY, quotas.usageBasedBilling, StorageScope.PROFILE, StorageTarget.MACHINE);
606 } else {
607 this.storageService.remove(ChatEntitlementService.CACHED_UBB_STORAGE_KEY, StorageScope.PROFILE);
608 }
609 }
611 > if (this.logService.getLevel() === LogLevel.Trace) {
612 this.logService.trace(`[chat entitlement]: acceptQuotas: ${JSON.stringify(quotas)}`);
613 }
615 > const { changed: chatChanged } = this.compareQuotas(oldQuota.chat, quotas.chat);
616 > const { changed: completionsChanged } = this.compareQuotas(oldQuota.completions, quotas.completions);
617 > const { changed: premiumChatChanged } = this.compareQuotas(oldQuota.premiumChat, quotas.premiumChat);
618 >
619 > if (chatChanged.exceeded || completionsChanged.exceeded || premiumChatChanged.exceeded) {
620 this._onDidChangeQuotaExceeded.fire();
621 }
623 > const sessionRateLimitChanged = oldQuota.sessionRateLimit?.percentRemaining !== quotas.sessionRateLimit?.percentRemaining;
624 > const weeklyRateLimitChanged = oldQuota.weeklyRateLimit?.percentRemaining !== quotas.weeklyRateLimit?.percentRemaining;
625 >
626 > if (chatChanged.remaining || completionsChanged.remaining || premiumChatChanged.remaining || sessionRateLimitChanged || weeklyRateLimitChanged || oldQuota.usageBasedBilling !== quotas.usageBasedBilling) {
627 > this._onDidChangeQuotaRemaining.fire();
628 > }
629 >
630 > if (oldQuota.usageBasedBilling !== quotas.usageBasedBilling) {
631 this._onDidChangeUsageBasedBilling.fire();
632 }
634 > // Track additional spend configuration changes (only when both values come from server snapshots)
635 > if (oldQuota.additionalUsageEnabled !== undefined && quotas.additionalUsageEnabled !== undefined && oldQuota.additionalUsageEnabled !== quotas.additionalUsageEnabled) {
636 this.telemetryService.publicLog2<ChatAdditionalSpendConfigurationEvent, ChatAdditionalSpendConfigurationClassification>('chatAdditionalSpendConfiguration', {
637 enabled: quotas.additionalUsageEnabled ?? false,
638 entitlement: this.entitlement,
639 });
640 }
642 > // Track entering additional spend: included quota just exhausted while additional spend is enabled
643 > if (quotas.additionalUsageEnabled && quotas.premiumChat?.percentRemaining === 0
644 > && oldQuota.premiumChat?.percentRemaining !== undefined && oldQuota.premiumChat.percentRemaining > 0) {
645 this.telemetryService.publicLog2<ChatAdditionalSpendActiveEvent, ChatAdditionalSpendActiveClassification>('chatAdditionalSpendActive', {
646 entitlement: this.entitlement,
647 additionalUsageCount: quotas.additionalUsageCount ?? 0,
648 });
649 }
652 > private compareQuotas(oldQuota: IQuotaSnapshot | undefined, newQuota: IQuotaSnapshot | undefined): { changed: { exceeded: boolean; remaining: boolean } } {
654 > changed: {
655 > exceeded: (oldQuota?.percentRemaining === 0) !== (newQuota?.percentRemaining === 0),
656 > remaining: oldQuota?.percentRemaining !== newQuota?.percentRemaining
657 > || oldQuota?.usageBasedBilling !== newQuota?.usageBasedBilling
658 > }
659 > };
660 > }
662 > clearQuotas(): void {
663 this.acceptQuotas({});
664 }
666 > private updateContextKeys(): void {
667 > const chatExhausted = this._quotas.chat?.percentRemaining === 0; chatEntitlementService.ts ×30
668 > const premiumChatExhausted = this._quotas.premiumChat?.unlimited
669 > ? this._quotas.premiumChat.hasQuota === false
670 > : this._quotas.premiumChat?.percentRemaining === 0;
671 > const additionalUsageEnabled = this._quotas.additionalUsageEnabled ?? false;
672 > const isManagedPlan = this.entitlement === ChatEntitlement.Business || this.entitlement === ChatEntitlement.Enterprise;
673 >
674 > // For Business/Enterprise users, hasQuota === false is the authoritative signal
675 > // that the org has blocked usage, regardless of additionalUsageEnabled.
676 > this.chatQuotaExceededContextKey.set(chatExhausted || (premiumChatExhausted && (isManagedPlan || !additionalUsageEnabled)));
677 > this.completionsQuotaExceededContextKey.set(this._quotas.completions?.percentRemaining === 0);
678 > }
680 > //#endregion
681 >
682 > //#region --- Sentiment
683 >
684 > readonly onDidChangeSentiment: Event<void>;
685 > readonly sentimentObs: IObservable<IChatSentiment>;
686 >
687 > get sentiment(): IChatSentiment {
689 > completed: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.completed.key) === true,
690 > installed: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.installed.key) === true,
691 > hidden: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.hidden.key) === true,
692 > disabledInWorkspace: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.disabledInWorkspace.key) === true,
693 > disabled: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.disabled.key) === true,
694 > untrusted: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.untrusted.key) === true,
695 > later: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.later.key) === true,
696 > registered: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.registered.key) === true
697 > };
698 > }
700 > //#endregion
701 >
702 > //region --- Anonymous
703 >
704 > private readonly anonymousContextKey: IContextKey<boolean>;
705 >
706 > private readonly _onDidChangeAnonymous = this._register(new Emitter<void>());
707 > readonly onDidChangeAnonymous = this._onDidChangeAnonymous.event;
708 >
709 > readonly anonymousObs = observableFromEvent(this.onDidChangeAnonymous, () => this.anonymous);
710 >
711 > get anonymous(): boolean {
712 > return isAnonymous(this.configurationService, this.entitlement, this.sentiment); chatEntitlementService.ts ×30
713 > }
715 > //#endregion
716 >
717 > markAnonymousRateLimited(): void {
718 if (!this.anonymous) {
719 return;
720 }
721
722 this.chatQuotaExceededContextKey.set(true);
723 this._onDidChangeQuotaExceeded.fire();
724 }
726 > markSetupCompleted(): void {
727 this.context?.value.update({ completed: true });
728 }
730 > setForceHidden(hidden: boolean): void {
731 if (this.context) {
732 this.context.value.setForceHidden(hidden);
733 } else {
734 // No ChatEntitlementContext (e.g. no defaultChatAgent in product.json).
735 // Set the context key directly as a fallback.
736 ChatEntitlementContextKeys.Setup.hidden.bindTo(this.contextKeyService).set(hidden);
737 }
738 }
740 > async update(token: CancellationToken): Promise<void> {
741 await this.requests?.value.forceResolveEntitlement(token);
742 }
744 >
745 > //#endregion
746 >
747 > //#region Chat Entitlement Request Service
748 >
749 > type EntitlementClassification = {
750 > tid: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; comment: 'The anonymized analytics id returned by the service'; endpoint: 'GoogleAnalyticsId' };
751 > entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' };
752 > sku: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The SKU of the chat entitlement' };
753 > quotaChatUnlimited: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user has unlimited chat requests' };
754 > quotaChatHasQuota: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user currently has chat quota available' };
755 > quotaChatEntitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The raw chat quota entitlement count' };
756 > quotaPremiumChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The percentage of premium chat requests remaining for the user' };
757 > quotaPremiumChatUnlimited: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user has unlimited premium chat requests' };
758 > quotaPremiumChatHasQuota: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user currently has premium chat quota available' };
759 > quotaPremiumChatEntitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The raw premium chat quota entitlement count' };
760 > quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The percentage of completions remaining for the user' };
761 > quotaCompletionsUnlimited: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user has unlimited completions' };
762 > quotaCompletionsHasQuota: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user currently has completions quota available' };
763 > quotaCompletionsEntitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The raw completions quota entitlement count' };
764 > quotaResetDate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The date the quota will reset' };
765 > usageBasedBilling: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is on usage-based billing' };
766 > additionalUsageEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether overage / additional spend is enabled' };
767 > additionalUsageCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of overage interactions used' };
768 > canUpgradePlan: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is eligible to upgrade their plan' };
769 > owner: 'bpasero';
770 > comment: 'Reporting chat entitlements';
771 > };
772 >
773 > type EntitlementEvent = {
774 > entitlement: ChatEntitlement;
775 > tid: string;
776 > sku: string | undefined;
777 > quotaChatUnlimited: boolean | undefined;
778 > quotaChatHasQuota: boolean | undefined;
779 > quotaChatEntitlement: number | undefined;
780 > quotaPremiumChat: number | undefined;
781 > quotaPremiumChatUnlimited: boolean | undefined;
782 > quotaPremiumChatHasQuota: boolean | undefined;
783 > quotaPremiumChatEntitlement: number | undefined;
784 > quotaCompletions: number | undefined;
785 > quotaCompletionsUnlimited: boolean | undefined;
786 > quotaCompletionsHasQuota: boolean | undefined;
787 > quotaCompletionsEntitlement: number | undefined;
788 > quotaResetDate: string | undefined;
789 > usageBasedBilling: boolean | undefined;
790 > additionalUsageEnabled: boolean | undefined;
791 > additionalUsageCount: number | undefined;
792 > canUpgradePlan: boolean | undefined;
793 > };
794 >
795 > interface IEntitlements {
796 > readonly entitlement: ChatEntitlement;
797 > readonly organisations?: string[];
798 > readonly sku?: string;
799 > readonly copilotTrackingId?: string;
800 > readonly quotas?: IQuotas;
801 > }
802 >
803 > export interface IQuotaSnapshot {
804 > readonly percentRemaining: number;
805 > readonly unlimited: boolean;
806 > readonly hasQuota?: boolean;
807 > readonly resetAt?: number;
808 > readonly usageBasedBilling?: boolean;
809 > readonly entitlement?: number;
810 > readonly quotaRemaining?: number;
811 > readonly creditsUsed?: number;
812 > }
813 >
814 > export interface IRateLimitSnapshot {
815 > readonly percentRemaining: number;
816 > readonly unlimited: boolean;
817 > readonly resetDate?: string;
818 > }
819 >
820 > interface IQuotas {
821 > readonly resetDate?: string;
822 > readonly resetDateHasTime?: boolean;
823 >
824 > readonly usageBasedBilling?: boolean;
825 > readonly canUpgradePlan?: boolean;
826 >
827 > readonly chat?: IQuotaSnapshot;
828 > readonly completions?: IQuotaSnapshot;
829 > readonly premiumChat?: IQuotaSnapshot;
830 > readonly additionalUsageEnabled?: boolean;
831 > readonly additionalUsageCount?: number;
832 > readonly additionalUsageEntitlement?: number;
833 >
834 > readonly sessionRateLimit?: IRateLimitSnapshot;
835 > readonly weeklyRateLimit?: IRateLimitSnapshot;
836 > }
837 >
838 > function mergeDefinedSnapshot<T extends object>(previous: T | undefined, current: T): T { chatEntitlementService.ts ×30
839 > const result = { ...previous, ...current };
840 > for (const key of Object.keys(current) as (keyof T)[]) {
841 > if (current[key] === undefined && previous?.[key] !== undefined) {
842 > result[key] = previous[key];
843 > }
844 > }
845 > return result;
846 > }
848 > export function parseQuotas(entitlementsData: IEntitlementsData): IQuotas {
849 > const quotas: Mutable<IQuotas> = { chatEntitlementService.ts ×8
850 > resetDate: entitlementsData.quota_reset_date_utc ?? entitlementsData.quota_reset_date ?? entitlementsData.limited_user_reset_date,
851 > resetDateHasTime: typeof entitlementsData.quota_reset_date_utc === 'string',
852 > usageBasedBilling: entitlementsData.token_based_billing,
853 > canUpgradePlan: entitlementsData.can_upgrade_plan,
854 > };
855 >
856 > // Legacy Free SKU Quota
857 > if (entitlementsData.monthly_quotas?.chat && typeof entitlementsData.limited_user_quotas?.chat === 'number') {
858 quotas.chat = {
859 percentRemaining: Math.min(100, Math.max(0, (entitlementsData.limited_user_quotas.chat / entitlementsData.monthly_quotas.chat) * 100)),
860 unlimited: false
861 };
862 }
864 > if (entitlementsData.monthly_quotas?.completions && typeof entitlementsData.limited_user_quotas?.completions === 'number') {
865 quotas.completions = {
866 percentRemaining: Math.min(100, Math.max(0, (entitlementsData.limited_user_quotas.completions / entitlementsData.monthly_quotas.completions) * 100)),
867 unlimited: false
868 };
869 }
871 > // New Quota Snapshot
872 > if (entitlementsData.quota_snapshots) {
873 > for (const quotaType of ['chat', 'completions', 'premium_interactions'] as const) {
874 > const rawQuotaSnapshot = entitlementsData.quota_snapshots[quotaType];
875 > if (!rawQuotaSnapshot) {
877 > }
878 > const parsedEntitlement = rawQuotaSnapshot.entitlement !== undefined ? Number(rawQuotaSnapshot.entitlement) : undefined; chatEntitlementService.ts ×8
879 > const parsedCreditsUsed = rawQuotaSnapshot.credits_used !== undefined ? Number(rawQuotaSnapshot.credits_used) : undefined;
880 >
881 > // Skip snapshots where the user has no allocated entitlement for this
882 > // category (e.g. free tier premium_interactions with 0 credits). Under
883 > // TBB, has_quota is always false at the per-snapshot level so we cannot
884 > // rely on it; instead check the actual entitlement value.
885 > if (!rawQuotaSnapshot.unlimited && parsedEntitlement === 0) {
887 > }
889 > const parsedQuotaRemaining = rawQuotaSnapshot.quota_remaining !== undefined ? Number(rawQuotaSnapshot.quota_remaining) : undefined;
890 > const quotaSnapshot: IQuotaSnapshot = {
891 > percentRemaining: Math.min(100, Math.max(0, rawQuotaSnapshot.percent_remaining)),
892 > unlimited: rawQuotaSnapshot.unlimited,
893 > hasQuota: rawQuotaSnapshot.has_quota,
894 > usageBasedBilling: entitlementsData.token_based_billing,
895 > resetAt: rawQuotaSnapshot.quota_reset_at || undefined,
896 > entitlement: parsedEntitlement !== undefined && Number.isFinite(parsedEntitlement) && parsedEntitlement >= 0 ? parsedEntitlement : undefined,
897 > quotaRemaining: parsedQuotaRemaining !== undefined && Number.isFinite(parsedQuotaRemaining) && parsedQuotaRemaining >= 0 ? parsedQuotaRemaining : undefined,
898 > creditsUsed: parsedCreditsUsed !== undefined && Number.isFinite(parsedCreditsUsed) && parsedCreditsUsed >= 0 ? parsedCreditsUsed : undefined,
899 > };
900 >
901 > switch (quotaType) {
902 > case 'chat':
903 > quotas.chat = quotaSnapshot; chatEntitlementService.ts ×2
904 > break;
905 > case 'completions': chatEntitlementService.ts ×8
906 > quotas.completions = quotaSnapshot; chatEntitlementService.ts ×2
907 > break;
908 > case 'premium_interactions': chatEntitlementService.ts ×8
909 > quotas.premiumChat = quotaSnapshot; chatEntitlementService.ts ×1
910 > break;
912 > }
913 >
914 > const overageSource = entitlementsData.quota_snapshots['premium_interactions'];
915 > quotas.additionalUsageEnabled = overageSource?.overage_permitted ?? false;
916 > quotas.additionalUsageCount = overageSource?.overage_count ?? 0;
917 > quotas.additionalUsageEntitlement = overageSource?.overage_entitlement ?? 0;
918 > }
919 > return quotas;
920 > }
922 > export class ChatEntitlementRequests extends Disposable {
923 >
924 > private state: IEntitlements;
925 >
926 > private pendingResolveCts = new CancellationTokenSource();
927 >
928 > constructor(
929 private readonly context: ChatEntitlementContext,
930 private readonly chatQuotasAccessor: IChatQuotasAccessor,
931 @ITelemetryService private readonly telemetryService: ITelemetryService,
932 @ILogService private readonly logService: ILogService,
933 @IRequestService private readonly requestService: IRequestService,
934 @IDialogService private readonly dialogService: IDialogService,
935 @IOpenerService private readonly openerService: IOpenerService,
936 @ILifecycleService private readonly lifecycleService: ILifecycleService,
937 @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,
938 @IAuthenticationService private readonly authenticationService: IAuthenticationService,
939 ) {
940 super();
941
942 this.state = { entitlement: this.context.state.entitlement };
943
944 this.registerListeners();
945
946 this.resolve();
947 }
949 > private registerListeners(): void {
950 this._register(this.defaultAccountService.onDidChangeDefaultAccount(() => this.resolve()));
951
952 this._register(this.context.onDidChange(() => {
953 if (this.context.state.disabled || this.context.state.entitlement === ChatEntitlement.Unknown) {
954 // When the extension is disabled or the user is not entitled
955 // make sure to clear quotas so that any indicators are also gone
956 this.state = { entitlement: this.state.entitlement, quotas: undefined };
957 this.chatQuotasAccessor.clearQuotas();
958 }
959 }));
960 }
962 > private async resolve(): Promise<void> {
963 this.pendingResolveCts.dispose(true);
964 const cts = this.pendingResolveCts = new CancellationTokenSource();
965
966 const defaultAccount = await this.defaultAccountService.getDefaultAccount();
967 if (cts.token.isCancellationRequested) {
968 return;
969 }
970
971 // Immediately signal whether we have a session or not
972 let state: IEntitlements | undefined = undefined;
973 if (defaultAccount) {
974 // Do not overwrite any state we have already
975 if (this.state.entitlement === ChatEntitlement.Unknown) {
976 state = { entitlement: ChatEntitlement.Unresolved };
977 }
978 } else {
979 state = { entitlement: ChatEntitlement.Unknown };
980 }
981 if (state) {
982 this.update(state);
983 }
984
985 if (defaultAccount) {
986 // Afterwards resolve entitlement with a network request
987 // but only unless it was not already resolved before.
988 await this.resolveEntitlement(defaultAccount, cts.token);
989 }
990 }
992 > private async resolveEntitlement(defaultAccount: IDefaultAccount, token: CancellationToken): Promise<IEntitlements | undefined> {
993 const entitlements = await this.doResolveEntitlement(defaultAccount, token);
994 if (typeof entitlements?.entitlement === 'number' && !token.isCancellationRequested) {
995 this.update(entitlements);
996 }
997 return entitlements;
998 }
1000 > private async doResolveEntitlement(defaultAccount: IDefaultAccount, token: CancellationToken): Promise<IEntitlements | undefined> {
1001 if (token.isCancellationRequested) {
1002 return undefined;
1003 }
1004
1005 const entitlementsData = defaultAccount.entitlementsData;
1006 if (!entitlementsData) {
1007 this.logService.trace('[chat entitlement]: no entitlements data available on default account');
1008 return { entitlement: entitlementsData === null ? ChatEntitlement.Unknown : ChatEntitlement.Unresolved };
1009 }
1010
1011 let entitlement: ChatEntitlement;
1012 if (entitlementsData.access_type_sku === 'free_limited_copilot') {
1013 entitlement = ChatEntitlement.Free;
1014 } else if (entitlementsData.access_type_sku === 'free_educational_quota') {
1015 entitlement = ChatEntitlement.EDU;
1016 } else if (entitlementsData.can_signup_for_limited) {
1017 entitlement = ChatEntitlement.Available;
1018 } else if (entitlementsData.copilot_plan === 'individual_edu') {
1019 entitlement = ChatEntitlement.EDU;
1020 } else if (entitlementsData.copilot_plan === 'individual') {
1021 entitlement = ChatEntitlement.Pro;
1022 } else if (entitlementsData.copilot_plan === 'individual_pro') {
1023 entitlement = ChatEntitlement.ProPlus;
1024 } else if (entitlementsData.copilot_plan === 'individual_max') {
1025 entitlement = ChatEntitlement.Max;
1026 } else if (entitlementsData.copilot_plan === 'business') {
1027 entitlement = ChatEntitlement.Business;
1028 } else if (entitlementsData.copilot_plan === 'enterprise') {
1029 entitlement = ChatEntitlement.Enterprise;
1030 } else {
1031 entitlement = ChatEntitlement.Unavailable;
1032 }
1033
1034 const entitlements: IEntitlements = {
1035 entitlement,
1036 organisations: entitlementsData.organization_login_list,
1037 quotas: this.toQuotas(entitlementsData),
1038 sku: entitlementsData.access_type_sku,
1039 copilotTrackingId: entitlementsData.analytics_tracking_id
1040 };
1041
1042 this.logService.trace(`[chat entitlement]: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`);
1043 this.telemetryService.publicLog2<EntitlementEvent, EntitlementClassification>('chatInstallEntitlement', {
1044 entitlement: entitlements.entitlement,
1045 tid: entitlementsData.analytics_tracking_id,
1046 sku: entitlements.sku,
1047 quotaChatUnlimited: entitlements.quotas?.chat?.unlimited,
1048 quotaChatHasQuota: entitlements.quotas?.chat?.hasQuota,
1049 quotaChatEntitlement: entitlements.quotas?.chat?.entitlement,
1050 quotaPremiumChat: entitlements.quotas?.premiumChat?.percentRemaining,
1051 quotaPremiumChatUnlimited: entitlements.quotas?.premiumChat?.unlimited,
1052 quotaPremiumChatHasQuota: entitlements.quotas?.premiumChat?.hasQuota,
1053 quotaPremiumChatEntitlement: entitlements.quotas?.premiumChat?.entitlement,
1054 quotaCompletions: entitlements.quotas?.completions?.percentRemaining,
1055 quotaCompletionsUnlimited: entitlements.quotas?.completions?.unlimited,
1056 quotaCompletionsHasQuota: entitlements.quotas?.completions?.hasQuota,
1057 quotaCompletionsEntitlement: entitlements.quotas?.completions?.entitlement,
1058 quotaResetDate: entitlements.quotas?.resetDate,
1059 usageBasedBilling: entitlements.quotas?.usageBasedBilling,
1060 additionalUsageEnabled: entitlements.quotas?.additionalUsageEnabled,
1061 additionalUsageCount: entitlements.quotas?.additionalUsageCount,
1062 canUpgradePlan: entitlements.quotas?.canUpgradePlan
1063 });
1064
1065 return entitlements;
1066 }
1068 > private toQuotas(entitlementsData: IEntitlementsData): IQuotas {
1069 return parseQuotas(entitlementsData);
1070 }
1072 > private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string): Promise<IRequestContext | undefined>;
1073 > private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken, callSite: string): Promise<IRequestContext | undefined>;
1074 > private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string): Promise<IRequestContext | undefined> {
1075 let lastRequest: IRequestContext | undefined;
1076
1077 for (const session of sessions) {
1078 if (token.isCancellationRequested) {
1079 return lastRequest;
1080 }
1081
1082 try {
1083 const response = await this.requestService.request({
1084 type,
1085 url,
1086 data: type === 'POST' ? JSON.stringify(body) : undefined,
1087 disableCache: true,
1088 headers: {
1089 'Authorization': `Bearer ${session.accessToken}`
1090 },
1091 callSite
1092 }, token);
1093
1094 const status = response.res.statusCode;
1095 if (status && status !== 200) {
1096 lastRequest = response;
1097 continue; // try next session
1098 }
1099
1100 return response;
1101 } catch (error) {
1102 if (!token.isCancellationRequested) {
1103 this.logService.error(`[chat entitlement] request: error ${error}`);
1104 }
1105 }
1106 }
1107
1108 return lastRequest;
1109 }
1111 > private update(state: IEntitlements): void {
1112 this.state = state;
1113
1114 this.context.update({ entitlement: this.state.entitlement, organisations: this.state.organisations, sku: this.state.sku, copilotTrackingId: this.state.copilotTrackingId });
1115
1116 if (state.quotas) {
1117 this.chatQuotasAccessor.acceptQuotas(state.quotas);
1118 }
1119 }
1121 > async forceResolveEntitlement(token = CancellationToken.None): Promise<IEntitlements | undefined> {
1122 const defaultAccount = await this.defaultAccountService.refresh({ forceRefresh: true });
1123 if (!defaultAccount) {
1124 return undefined;
1125 }
1126
1127 return this.resolveEntitlement(defaultAccount, token);
1128 }
1130 > async signUpFree(): Promise<true /* signed up */ | false /* already signed up */ | { errorCode: number } /* error */ | undefined /* no session */> {
1131 const sessions = await this.getSessions();
1132 if (sessions.length === 0) {
1133 return undefined;
1134 }
1135 return this.doSignUpFree(sessions);
1136 }
1138 > private async doSignUpFree(sessions: AuthenticationSession[]): Promise<true /* signed up */ | false /* already signed up */ | { errorCode: number } /* error */> {
1139 const body = {
1140 restricted_telemetry: this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? 'disabled' : 'enabled',
1141 public_code_suggestions: 'enabled'
1142 };
1143
1144 const response = await this.request(defaultChatAgent.entitlementSignupLimitedUrl, 'POST', body, sessions, CancellationToken.None, 'chatEntitlementService.signUpFree');
1145 if (!response) {
1146 const retry = await this.onUnknownSignUpError(localize('signUpNoResponseError', "No response received."), '[chat entitlement] sign-up: no response');
1147 return retry ? this.doSignUpFree(sessions) : { errorCode: 1 };
1148 }
1149
1150 if (response.res.statusCode && response.res.statusCode !== 200) {
1151 if (response.res.statusCode === 422) {
1152 try {
1153 const responseText = await asText(response);
1154 if (responseText) {
1155 const responseError: { message: string } = JSON.parse(responseText);
1156 if (typeof responseError.message === 'string' && responseError.message) {
1157 this.onUnprocessableSignUpError(`[chat entitlement] sign-up: unprocessable entity (${responseError.message})`, responseError.message);
1158 return { errorCode: response.res.statusCode };
1159 }
1160 }
1161 } catch (error) {
1162 // ignore - handled below
1163 }
1164 }
1165 const retry = await this.onUnknownSignUpError(localize('signUpUnexpectedStatusError', "Unexpected status code {0}.", response.res.statusCode), `[chat entitlement] sign-up: unexpected status code ${response.res.statusCode}`);
1166 return retry ? this.doSignUpFree(sessions) : { errorCode: response.res.statusCode };
1167 }
1168
1169 let responseText: string | null = null;
1170 try {
1171 responseText = await asText(response);
1172 } catch (error) {
1173 // ignore - handled below
1174 }
1175
1176 if (!responseText) {
1177 const retry = await this.onUnknownSignUpError(localize('signUpNoResponseContentsError', "Response has no contents."), '[chat entitlement] sign-up: response has no content');
1178 return retry ? this.doSignUpFree(sessions) : { errorCode: 2 };
1179 }
1180
1181 let parsedResult: { subscribed: boolean } | undefined = undefined;
1182 try {
1183 parsedResult = JSON.parse(responseText);
1184 this.logService.trace(`[chat entitlement] sign-up: response is ${responseText}`);
1185 } catch (err) {
1186 const retry = await this.onUnknownSignUpError(localize('signUpInvalidResponseError', "Invalid response contents."), `[chat entitlement] sign-up: error parsing response (${err})`);
1187 return retry ? this.doSignUpFree(sessions) : { errorCode: 3 };
1188 }
1189
1190 // We have made it this far, so the user either did sign-up or was signed-up already.
1191 // That is, because the endpoint throws in all other case according to Patrick.
1192 this.update({ entitlement: ChatEntitlement.Free });
1193
1194 return Boolean(parsedResult?.subscribed);
1195 }
1197 > private async getSessions(): Promise<AuthenticationSession[]> {
1198 const defaultAccount = await this.defaultAccountService.getDefaultAccount();
1199 if (defaultAccount) {
1200 const sessions = await this.authenticationService.getSessions(defaultAccount.authenticationProvider.id);
1201 const accountSessions = sessions.filter(s => s.id === defaultAccount.sessionId);
1202 if (accountSessions.length) {
1203 return accountSessions;
1204 }
1205 }
1206 return [...(await this.authenticationService.getSessions(this.defaultAccountService.getDefaultAccountAuthenticationProvider().id))];
1207 }
1209 > private async onUnknownSignUpError(detail: string, logMessage: string): Promise<boolean> {
1210 this.logService.error(logMessage);
1211
1212 if (!this.lifecycleService.willShutdown) {
1213 const { confirmed } = await this.dialogService.confirm({
1214 type: Severity.Error,
1215 message: localize('unknownSignUpError', "An error occurred while signing up for the GitHub Copilot Free plan. Would you like to try again?"),
1216 detail,
1217 primaryButton: localize('retry', "Retry")
1218 });
1219
1220 return confirmed;
1221 }
1222
1223 return false;
1224 }
1226 > private onUnprocessableSignUpError(logMessage: string, logDetails: string): void {
1227 this.logService.error(logMessage);
1228
1229 if (!this.lifecycleService.willShutdown) {
1230 this.dialogService.prompt({
1231 type: Severity.Error,
1232 message: localize('unprocessableSignUpError', "An error occurred while signing up for the GitHub Copilot Free plan."),
1233 detail: logDetails,
1234 buttons: [
1235 {
1236 label: localize('ok', "OK"),
1237 run: () => { /* noop */ }
1238 },
1239 {
1240 label: localize('learnMore', "Learn More"),
1241 run: () => this.openerService.open(URI.parse(defaultChatAgent.upgradePlanUrl))
1242 }
1243 ]
1244 });
1245 }
1246 }
1248 > async signIn(options?: { useSocialProvider?: string; additionalScopes?: readonly string[] }): Promise<{ defaultAccount?: IDefaultAccount; entitlements?: IEntitlements }> {
1249 const defaultAccount = await this.defaultAccountService.signIn({
1250 additionalScopes: options?.additionalScopes,
1251 extraAuthorizeParameters: { get_started_with: 'copilot-vscode' },
1252 provider: options?.useSocialProvider
1253 });
1254 if (!defaultAccount) {
1255 return {};
1256 }
1257
1258 const entitlements = await this.doResolveEntitlement(defaultAccount, CancellationToken.None);
1259 return { defaultAccount, entitlements };
1260 }
1262 > override dispose(): void {
1263 this.pendingResolveCts.dispose(true);
1264
1265 super.dispose();
1266 }
1268 >
1269 > //#endregion
1270 >
1271 > //#region Context
1272 >
1273 > export interface IChatEntitlementContextState extends IChatSentiment {
1274 >
1275 > /**
1276 > * Users last known or resolved entitlement.
1277 > */
1278 > entitlement: ChatEntitlement;
1279 >
1280 > /**
1281 > * User's last known or resolved raw SKU type.
1282 > */
1283 > sku: string | undefined;
1284 >
1285 > /**
1286 > * User's last known or resolved organisations.
1287 > */
1288 > organisations: string[] | undefined;
1289 >
1290 > /**
1291 > * User's Copilot tracking ID from the entitlement API.
1292 > */
1293 > copilotTrackingId: string | undefined;
1294 > }
1295 >
1296 > export class ChatEntitlementContext extends Disposable {
1297 >
1298 > private static readonly CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY = 'chat.setupContext';
1299 > private static readonly CHAT_ENTITLEMENT_CONTEXT_MIGRATED_STORAGE_KEY = 'chat.setupContext.migrated.v1';
1300 >
1301 > private static readonly CHAT_DISABLED_CONFIGURATION_KEY = 'chat.disableAIFeatures';
1302 >
1303 > private readonly canSignUpContextKey: IContextKey<boolean>;
1304 > private readonly signedOutContextKey: IContextKey<boolean>;
1305 >
1306 > private readonly freeContextKey: IContextKey<boolean>;
1307 > private readonly eduContextKey: IContextKey<boolean>;
1308 > private readonly proContextKey: IContextKey<boolean>;
1309 > private readonly proPlusContextKey: IContextKey<boolean>;
1310 > private readonly maxContextKey: IContextKey<boolean>;
1311 > private readonly businessContextKey: IContextKey<boolean>;
1312 > private readonly enterpriseContextKey: IContextKey<boolean>;
1313 >
1314 > private readonly organisationsContextKey: IContextKey<string[] | undefined>;
1315 > private readonly isInternalContextKey: IContextKey<boolean>;
1316 > private readonly skuContextKey: IContextKey<string | undefined>;
1317 >
1318 > private readonly completedContext: IContextKey<boolean>;
1319 > private readonly hiddenContext: IContextKey<boolean>;
1320 > private readonly disabledInWorkspaceContext: IContextKey<boolean>;
1321 > private readonly laterContext: IContextKey<boolean>;
1322 > private readonly installedContext: IContextKey<boolean>;
1323 > private readonly disabledContext: IContextKey<boolean>;
1324 > private readonly untrustedContext: IContextKey<boolean>;
1325 > private readonly registeredContext: IContextKey<boolean>;
1326 >
1327 > private _state: IChatEntitlementContextState;
1328 > private suspendedState: IChatEntitlementContextState | undefined = undefined;
1329 > get state(): IChatEntitlementContextState { return this.withConfiguration(this.suspendedState ?? this._state); }
1330 >
1331 > private readonly _onDidChange = this._register(new Emitter<void>());
1332 > readonly onDidChange = this._onDidChange.event;
1333 >
1334 > private updateBarrier: Barrier | undefined = undefined;
1335 >
1336 > constructor(
1337 @IContextKeyService contextKeyService: IContextKeyService,
1338 @IStorageService private readonly storageService: IStorageService,
1339 @ILogService private readonly logService: ILogService,
1340 @IConfigurationService private readonly configurationService: IConfigurationService,
1341 @ITelemetryService private readonly telemetryService: ITelemetryService
1342 ) {
1343 super();
1344
1345 this.canSignUpContextKey = ChatEntitlementContextKeys.Entitlement.canSignUp.bindTo(contextKeyService);
1346 this.signedOutContextKey = ChatEntitlementContextKeys.Entitlement.signedOut.bindTo(contextKeyService);
1347
1348 this.freeContextKey = ChatEntitlementContextKeys.Entitlement.planFree.bindTo(contextKeyService);
1349 this.eduContextKey = ChatEntitlementContextKeys.Entitlement.planEdu.bindTo(contextKeyService);
1350 this.proContextKey = ChatEntitlementContextKeys.Entitlement.planPro.bindTo(contextKeyService);
1351 this.proPlusContextKey = ChatEntitlementContextKeys.Entitlement.planProPlus.bindTo(contextKeyService);
1352 this.maxContextKey = ChatEntitlementContextKeys.Entitlement.planMax.bindTo(contextKeyService);
1353 this.businessContextKey = ChatEntitlementContextKeys.Entitlement.planBusiness.bindTo(contextKeyService);
1354 this.enterpriseContextKey = ChatEntitlementContextKeys.Entitlement.planEnterprise.bindTo(contextKeyService);
1355
1356 this.organisationsContextKey = ChatEntitlementContextKeys.Entitlement.organisations.bindTo(contextKeyService);
1357 this.isInternalContextKey = ChatEntitlementContextKeys.Entitlement.internal.bindTo(contextKeyService);
1358 this.skuContextKey = ChatEntitlementContextKeys.Entitlement.sku.bindTo(contextKeyService);
1359
1360 this.completedContext = ChatEntitlementContextKeys.Setup.completed.bindTo(contextKeyService);
1361 this.hiddenContext = ChatEntitlementContextKeys.Setup.hidden.bindTo(contextKeyService);
1362 this.disabledInWorkspaceContext = ChatEntitlementContextKeys.Setup.disabledInWorkspace.bindTo(contextKeyService);
1363 this.laterContext = ChatEntitlementContextKeys.Setup.later.bindTo(contextKeyService);
1364 this.installedContext = ChatEntitlementContextKeys.Setup.installed.bindTo(contextKeyService);
1365 this.disabledContext = ChatEntitlementContextKeys.Setup.disabled.bindTo(contextKeyService);
1366 this.untrustedContext = ChatEntitlementContextKeys.Setup.untrusted.bindTo(contextKeyService);
1367 this.registeredContext = ChatEntitlementContextKeys.Setup.registered.bindTo(contextKeyService);
1368
1369 this._state = this.storageService.getObject<IChatEntitlementContextState>(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? {
1370 entitlement: ChatEntitlement.Unknown,
1371 organisations: undefined,
1372 sku: undefined,
1373 copilotTrackingId: undefined
1374 };
1375
1376 const migrated = this.storageService.getBoolean(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_MIGRATED_STORAGE_KEY, StorageScope.PROFILE) === true;
1377 if (!migrated) {
1378 this.storageService.store(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_MIGRATED_STORAGE_KEY, true, StorageScope.PROFILE, StorageTarget.MACHINE);
1379 if (this._state.installed && !this._state.completed) {
1380 this._state.completed = true; // treat installation signal as completed signal once
1381 this.storageService.store(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, this._state, StorageScope.PROFILE, StorageTarget.MACHINE);
1382 }
1383 }
1384
1385 this.updateContextSync();
1386
1387 this.registerListeners();
1388 }
1390 > private registerListeners(): void {
1391 this._register(this.configurationService.onDidChangeConfiguration(e => {
1392 if (e.affectsConfiguration(ChatEntitlementContext.CHAT_DISABLED_CONFIGURATION_KEY)) {
1393 this.updateContext();
1394 }
1395 }));
1396 }
1398 > private _forceHidden = false;
1399 >
1400 > private withConfiguration(state: IChatEntitlementContextState): IChatEntitlementContextState {
1401 if (this._forceHidden || this.configurationService.getValue(ChatEntitlementContext.CHAT_DISABLED_CONFIGURATION_KEY) === true) {
1402 return {
1403 ...state,
1404 hidden: true
1405 };
1406 }
1407
1408 return state;
1409 }
1411 > setForceHidden(hidden: boolean): void {
1412 if (this._forceHidden !== hidden) {
1413 this._forceHidden = hidden;
1414 this.updateContext();
1415 }
1416 }
1418 > update(context: { installed: boolean; disabled: boolean; untrusted: boolean; disabledInWorkspace: boolean }): Promise<void>;
1419 > update(context: { completed: true }): Promise<void>;
1420 > 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
1421 > update(context: { later: boolean }): Promise<void>;
1422 > update(context: { entitlement: ChatEntitlement; organisations: string[] | undefined; sku: string | undefined; copilotTrackingId: string | undefined }): Promise<void>;
1423 > async update(context: { completed?: boolean; installed?: boolean; disabled?: boolean; untrusted?: boolean; disabledInWorkspace?: boolean; hidden?: false; later?: boolean; entitlement?: ChatEntitlement; organisations?: string[]; sku?: string; copilotTrackingId?: string }): Promise<void> {
1424 this.logService.trace(`[chat entitlement context] update(): ${JSON.stringify(context)}`);
1425
1426 const oldState = JSON.stringify(this._state);
1427
1428 if (typeof context.installed === 'boolean' && typeof context.disabled === 'boolean' && typeof context.untrusted === 'boolean') {
1429 this._state.installed = context.installed;
1430 this._state.disabled = context.disabled;
1431 this._state.untrusted = context.untrusted;
1432 this._state.disabledInWorkspace = context.disabledInWorkspace;
1433
1434 if (context.installed && !context.disabled) {
1435 context.hidden = false; // treat this as a sign to make Chat visible again in case it is hidden
1436 }
1437 }
1438
1439 if (typeof context.hidden === 'boolean') {
1440 this._state.hidden = context.hidden;
1441 }
1442
1443 if (typeof context.later === 'boolean') {
1444 this._state.later = context.later;
1445 }
1446
1447 if (typeof context.completed === 'boolean') {
1448 this._state.completed = context.completed;
1449 }
1450
1451 if (typeof context.entitlement === 'number') {
1452 this._state.entitlement = context.entitlement;
1453 this._state.organisations = context.organisations;
1454 this._state.sku = context.sku;
1455 this._state.copilotTrackingId = context.copilotTrackingId;
1456
1457 if (this._state.entitlement === ChatEntitlement.Free || isProUser(this._state.entitlement)) {
1458 this._state.registered = true;
1459 } else if (this._state.entitlement === ChatEntitlement.Available) {
1460 this._state.registered = false; // only reset when signed-in user can sign-up for free
1461 }
1462 }
1463
1464 if (isAnonymous(this.configurationService, this._state.entitlement, this._state)) {
1465 this._state.sku = 'no_auth_limited_copilot'; // no-auth users have a fixed SKU
1466 }
1467
1468 if (oldState === JSON.stringify(this._state)) {
1469 return; // state did not change
1470 }
1471
1472 this.storageService.store(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, {
1473 ...this._state,
1474 later: undefined // do not persist this across restarts for now
1475 }, StorageScope.PROFILE, StorageTarget.MACHINE);
1476
1477 return this.updateContext();
1478 }
1480 > private async updateContext(): Promise<void> {
1481 await this.updateBarrier?.wait();
1482
1483 this.updateContextSync();
1484 }
1486 > private updateContextSync(): void {
1487 const state = this.withConfiguration(this._state);
1488
1489 this.signedOutContextKey.set(state.entitlement === ChatEntitlement.Unknown);
1490 this.canSignUpContextKey.set(state.entitlement === ChatEntitlement.Available);
1491
1492 this.freeContextKey.set(state.entitlement === ChatEntitlement.Free);
1493 this.eduContextKey.set(state.entitlement === ChatEntitlement.EDU);
1494 this.proContextKey.set(state.entitlement === ChatEntitlement.Pro);
1495 this.proPlusContextKey.set(state.entitlement === ChatEntitlement.ProPlus);
1496 this.maxContextKey.set(state.entitlement === ChatEntitlement.Max);
1497 this.businessContextKey.set(state.entitlement === ChatEntitlement.Business);
1498 this.enterpriseContextKey.set(state.entitlement === ChatEntitlement.Enterprise);
1499
1500 this.organisationsContextKey.set(state.organisations);
1501 this.isInternalContextKey.set(Boolean(state.organisations?.some(org => org === 'github' || org === 'microsoft' || org === 'ms-copilot' || org === 'MicrosoftCopilot')));
1502 this.skuContextKey.set(state.sku);
1503
1504 this.completedContext.set(!!state.completed);
1505 this.hiddenContext.set(!!state.hidden);
1506 this.disabledInWorkspaceContext.set(!!state.disabledInWorkspace);
1507 this.laterContext.set(!!state.later);
1508 this.installedContext.set(!!state.installed);
1509 this.disabledContext.set(!!state.disabled);
1510 this.untrustedContext.set(!!state.untrusted);
1511 this.registeredContext.set(!!state.registered);
1512
1513 this.logService.trace(`[chat entitlement context] updateContext(): ${JSON.stringify(state)}`);
1514 logChatEntitlements(state, this.configurationService, this.telemetryService);
1515
1516 this._onDidChange.fire();
1517 }
1519 > suspend(): void {
1520 this.suspendedState = { ...this._state };
1521 this.updateBarrier = new Barrier();
1522 }
1524 > resume(): void {
1525 this.suspendedState = undefined;
1526 this.updateBarrier?.open();
1527 this.updateBarrier = undefined;
1528 }
1530 >
1531 > //#endregion
1532 >
1533 > registerSingleton(IChatEntitlementService, ChatEntitlementService, InstantiationType.Eager /* To ensure context keys are set asap */);