Path: blob/main/src/vs/workbench/services/chat/common/chatEntitlementService.ts
4779 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import product from '../../../../platform/product/common/product.js';6import { Barrier } from '../../../../base/common/async.js';7import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import { Lazy } from '../../../../base/common/lazy.js';10import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';11import { IRequestContext } from '../../../../base/parts/request/common/request.js';12import { localize } from '../../../../nls.js';13import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';14import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';15import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';16import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';17import { ILogService } from '../../../../platform/log/common/log.js';18import { IProductService } from '../../../../platform/product/common/productService.js';19import { asText, IRequestService } from '../../../../platform/request/common/request.js';20import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';21import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js';22import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationExtensionsService, IAuthenticationService } from '../../authentication/common/authentication.js';23import { IOpenerService } from '../../../../platform/opener/common/opener.js';24import { URI } from '../../../../base/common/uri.js';25import Severity from '../../../../base/common/severity.js';26import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';27import { isWeb } from '../../../../base/common/platform.js';28import { ILifecycleService } from '../../lifecycle/common/lifecycle.js';29import { Mutable } from '../../../../base/common/types.js';30import { distinct } from '../../../../base/common/arrays.js';31import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';32import { IObservable, observableFromEvent } from '../../../../base/common/observable.js';3334export namespace ChatEntitlementContextKeys {3536export const Setup = {37hidden: new RawContextKey<boolean>('chatSetupHidden', false, true), // True when chat setup is explicitly hidden.38installed: new RawContextKey<boolean>('chatSetupInstalled', false, true), // True when the chat extension is installed and enabled.39disabled: new RawContextKey<boolean>('chatSetupDisabled', false, true), // True when the chat extension is disabled due to any other reason than workspace trust.40untrusted: new RawContextKey<boolean>('chatSetupUntrusted', false, true), // True when the chat extension is disabled due to workspace trust.41later: new RawContextKey<boolean>('chatSetupLater', false, true), // True when the user wants to finish setup later.42registered: new RawContextKey<boolean>('chatSetupRegistered', false, true) // True when the user has registered as Free or Pro user.43};4445export const Entitlement = {46signedOut: new RawContextKey<boolean>('chatEntitlementSignedOut', false, true), // True when user is signed out.47canSignUp: new RawContextKey<boolean>('chatPlanCanSignUp', false, true), // True when user can sign up to be a chat free user.4849planFree: new RawContextKey<boolean>('chatPlanFree', false, true), // True when user is a chat free user.50planPro: new RawContextKey<boolean>('chatPlanPro', false, true), // True when user is a chat pro user.51planProPlus: new RawContextKey<boolean>('chatPlanProPlus', false, true), // True when user is a chat pro plus user.52planBusiness: new RawContextKey<boolean>('chatPlanBusiness', false, true), // True when user is a chat business user.53planEnterprise: new RawContextKey<boolean>('chatPlanEnterprise', false, true), // True when user is a chat enterprise user.5455organisations: new RawContextKey<string[]>('chatEntitlementOrganisations', undefined, true), // The organizations the user belongs to.56internal: new RawContextKey<boolean>('chatEntitlementInternal', false, true), // True when user belongs to internal organisation.57sku: new RawContextKey<string>('chatEntitlementSku', undefined, true), // The SKU of the user.58};5960export const chatQuotaExceeded = new RawContextKey<boolean>('chatQuotaExceeded', false, true);61export const completionsQuotaExceeded = new RawContextKey<boolean>('completionsQuotaExceeded', false, true);6263export const chatAnonymous = new RawContextKey<boolean>('chatAnonymous', false, true);64}6566export const IChatEntitlementService = createDecorator<IChatEntitlementService>('chatEntitlementService');6768export enum ChatEntitlement {69/** Signed out */70Unknown = 1,71/** Signed in but not yet resolved */72Unresolved = 2,73/** Signed in and entitled to Free */74Available = 3,75/** Signed in but not entitled to Free */76Unavailable = 4,77/** Signed-up to Free */78Free = 5,79/** Signed-up to Pro */80Pro = 6,81/** Signed-up to Pro Plus */82ProPlus = 7,83/** Signed-up to Business */84Business = 8,85/** Signed-up to Enterprise */86Enterprise = 9,87}8889export interface IChatSentiment {9091/**92* User has Chat installed.93*/94installed?: boolean;9596/**97* User signals no intent in using Chat.98*99* Note: in contrast to `disabled`, this should not only disable100* Chat but also hide all of its UI.101*/102hidden?: boolean;103104/**105* User signals intent to disable Chat.106*107* Note: in contrast to `hidden`, this should not hide108* Chat but but disable its functionality.109*/110disabled?: boolean;111112/**113* Chat is disabled due to missing workspace trust.114*115* Note: even though this disables Chat, we want to treat it116* different from the `disabled` state that is by explicit117* user choice.118*/119untrusted?: boolean;120121/**122* User signals intent to use Chat later.123*/124later?: boolean;125126/**127* User has registered as Free or Pro user.128*/129registered?: boolean;130}131132export interface IChatEntitlementService {133134_serviceBrand: undefined;135136readonly onDidChangeEntitlement: Event<void>;137138readonly entitlement: ChatEntitlement;139readonly entitlementObs: IObservable<ChatEntitlement>;140141readonly organisations: string[] | undefined;142readonly isInternal: boolean;143readonly sku: string | undefined;144145readonly onDidChangeQuotaExceeded: Event<void>;146readonly onDidChangeQuotaRemaining: Event<void>;147148readonly quotas: IQuotas;149150readonly onDidChangeSentiment: Event<void>;151152readonly sentiment: IChatSentiment;153readonly sentimentObs: IObservable<IChatSentiment>;154155// TODO@bpasero eventually this will become enabled by default156// and in that case we only need to check on entitlements change157// between `unknown` and any other entitlement.158readonly onDidChangeAnonymous: Event<void>;159readonly anonymous: boolean;160readonly anonymousObs: IObservable<boolean>;161162update(token: CancellationToken): Promise<void>;163}164165//#region Helper Functions166167/**168* Checks the chat entitlements to see if the user falls into the paid category169* @param chatEntitlement The chat entitlement to check170* @returns Whether or not they are a paid user171*/172export function isProUser(chatEntitlement: ChatEntitlement): boolean {173return chatEntitlement === ChatEntitlement.Pro ||174chatEntitlement === ChatEntitlement.ProPlus ||175chatEntitlement === ChatEntitlement.Business ||176chatEntitlement === ChatEntitlement.Enterprise;177}178179//#region Service Implementation180181const defaultChat = {182extensionId: product.defaultChatAgent?.extensionId ?? '',183chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '',184upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',185provider: product.defaultChatAgent?.provider ?? { default: { id: '' }, enterprise: { id: '' } },186providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '',187providerScopes: product.defaultChatAgent?.providerScopes ?? [[]],188entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '',189entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '',190completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '',191chatQuotaExceededContext: product.defaultChatAgent?.chatQuotaExceededContext ?? '',192completionsQuotaExceededContext: product.defaultChatAgent?.completionsQuotaExceededContext ?? ''193};194195interface IChatQuotasAccessor {196clearQuotas(): void;197acceptQuotas(quotas: IQuotas): void;198}199200const CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY = 'chat.allowAnonymousAccess';201202function isAnonymous(configurationService: IConfigurationService, entitlement: ChatEntitlement, sentiment: IChatSentiment): boolean {203if (configurationService.getValue(CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY) !== true) {204return false; // only enabled behind an experimental setting205}206207if (entitlement !== ChatEntitlement.Unknown) {208return false; // only consider signed out users209}210211if (sentiment.hidden || sentiment.disabled) {212return false; // only consider enabled scenarios213}214215return true;216}217218type ChatEntitlementClassification = {219owner: 'bpasero';220comment: 'Provides insight into chat entitlements.';221chatHidden: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether chat is hidden or not.' };222chatEntitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current chat entitlement of the user.' };223chatAnonymous: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is anonymously using chat.' };224chatRegistered: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is registered for chat.' };225chatDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether chat is disabled or not.' };226};227type ChatEntitlementEvent = {228chatHidden: boolean;229chatEntitlement: ChatEntitlement;230chatAnonymous: boolean;231chatRegistered: boolean;232chatDisabled: boolean;233};234235function logChatEntitlements(state: IChatEntitlementContextState, configurationService: IConfigurationService, telemetryService: ITelemetryService): void {236telemetryService.publicLog2<ChatEntitlementEvent, ChatEntitlementClassification>('chatEntitlements', {237chatHidden: Boolean(state.hidden),238chatDisabled: Boolean(state.disabled),239chatEntitlement: state.entitlement,240chatRegistered: Boolean(state.registered),241chatAnonymous: isAnonymous(configurationService, state.entitlement, state)242});243}244245export class ChatEntitlementService extends Disposable implements IChatEntitlementService {246247declare _serviceBrand: undefined;248249readonly context: Lazy<ChatEntitlementContext> | undefined;250readonly requests: Lazy<ChatEntitlementRequests> | undefined;251252constructor(253@IInstantiationService instantiationService: IInstantiationService,254@IProductService productService: IProductService,255@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,256@IContextKeyService private readonly contextKeyService: IContextKeyService,257@IConfigurationService private readonly configurationService: IConfigurationService,258@ITelemetryService private readonly telemetryService: ITelemetryService,259) {260super();261262this.chatQuotaExceededContextKey = ChatEntitlementContextKeys.chatQuotaExceeded.bindTo(this.contextKeyService);263this.completionsQuotaExceededContextKey = ChatEntitlementContextKeys.completionsQuotaExceeded.bindTo(this.contextKeyService);264265this.anonymousContextKey = ChatEntitlementContextKeys.chatAnonymous.bindTo(this.contextKeyService);266this.anonymousContextKey.set(this.anonymous);267268this.onDidChangeEntitlement = Event.map(269Event.filter(270this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([271ChatEntitlementContextKeys.Entitlement.planPro.key,272ChatEntitlementContextKeys.Entitlement.planBusiness.key,273ChatEntitlementContextKeys.Entitlement.planEnterprise.key,274ChatEntitlementContextKeys.Entitlement.planProPlus.key,275ChatEntitlementContextKeys.Entitlement.planFree.key,276ChatEntitlementContextKeys.Entitlement.canSignUp.key,277ChatEntitlementContextKeys.Entitlement.signedOut.key,278ChatEntitlementContextKeys.Entitlement.organisations.key,279ChatEntitlementContextKeys.Entitlement.internal.key,280ChatEntitlementContextKeys.Entitlement.sku.key281])), this._store282), () => { }, this._store283);284this.entitlementObs = observableFromEvent(this.onDidChangeEntitlement, () => this.entitlement);285286this.onDidChangeSentiment = Event.map(287Event.filter(288this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([289ChatEntitlementContextKeys.Setup.hidden.key,290ChatEntitlementContextKeys.Setup.disabled.key,291ChatEntitlementContextKeys.Setup.untrusted.key,292ChatEntitlementContextKeys.Setup.installed.key,293ChatEntitlementContextKeys.Setup.later.key,294ChatEntitlementContextKeys.Setup.registered.key295])), this._store296), () => { }, this._store297);298this.sentimentObs = observableFromEvent(this.onDidChangeSentiment, () => this.sentiment);299300if ((isWeb && !environmentService.remoteAuthority)) {301ChatEntitlementContextKeys.Setup.hidden.bindTo(this.contextKeyService).set(true); // hide copilot UI on web if unsupported302return;303}304305if (!productService.defaultChatAgent) {306return; // we need a default chat agent configured going forward from here307}308309const context = this.context = new Lazy(() => this._register(instantiationService.createInstance(ChatEntitlementContext)));310this.requests = new Lazy(() => this._register(instantiationService.createInstance(ChatEntitlementRequests, context.value, {311clearQuotas: () => this.clearQuotas(),312acceptQuotas: quotas => this.acceptQuotas(quotas)313})));314315this.registerListeners();316}317318//#region --- Entitlements319320readonly onDidChangeEntitlement: Event<void>;321readonly entitlementObs: IObservable<ChatEntitlement>;322323get entitlement(): ChatEntitlement {324if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planPro.key) === true) {325return ChatEntitlement.Pro;326} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planBusiness.key) === true) {327return ChatEntitlement.Business;328} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planEnterprise.key) === true) {329return ChatEntitlement.Enterprise;330} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planProPlus.key) === true) {331return ChatEntitlement.ProPlus;332} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planFree.key) === true) {333return ChatEntitlement.Free;334} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.canSignUp.key) === true) {335return ChatEntitlement.Available;336} else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.signedOut.key) === true) {337return ChatEntitlement.Unknown;338}339340return ChatEntitlement.Unresolved;341}342343get isInternal(): boolean {344return this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.internal.key) === true;345}346347get organisations(): string[] | undefined {348return this.contextKeyService.getContextKeyValue<string[]>(ChatEntitlementContextKeys.Entitlement.organisations.key);349}350351get sku(): string | undefined {352return this.contextKeyService.getContextKeyValue<string>(ChatEntitlementContextKeys.Entitlement.sku.key);353}354355//#endregion356357//#region --- Quotas358359private readonly _onDidChangeQuotaExceeded = this._register(new Emitter<void>());360readonly onDidChangeQuotaExceeded = this._onDidChangeQuotaExceeded.event;361362private readonly _onDidChangeQuotaRemaining = this._register(new Emitter<void>());363readonly onDidChangeQuotaRemaining = this._onDidChangeQuotaRemaining.event;364365private _quotas: IQuotas = {};366get quotas() { return this._quotas; }367368private readonly chatQuotaExceededContextKey: IContextKey<boolean>;369private readonly completionsQuotaExceededContextKey: IContextKey<boolean>;370371private ExtensionQuotaContextKeys = {372chatQuotaExceeded: defaultChat.chatQuotaExceededContext,373completionsQuotaExceeded: defaultChat.completionsQuotaExceededContext,374};375376private registerListeners(): void {377const quotaExceededSet = new Set([this.ExtensionQuotaContextKeys.chatQuotaExceeded, this.ExtensionQuotaContextKeys.completionsQuotaExceeded]);378379const cts = this._register(new MutableDisposable<CancellationTokenSource>());380this._register(this.contextKeyService.onDidChangeContext(e => {381if (e.affectsSome(quotaExceededSet)) {382if (cts.value) {383cts.value.cancel();384}385cts.value = new CancellationTokenSource();386this.update(cts.value.token);387}388}));389390let anonymousUsage = this.anonymous;391392const updateAnonymousUsage = () => {393const newAnonymousUsage = this.anonymous;394if (newAnonymousUsage !== anonymousUsage) {395anonymousUsage = newAnonymousUsage;396this.anonymousContextKey.set(newAnonymousUsage);397398if (this.context?.hasValue) {399logChatEntitlements(this.context.value.state, this.configurationService, this.telemetryService);400}401402this._onDidChangeAnonymous.fire();403}404};405406this._register(this.configurationService.onDidChangeConfiguration(e => {407if (e.affectsConfiguration(CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY)) {408updateAnonymousUsage();409}410}));411412this._register(this.onDidChangeEntitlement(() => updateAnonymousUsage()));413this._register(this.onDidChangeSentiment(() => updateAnonymousUsage()));414}415416acceptQuotas(quotas: IQuotas): void {417const oldQuota = this._quotas;418this._quotas = quotas;419this.updateContextKeys();420421const { changed: chatChanged } = this.compareQuotas(oldQuota.chat, quotas.chat);422const { changed: completionsChanged } = this.compareQuotas(oldQuota.completions, quotas.completions);423const { changed: premiumChatChanged } = this.compareQuotas(oldQuota.premiumChat, quotas.premiumChat);424425if (chatChanged.exceeded || completionsChanged.exceeded || premiumChatChanged.exceeded) {426this._onDidChangeQuotaExceeded.fire();427}428429if (chatChanged.remaining || completionsChanged.remaining || premiumChatChanged.remaining) {430this._onDidChangeQuotaRemaining.fire();431}432}433434private compareQuotas(oldQuota: IQuotaSnapshot | undefined, newQuota: IQuotaSnapshot | undefined): { changed: { exceeded: boolean; remaining: boolean } } {435return {436changed: {437exceeded: (oldQuota?.percentRemaining === 0) !== (newQuota?.percentRemaining === 0),438remaining: oldQuota?.percentRemaining !== newQuota?.percentRemaining439}440};441}442443clearQuotas(): void {444this.acceptQuotas({});445}446447private updateContextKeys(): void {448this.chatQuotaExceededContextKey.set(this._quotas.chat?.percentRemaining === 0);449this.completionsQuotaExceededContextKey.set(this._quotas.completions?.percentRemaining === 0);450}451452//#endregion453454//#region --- Sentiment455456readonly onDidChangeSentiment: Event<void>;457readonly sentimentObs: IObservable<IChatSentiment>;458459get sentiment(): IChatSentiment {460return {461installed: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.installed.key) === true,462hidden: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.hidden.key) === true,463disabled: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.disabled.key) === true,464untrusted: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.untrusted.key) === true,465later: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.later.key) === true,466registered: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.registered.key) === true467};468}469470//#endregion471472//region --- Anonymous473474private readonly anonymousContextKey: IContextKey<boolean>;475476private readonly _onDidChangeAnonymous = this._register(new Emitter<void>());477readonly onDidChangeAnonymous = this._onDidChangeAnonymous.event;478479readonly anonymousObs = observableFromEvent(this.onDidChangeAnonymous, () => this.anonymous);480481get anonymous(): boolean {482return isAnonymous(this.configurationService, this.entitlement, this.sentiment);483}484485//#endregion486487async update(token: CancellationToken): Promise<void> {488await this.requests?.value.forceResolveEntitlement(undefined, token);489}490}491492//#endregion493494//#region Chat Entitlement Request Service495496type EntitlementClassification = {497tid: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; comment: 'The anonymized analytics id returned by the service'; endpoint: 'GoogleAnalyticsId' };498entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' };499sku: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The SKU of the chat entitlement' };500quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat requests available to the user' };501quotaPremiumChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of premium chat requests available to the user' };502quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of inline suggestions available to the user' };503quotaResetDate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The date the quota will reset' };504owner: 'bpasero';505comment: 'Reporting chat entitlements';506};507508type EntitlementEvent = {509entitlement: ChatEntitlement;510tid: string;511sku: string | undefined;512quotaChat: number | undefined;513quotaPremiumChat: number | undefined;514quotaCompletions: number | undefined;515quotaResetDate: string | undefined;516};517518interface IQuotaSnapshotResponse {519readonly entitlement: number;520readonly overage_count: number;521readonly overage_permitted: boolean;522readonly percent_remaining: number;523readonly remaining: number;524readonly unlimited: boolean;525}526527interface ILegacyQuotaSnapshotResponse {528readonly limited_user_quotas?: {529readonly chat: number;530readonly completions: number;531};532readonly monthly_quotas?: {533readonly chat: number;534readonly completions: number;535};536}537538interface IEntitlementsResponse extends ILegacyQuotaSnapshotResponse {539readonly access_type_sku: string;540readonly assigned_date: string;541readonly can_signup_for_limited: boolean;542readonly chat_enabled: boolean;543readonly copilot_plan: string;544readonly organization_login_list: string[];545readonly analytics_tracking_id: string;546readonly limited_user_reset_date?: string; // for Copilot Free547readonly quota_reset_date?: string; // for all other Copilot SKUs548readonly quota_reset_date_utc?: string; // for all other Copilot SKUs (includes time)549readonly quota_snapshots?: {550chat?: IQuotaSnapshotResponse;551completions?: IQuotaSnapshotResponse;552premium_interactions?: IQuotaSnapshotResponse;553};554}555556interface IEntitlements {557readonly entitlement: ChatEntitlement;558readonly organisations?: string[];559readonly sku?: string;560readonly quotas?: IQuotas;561}562563export interface IQuotaSnapshot {564readonly total: number;565566readonly remaining: number;567readonly percentRemaining: number;568569readonly overageEnabled: boolean;570readonly overageCount: number;571572readonly unlimited: boolean;573}574575interface IQuotas {576readonly resetDate?: string;577readonly resetDateHasTime?: boolean;578579readonly chat?: IQuotaSnapshot;580readonly completions?: IQuotaSnapshot;581readonly premiumChat?: IQuotaSnapshot;582}583584export class ChatEntitlementRequests extends Disposable {585586static providerId(configurationService: IConfigurationService): string {587if (configurationService.getValue<string | undefined>(`${defaultChat.completionsAdvancedSetting}.authProvider`) === defaultChat.provider.enterprise.id) {588return defaultChat.provider.enterprise.id;589}590591return defaultChat.provider.default.id;592}593594private state: IEntitlements;595596private pendingResolveCts = new CancellationTokenSource();597private didResolveEntitlements = false;598599constructor(600private readonly context: ChatEntitlementContext,601private readonly chatQuotasAccessor: IChatQuotasAccessor,602@ITelemetryService private readonly telemetryService: ITelemetryService,603@IAuthenticationService private readonly authenticationService: IAuthenticationService,604@ILogService private readonly logService: ILogService,605@IRequestService private readonly requestService: IRequestService,606@IDialogService private readonly dialogService: IDialogService,607@IOpenerService private readonly openerService: IOpenerService,608@IConfigurationService private readonly configurationService: IConfigurationService,609@IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService,610@ILifecycleService private readonly lifecycleService: ILifecycleService,611) {612super();613614this.state = { entitlement: this.context.state.entitlement };615616this.registerListeners();617618this.resolve();619}620621private registerListeners(): void {622this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.resolve()));623624this._register(this.authenticationService.onDidChangeSessions(e => {625if (e.providerId === ChatEntitlementRequests.providerId(this.configurationService)) {626this.resolve();627}628}));629630this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => {631if (e.id === ChatEntitlementRequests.providerId(this.configurationService)) {632this.resolve();633}634}));635636this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => {637if (e.id === ChatEntitlementRequests.providerId(this.configurationService)) {638this.resolve();639}640}));641642this._register(this.context.onDidChange(() => {643if (!this.context.state.installed || this.context.state.disabled || this.context.state.entitlement === ChatEntitlement.Unknown) {644// When the extension is not installed, disabled or the user is not entitled645// make sure to clear quotas so that any indicators are also gone646this.state = { entitlement: this.state.entitlement, quotas: undefined };647this.chatQuotasAccessor.clearQuotas();648}649}));650}651652private async resolve(): Promise<void> {653this.pendingResolveCts.dispose(true);654const cts = this.pendingResolveCts = new CancellationTokenSource();655656const session = await this.findMatchingProviderSession(cts.token);657if (cts.token.isCancellationRequested) {658return;659}660661// Immediately signal whether we have a session or not662let state: IEntitlements | undefined = undefined;663if (session) {664// Do not overwrite any state we have already665if (this.state.entitlement === ChatEntitlement.Unknown) {666state = { entitlement: ChatEntitlement.Unresolved };667}668} else {669this.didResolveEntitlements = false; // reset so that we resolve entitlements fresh when signed in again670state = { entitlement: ChatEntitlement.Unknown };671}672if (state) {673this.update(state);674}675676if (session && !this.didResolveEntitlements) {677// Afterwards resolve entitlement with a network request678// but only unless it was not already resolved before.679await this.resolveEntitlement(session, cts.token);680}681}682683private async findMatchingProviderSession(token: CancellationToken): Promise<AuthenticationSession[] | undefined> {684const sessions = await this.doGetSessions(ChatEntitlementRequests.providerId(this.configurationService));685if (token.isCancellationRequested) {686return undefined;687}688689const matchingSessions = new Set<AuthenticationSession>();690for (const session of sessions) {691for (const scopes of defaultChat.providerScopes) {692if (this.includesScopes(session.scopes, scopes)) {693matchingSessions.add(session);694}695}696}697698// We intentionally want to return an array of matching sessions and699// not just the first, because it is possible that a matching session700// has an expired token. As such, we want to try them all until we701// succeeded with the request.702return matchingSessions.size > 0 ? Array.from(matchingSessions) : undefined;703}704705private async doGetSessions(providerId: string): Promise<readonly AuthenticationSession[]> {706const preferredAccountName = this.authenticationExtensionsService.getAccountPreference(defaultChat.chatExtensionId, providerId) ?? this.authenticationExtensionsService.getAccountPreference(defaultChat.extensionId, providerId);707let preferredAccount: AuthenticationSessionAccount | undefined;708for (const account of await this.authenticationService.getAccounts(providerId)) {709if (account.label === preferredAccountName) {710preferredAccount = account;711break;712}713}714715try {716return await this.authenticationService.getSessions(providerId, undefined, { account: preferredAccount });717} catch (error) {718// ignore - errors can throw if a provider is not registered719}720721return [];722}723724private includesScopes(scopes: ReadonlyArray<string>, expectedScopes: string[]): boolean {725return expectedScopes.every(scope => scopes.includes(scope));726}727728private async resolveEntitlement(sessions: AuthenticationSession[], token: CancellationToken): Promise<IEntitlements | undefined> {729const entitlements = await this.doResolveEntitlement(sessions, token);730if (typeof entitlements?.entitlement === 'number' && !token.isCancellationRequested) {731this.didResolveEntitlements = true;732this.update(entitlements);733}734735return entitlements;736}737738private async doResolveEntitlement(sessions: AuthenticationSession[], token: CancellationToken): Promise<IEntitlements | undefined> {739if (token.isCancellationRequested) {740return undefined;741}742743const response = await this.request(this.getEntitlementUrl(), 'GET', undefined, sessions, token);744if (token.isCancellationRequested) {745return undefined;746}747748if (!response) {749this.logService.trace('[chat entitlement]: no response');750return { entitlement: ChatEntitlement.Unresolved };751}752753if (response.res.statusCode && response.res.statusCode !== 200) {754this.logService.trace(`[chat entitlement]: unexpected status code ${response.res.statusCode}`);755return (756response.res.statusCode === 401 || // oauth token being unavailable (expired/revoked)757response.res.statusCode === 404 // missing scopes/permissions, service pretends the endpoint doesn't exist758) ? { entitlement: ChatEntitlement.Unknown /* treat as signed out */ } : { entitlement: ChatEntitlement.Unresolved };759}760761let responseText: string | null = null;762try {763responseText = await asText(response);764} catch (error) {765// ignore - handled below766}767if (token.isCancellationRequested) {768return undefined;769}770771if (!responseText) {772this.logService.trace('[chat entitlement]: response has no content');773return { entitlement: ChatEntitlement.Unresolved };774}775776let entitlementsResponse: IEntitlementsResponse;777try {778entitlementsResponse = JSON.parse(responseText);779this.logService.trace(`[chat entitlement]: parsed result is ${JSON.stringify(entitlementsResponse)}`);780} catch (err) {781this.logService.trace(`[chat entitlement]: error parsing response (${err})`);782return { entitlement: ChatEntitlement.Unresolved };783}784785let entitlement: ChatEntitlement;786if (entitlementsResponse.access_type_sku === 'free_limited_copilot') {787entitlement = ChatEntitlement.Free;788} else if (entitlementsResponse.can_signup_for_limited) {789entitlement = ChatEntitlement.Available;790} else if (entitlementsResponse.copilot_plan === 'individual') {791entitlement = ChatEntitlement.Pro;792} else if (entitlementsResponse.copilot_plan === 'individual_pro') {793entitlement = ChatEntitlement.ProPlus;794} else if (entitlementsResponse.copilot_plan === 'business') {795entitlement = ChatEntitlement.Business;796} else if (entitlementsResponse.copilot_plan === 'enterprise') {797entitlement = ChatEntitlement.Enterprise;798} else if (entitlementsResponse.chat_enabled) {799// This should never happen as we exhaustively list the plans above. But if a new plan is added in the future older clients won't break800entitlement = ChatEntitlement.Pro;801} else {802entitlement = ChatEntitlement.Unavailable;803}804805const entitlements: IEntitlements = {806entitlement,807organisations: entitlementsResponse.organization_login_list,808quotas: this.toQuotas(entitlementsResponse),809sku: entitlementsResponse.access_type_sku810};811812this.logService.trace(`[chat entitlement]: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`);813this.telemetryService.publicLog2<EntitlementEvent, EntitlementClassification>('chatInstallEntitlement', {814entitlement: entitlements.entitlement,815tid: entitlementsResponse.analytics_tracking_id,816sku: entitlements.sku,817quotaChat: entitlements.quotas?.chat?.remaining,818quotaPremiumChat: entitlements.quotas?.premiumChat?.remaining,819quotaCompletions: entitlements.quotas?.completions?.remaining,820quotaResetDate: entitlements.quotas?.resetDate821});822823return entitlements;824}825826private getEntitlementUrl(): string {827if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id) {828try {829const enterpriseUrl = new URL(this.configurationService.getValue(defaultChat.providerUriSetting));830return `${enterpriseUrl.protocol}//api.${enterpriseUrl.hostname}${enterpriseUrl.port ? ':' + enterpriseUrl.port : ''}/copilot_internal/user`;831} catch (error) {832this.logService.error(error);833}834}835836return defaultChat.entitlementUrl;837}838839private toQuotas(response: IEntitlementsResponse): IQuotas {840const quotas: Mutable<IQuotas> = {841resetDate: response.quota_reset_date_utc ?? response.quota_reset_date ?? response.limited_user_reset_date,842resetDateHasTime: typeof response.quota_reset_date_utc === 'string',843};844845// Legacy Free SKU Quota846if (response.monthly_quotas?.chat && typeof response.limited_user_quotas?.chat === 'number') {847quotas.chat = {848total: response.monthly_quotas.chat,849remaining: response.limited_user_quotas.chat,850percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.chat / response.monthly_quotas.chat) * 100)),851overageEnabled: false,852overageCount: 0,853unlimited: false854};855}856857if (response.monthly_quotas?.completions && typeof response.limited_user_quotas?.completions === 'number') {858quotas.completions = {859total: response.monthly_quotas.completions,860remaining: response.limited_user_quotas.completions,861percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.completions / response.monthly_quotas.completions) * 100)),862overageEnabled: false,863overageCount: 0,864unlimited: false865};866}867868// New Quota Snapshot869if (response.quota_snapshots) {870for (const quotaType of ['chat', 'completions', 'premium_interactions'] as const) {871const rawQuotaSnapshot = response.quota_snapshots[quotaType];872if (!rawQuotaSnapshot) {873continue;874}875const quotaSnapshot: IQuotaSnapshot = {876total: rawQuotaSnapshot.entitlement,877remaining: rawQuotaSnapshot.remaining,878percentRemaining: Math.min(100, Math.max(0, rawQuotaSnapshot.percent_remaining)),879overageEnabled: rawQuotaSnapshot.overage_permitted,880overageCount: rawQuotaSnapshot.overage_count,881unlimited: rawQuotaSnapshot.unlimited882};883884switch (quotaType) {885case 'chat':886quotas.chat = quotaSnapshot;887break;888case 'completions':889quotas.completions = quotaSnapshot;890break;891case 'premium_interactions':892quotas.premiumChat = quotaSnapshot;893break;894}895}896}897898return quotas;899}900901private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined>;902private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined>;903private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined> {904let lastRequest: IRequestContext | undefined;905906for (const session of sessions) {907if (token.isCancellationRequested) {908return lastRequest;909}910911try {912const response = await this.requestService.request({913type,914url,915data: type === 'POST' ? JSON.stringify(body) : undefined,916disableCache: true,917headers: {918'Authorization': `Bearer ${session.accessToken}`919}920}, token);921922const status = response.res.statusCode;923if (status && status !== 200) {924lastRequest = response;925continue; // try next session926}927928return response;929} catch (error) {930if (!token.isCancellationRequested) {931this.logService.error(`[chat entitlement] request: error ${error}`);932}933}934}935936return lastRequest;937}938939private update(state: IEntitlements): void {940this.state = state;941942this.context.update({ entitlement: this.state.entitlement, organisations: this.state.organisations, sku: this.state.sku });943944if (state.quotas) {945this.chatQuotasAccessor.acceptQuotas(state.quotas);946}947}948949async forceResolveEntitlement(sessions: AuthenticationSession[] | undefined, token = CancellationToken.None): Promise<IEntitlements | undefined> {950if (!sessions) {951sessions = await this.findMatchingProviderSession(token);952}953954if (!sessions || sessions.length === 0) {955return undefined;956}957958return this.resolveEntitlement(sessions, token);959}960961async signUpFree(sessions: AuthenticationSession[]): Promise<true /* signed up */ | false /* already signed up */ | { errorCode: number } /* error */> {962const body = {963restricted_telemetry: this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? 'disabled' : 'enabled',964public_code_suggestions: 'enabled'965};966967const response = await this.request(defaultChat.entitlementSignupLimitedUrl, 'POST', body, sessions, CancellationToken.None);968if (!response) {969const retry = await this.onUnknownSignUpError(localize('signUpNoResponseError', "No response received."), '[chat entitlement] sign-up: no response');970return retry ? this.signUpFree(sessions) : { errorCode: 1 };971}972973if (response.res.statusCode && response.res.statusCode !== 200) {974if (response.res.statusCode === 422) {975try {976const responseText = await asText(response);977if (responseText) {978const responseError: { message: string } = JSON.parse(responseText);979if (typeof responseError.message === 'string' && responseError.message) {980this.onUnprocessableSignUpError(`[chat entitlement] sign-up: unprocessable entity (${responseError.message})`, responseError.message);981return { errorCode: response.res.statusCode };982}983}984} catch (error) {985// ignore - handled below986}987}988const retry = await this.onUnknownSignUpError(localize('signUpUnexpectedStatusError', "Unexpected status code {0}.", response.res.statusCode), `[chat entitlement] sign-up: unexpected status code ${response.res.statusCode}`);989return retry ? this.signUpFree(sessions) : { errorCode: response.res.statusCode };990}991992let responseText: string | null = null;993try {994responseText = await asText(response);995} catch (error) {996// ignore - handled below997}998999if (!responseText) {1000const retry = await this.onUnknownSignUpError(localize('signUpNoResponseContentsError', "Response has no contents."), '[chat entitlement] sign-up: response has no content');1001return retry ? this.signUpFree(sessions) : { errorCode: 2 };1002}10031004let parsedResult: { subscribed: boolean } | undefined = undefined;1005try {1006parsedResult = JSON.parse(responseText);1007this.logService.trace(`[chat entitlement] sign-up: response is ${responseText}`);1008} catch (err) {1009const retry = await this.onUnknownSignUpError(localize('signUpInvalidResponseError', "Invalid response contents."), `[chat entitlement] sign-up: error parsing response (${err})`);1010return retry ? this.signUpFree(sessions) : { errorCode: 3 };1011}10121013// We have made it this far, so the user either did sign-up or was signed-up already.1014// That is, because the endpoint throws in all other case according to Patrick.1015this.update({ entitlement: ChatEntitlement.Free });10161017return Boolean(parsedResult?.subscribed);1018}10191020private async onUnknownSignUpError(detail: string, logMessage: string): Promise<boolean> {1021this.logService.error(logMessage);10221023if (!this.lifecycleService.willShutdown) {1024const { confirmed } = await this.dialogService.confirm({1025type: Severity.Error,1026message: localize('unknownSignUpError', "An error occurred while signing up for the GitHub Copilot Free plan. Would you like to try again?"),1027detail,1028primaryButton: localize('retry', "Retry")1029});10301031return confirmed;1032}10331034return false;1035}10361037private onUnprocessableSignUpError(logMessage: string, logDetails: string): void {1038this.logService.error(logMessage);10391040if (!this.lifecycleService.willShutdown) {1041this.dialogService.prompt({1042type: Severity.Error,1043message: localize('unprocessableSignUpError', "An error occurred while signing up for the GitHub Copilot Free plan."),1044detail: logDetails,1045buttons: [1046{1047label: localize('ok', "OK"),1048run: () => { /* noop */ }1049},1050{1051label: localize('learnMore', "Learn More"),1052run: () => this.openerService.open(URI.parse(defaultChat.upgradePlanUrl))1053}1054]1055});1056}1057}10581059async signIn(options?: { useSocialProvider?: string; additionalScopes?: readonly string[] }) {1060const providerId = ChatEntitlementRequests.providerId(this.configurationService);10611062const scopes = options?.additionalScopes ? distinct([...defaultChat.providerScopes[0], ...options.additionalScopes]) : defaultChat.providerScopes[0];1063const session = await this.authenticationService.createSession(1064providerId,1065scopes,1066{1067extraAuthorizeParameters: { get_started_with: 'copilot-vscode' },1068provider: options?.useSocialProvider1069});10701071this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account);1072this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account);10731074const entitlements = await this.forceResolveEntitlement([session]);10751076return { session, entitlements };1077}10781079override dispose(): void {1080this.pendingResolveCts.dispose(true);10811082super.dispose();1083}1084}10851086//#endregion10871088//#region Context10891090export interface IChatEntitlementContextState extends IChatSentiment {10911092/**1093* Users last known or resolved entitlement.1094*/1095entitlement: ChatEntitlement;10961097/**1098* User's last known or resolved raw SKU type.1099*/1100sku: string | undefined;11011102/**1103* User's last known or resolved organisations.1104*/1105organisations: string[] | undefined;11061107/**1108* User is or was a registered Chat user.1109*/1110registered?: boolean;1111}11121113export class ChatEntitlementContext extends Disposable {11141115private static readonly CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY = 'chat.setupContext';11161117private static readonly CHAT_DISABLED_CONFIGURATION_KEY = 'chat.disableAIFeatures';11181119private readonly canSignUpContextKey: IContextKey<boolean>;1120private readonly signedOutContextKey: IContextKey<boolean>;11211122private readonly freeContextKey: IContextKey<boolean>;1123private readonly proContextKey: IContextKey<boolean>;1124private readonly proPlusContextKey: IContextKey<boolean>;1125private readonly businessContextKey: IContextKey<boolean>;1126private readonly enterpriseContextKey: IContextKey<boolean>;11271128private readonly organisationsContextKey: IContextKey<string[] | undefined>;1129private readonly isInternalContextKey: IContextKey<boolean>;1130private readonly skuContextKey: IContextKey<string | undefined>;11311132private readonly hiddenContext: IContextKey<boolean>;1133private readonly laterContext: IContextKey<boolean>;1134private readonly installedContext: IContextKey<boolean>;1135private readonly disabledContext: IContextKey<boolean>;1136private readonly untrustedContext: IContextKey<boolean>;1137private readonly registeredContext: IContextKey<boolean>;11381139private _state: IChatEntitlementContextState;1140private suspendedState: IChatEntitlementContextState | undefined = undefined;1141get state(): IChatEntitlementContextState { return this.withConfiguration(this.suspendedState ?? this._state); }11421143private readonly _onDidChange = this._register(new Emitter<void>());1144readonly onDidChange = this._onDidChange.event;11451146private updateBarrier: Barrier | undefined = undefined;11471148constructor(1149@IContextKeyService contextKeyService: IContextKeyService,1150@IStorageService private readonly storageService: IStorageService,1151@ILogService private readonly logService: ILogService,1152@IConfigurationService private readonly configurationService: IConfigurationService,1153@ITelemetryService private readonly telemetryService: ITelemetryService1154) {1155super();11561157this.canSignUpContextKey = ChatEntitlementContextKeys.Entitlement.canSignUp.bindTo(contextKeyService);1158this.signedOutContextKey = ChatEntitlementContextKeys.Entitlement.signedOut.bindTo(contextKeyService);11591160this.freeContextKey = ChatEntitlementContextKeys.Entitlement.planFree.bindTo(contextKeyService);1161this.proContextKey = ChatEntitlementContextKeys.Entitlement.planPro.bindTo(contextKeyService);1162this.proPlusContextKey = ChatEntitlementContextKeys.Entitlement.planProPlus.bindTo(contextKeyService);1163this.businessContextKey = ChatEntitlementContextKeys.Entitlement.planBusiness.bindTo(contextKeyService);1164this.enterpriseContextKey = ChatEntitlementContextKeys.Entitlement.planEnterprise.bindTo(contextKeyService);11651166this.organisationsContextKey = ChatEntitlementContextKeys.Entitlement.organisations.bindTo(contextKeyService);1167this.isInternalContextKey = ChatEntitlementContextKeys.Entitlement.internal.bindTo(contextKeyService);1168this.skuContextKey = ChatEntitlementContextKeys.Entitlement.sku.bindTo(contextKeyService);11691170this.hiddenContext = ChatEntitlementContextKeys.Setup.hidden.bindTo(contextKeyService);1171this.laterContext = ChatEntitlementContextKeys.Setup.later.bindTo(contextKeyService);1172this.installedContext = ChatEntitlementContextKeys.Setup.installed.bindTo(contextKeyService);1173this.disabledContext = ChatEntitlementContextKeys.Setup.disabled.bindTo(contextKeyService);1174this.untrustedContext = ChatEntitlementContextKeys.Setup.untrusted.bindTo(contextKeyService);1175this.registeredContext = ChatEntitlementContextKeys.Setup.registered.bindTo(contextKeyService);11761177this._state = this.storageService.getObject<IChatEntitlementContextState>(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown, organisations: undefined, sku: undefined };11781179this.updateContextSync();11801181this.registerListeners();1182}11831184private registerListeners(): void {1185this._register(this.configurationService.onDidChangeConfiguration(e => {1186if (e.affectsConfiguration(ChatEntitlementContext.CHAT_DISABLED_CONFIGURATION_KEY)) {1187this.updateContext();1188}1189}));1190}11911192private withConfiguration(state: IChatEntitlementContextState): IChatEntitlementContextState {1193if (this.configurationService.getValue(ChatEntitlementContext.CHAT_DISABLED_CONFIGURATION_KEY) === true) {1194return {1195...state,1196hidden: true // Setting always wins: if AI is disabled, set `hidden: true`1197};1198}11991200return state;1201}12021203update(context: { installed: boolean; disabled: boolean; untrusted: boolean }): Promise<void>;1204update(context: { hidden: false }): Promise<void>; // legacy UI state from before we had a setting to hide, keep around to still support users who used this1205update(context: { later: boolean }): Promise<void>;1206update(context: { entitlement: ChatEntitlement; organisations: string[] | undefined; sku: string | undefined }): Promise<void>;1207async update(context: { installed?: boolean; disabled?: boolean; untrusted?: boolean; hidden?: false; later?: boolean; entitlement?: ChatEntitlement; organisations?: string[]; sku?: string }): Promise<void> {1208this.logService.trace(`[chat entitlement context] update(): ${JSON.stringify(context)}`);12091210const oldState = JSON.stringify(this._state);12111212if (typeof context.installed === 'boolean' && typeof context.disabled === 'boolean' && typeof context.untrusted === 'boolean') {1213this._state.installed = context.installed;1214this._state.disabled = context.disabled;1215this._state.untrusted = context.untrusted;12161217if (context.installed && !context.disabled) {1218context.hidden = false; // treat this as a sign to make Chat visible again in case it is hidden1219}1220}12211222if (typeof context.hidden === 'boolean') {1223this._state.hidden = context.hidden;1224}12251226if (typeof context.later === 'boolean') {1227this._state.later = context.later;1228}12291230if (typeof context.entitlement === 'number') {1231this._state.entitlement = context.entitlement;1232this._state.organisations = context.organisations;1233this._state.sku = context.sku;12341235if (this._state.entitlement === ChatEntitlement.Free || isProUser(this._state.entitlement)) {1236this._state.registered = true;1237} else if (this._state.entitlement === ChatEntitlement.Available) {1238this._state.registered = false; // only reset when signed-in user can sign-up for free1239}1240}12411242if (isAnonymous(this.configurationService, this._state.entitlement, this._state)) {1243this._state.sku = 'no_auth_limited_copilot'; // no-auth users have a fixed SKU1244}12451246if (oldState === JSON.stringify(this._state)) {1247return; // state did not change1248}12491250this.storageService.store(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, {1251...this._state,1252later: undefined // do not persist this across restarts for now1253}, StorageScope.PROFILE, StorageTarget.MACHINE);12541255return this.updateContext();1256}12571258private async updateContext(): Promise<void> {1259await this.updateBarrier?.wait();12601261this.updateContextSync();1262}12631264private updateContextSync(): void {1265const state = this.withConfiguration(this._state);12661267this.signedOutContextKey.set(state.entitlement === ChatEntitlement.Unknown);1268this.canSignUpContextKey.set(state.entitlement === ChatEntitlement.Available);12691270this.freeContextKey.set(state.entitlement === ChatEntitlement.Free);1271this.proContextKey.set(state.entitlement === ChatEntitlement.Pro);1272this.proPlusContextKey.set(state.entitlement === ChatEntitlement.ProPlus);1273this.businessContextKey.set(state.entitlement === ChatEntitlement.Business);1274this.enterpriseContextKey.set(state.entitlement === ChatEntitlement.Enterprise);12751276this.organisationsContextKey.set(state.organisations);1277this.isInternalContextKey.set(Boolean(state.organisations?.some(org => org === 'github' || org === 'microsoft' || org === 'ms-copilot' || org === 'MicrosoftCopilot')));1278this.skuContextKey.set(state.sku);12791280this.hiddenContext.set(!!state.hidden);1281this.laterContext.set(!!state.later);1282this.installedContext.set(!!state.installed);1283this.disabledContext.set(!!state.disabled);1284this.untrustedContext.set(!!state.untrusted);1285this.registeredContext.set(!!state.registered);12861287this.logService.trace(`[chat entitlement context] updateContext(): ${JSON.stringify(state)}`);1288logChatEntitlements(state, this.configurationService, this.telemetryService);12891290this._onDidChange.fire();1291}12921293suspend(): void {1294this.suspendedState = { ...this._state };1295this.updateBarrier = new Barrier();1296}12971298resume(): void {1299this.suspendedState = undefined;1300this.updateBarrier?.open();1301this.updateBarrier = undefined;1302}1303}13041305//#endregion13061307registerSingleton(IChatEntitlementService, ChatEntitlementService, InstantiationType.Eager /* To ensure context keys are set asap */);130813091310