Не подтверждена Коммит 27ada980 создал по автору Benjamin Pasero's avatar Benjamin Pasero Зафиксировано автором GitHub
Просмотр файлов

chat status tweaks (#241561)

владелец 92803a9d
Это отличие свёрнуто
......@@ -80,7 +80,8 @@ import { ChatPasteProvidersFeature } from './chatPasteProviders.js';
import { QuickChatService } from './chatQuick.js';
import { ChatQuotasService, IChatQuotasService } from '../common/chatQuotasService.js';
import { ChatResponseAccessibleView } from './chatResponseAccessibleView.js';
import { ChatSetupContribution } from './chatSetup.js';
import { ChatEntitlementsService, ChatSetupContribution } from './chatSetup.js';
import { IChatEntitlementsService } from '../common/chatEntitlementsService.js';
import { ChatVariablesService } from './chatVariables.js';
import { ChatWidgetService } from './chatWidget.js';
import { ChatCodeBlockContextProviderService } from './codeBlockContextProviderService.js';
......@@ -469,5 +470,6 @@ registerSingleton(IChatEditingService, ChatEditingService, InstantiationType.Del
registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, InstantiationType.Delayed);
registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed);
registerSingleton(IChatQuotasService, ChatQuotasService, InstantiationType.Delayed);
registerSingleton(IChatEntitlementsService, ChatEntitlementsService, InstantiationType.Delayed);
registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed);
......@@ -69,6 +69,7 @@ import { IQuickInputService } from '../../../../platform/quickinput/common/quick
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
import { equalsIgnoreCase } from '../../../../base/common/strings.js';
import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js';
import { ChatEntitlement, IChatEntitlements, IChatEntitlementsService } from '../common/chatEntitlementsService.js';
const defaultChat = {
extensionId: product.defaultChatAgent?.extensionId ?? '',
......@@ -91,21 +92,40 @@ const defaultChat = {
manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',
};
enum ChatEntitlement {
/** Signed out */
Unknown = 1,
/** Signed in but not yet resolved */
Unresolved,
/** Signed in and entitled to Limited */
Available,
/** Signed in but not entitled to Limited */
Unavailable,
/** Signed-up to Limited */
Limited,
/** Signed-up to Pro */
Pro
//#region Service
export class ChatEntitlementsService extends Disposable implements IChatEntitlementsService {
declare _serviceBrand: undefined;
readonly context: ChatSetupContext | undefined;
readonly requests: ChatSetupRequests | undefined;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IProductService productService: IProductService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService
) {
super();
if (
!productService.defaultChatAgent || // needs product config
(isWeb && !environmentService.remoteAuthority) // only enabled locally or a remote backend
) {
return;
}
this.context = this._register(instantiationService.createInstance(ChatSetupContext));
this.requests = this._register(instantiationService.createInstance(ChatSetupRequests, this.context));
}
async resolve(token: CancellationToken): Promise<IChatEntitlements | undefined> {
return this.requests?.forceResolveEntitlement(undefined, token);
}
}
//#endregion
//#region Contribution
export class ChatSetupContribution extends Disposable implements IWorkbenchContribution {
......@@ -115,22 +135,19 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr
constructor(
@IProductService private readonly productService: IProductService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@ICommandService private readonly commandService: ICommandService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService,
@IChatEntitlementsService chatEntitlementsService: ChatEntitlementsService,
) {
super();
if (
!this.productService.defaultChatAgent || // needs product config
(isWeb && !this.environmentService.remoteAuthority) // only enabled locally or a remote backend
) {
return;
const context = chatEntitlementsService.context;
const requests = chatEntitlementsService.requests;
if (!context || !requests) {
return; // disabled
}
const context = this._register(this.instantiationService.createInstance(ChatSetupContext));
const requests = this._register(this.instantiationService.createInstance(ChatSetupRequests, context));
const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests)));
this.registerChatWelcome(controller, context);
......@@ -285,8 +302,8 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr
if (focus) {
windowFocusListener.clear();
const entitlement = await requests.forceResolveEntitlement(undefined);
if (entitlement === ChatEntitlement.Pro) {
const entitlements = await requests.forceResolveEntitlement(undefined);
if (entitlements?.entitlement === ChatEntitlement.Pro) {
refreshTokens(commandService);
}
}
......@@ -384,21 +401,6 @@ interface IEntitlementsResponse {
readonly limited_user_reset_date: string;
}
interface IQuotas {
readonly chatTotal?: number;
readonly completionsTotal?: number;
readonly chatRemaining?: number;
readonly completionsRemaining?: number;
readonly resetDate?: string;
}
interface IChatEntitlements {
readonly entitlement: ChatEntitlement;
readonly quotas?: IQuotas;
}
class ChatSetupRequests extends Disposable {
static providerId(configurationService: IConfigurationService): string {
......@@ -525,14 +527,14 @@ class ChatSetupRequests extends Disposable {
return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope));
}
private async resolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise<ChatEntitlement | undefined> {
private async resolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise<IChatEntitlements | undefined> {
const entitlements = await this.doResolveEntitlement(session, token);
if (typeof entitlements?.entitlement === 'number' && !token.isCancellationRequested) {
this.didResolveEntitlements = true;
this.update(entitlements);
}
return entitlements?.entitlement;
return entitlements;
}
private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise<IChatEntitlements | undefined> {
......@@ -656,16 +658,16 @@ class ChatSetupRequests extends Disposable {
}
}
async forceResolveEntitlement(session: AuthenticationSession | undefined): Promise<ChatEntitlement | undefined> {
async forceResolveEntitlement(session: AuthenticationSession | undefined, token = CancellationToken.None): Promise<IChatEntitlements | undefined> {
if (!session) {
session = await this.findMatchingProviderSession(CancellationToken.None);
session = await this.findMatchingProviderSession(token);
}
if (!session) {
return undefined;
}
return this.resolveEntitlement(session, CancellationToken.None);
return this.resolveEntitlement(session, token);
}
async signUpLimited(session: AuthenticationSession): Promise<true /* signed up */ | false /* already signed up */ | { errorCode: number } /* error */> {
......@@ -907,7 +909,7 @@ class ChatSetupController extends Disposable {
private async signIn(providerId: string): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> {
let session: AuthenticationSession | undefined;
let entitlement: ChatEntitlement | undefined;
let entitlements: IChatEntitlements | undefined;
try {
showCopilotView(this.viewsService, this.layoutService);
......@@ -916,7 +918,7 @@ class ChatSetupController extends Disposable {
this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account);
this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account);
entitlement = await this.requests.forceResolveEntitlement(session);
entitlements = await this.requests.forceResolveEntitlement(session);
} catch (e) {
this.logService.error(`[chat setup] signIn: error ${e}`);
}
......@@ -934,7 +936,7 @@ class ChatSetupController extends Disposable {
}
}
return { session, entitlement };
return { session, entitlement: entitlements?.entitlement };
}
private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch,): Promise<void> {
......
......@@ -3,7 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js';
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { localize } from '../../../../nls.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
......@@ -13,8 +14,9 @@ import { IWorkbenchContribution } from '../../../common/contributions.js';
import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js';
import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { IChatEntitlementsService } from '../common/chatEntitlementsService.js';
import { IChatQuotasService } from '../common/chatQuotasService.js';
import { quotaToButtonMessage, OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, CHAT_SETUP_ACTION_ID, CHAT_SETUP_ACTION_LABEL, CHAT_OPEN_ACTION_ID } from './actions/chatActions.js';
import { quotaToButtonMessage, OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, CHAT_SETUP_ACTION_LABEL, TOGGLE_CHAT_ACTION_ID } from './actions/chatActions.js';
export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution {
......@@ -27,6 +29,7 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu
constructor(
@IStatusbarService private readonly statusbarService: IStatusbarService,
@IChatQuotasService private readonly chatQuotasService: IChatQuotasService,
@IChatEntitlementsService private readonly chatEntitlementsService: IChatEntitlementsService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IWorkbenchAssignmentService private readonly assignmentService: IWorkbenchAssignmentService,
@IProductService private readonly productService: IProductService,
......@@ -73,14 +76,14 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu
this.statusbarService.updateEntryVisibility(ChatStatusBarEntry.ID, !this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Setup.hidden.key));
}));
this._register(this.chatQuotasService.onDidChangeQuotas(() => this.entry?.update(this.getEntryProps())));
this._register(this.chatQuotasService.onDidChangeQuotaExceeded(() => this.entry?.update(this.getEntryProps())));
}
private getEntryProps(): IStatusbarEntry {
let text = '$(copilot)';
let ariaLabel = localize('chatStatus', "Copilot Status");
let command = CHAT_OPEN_ACTION_ID;
let tooltip: string | IMarkdownString = localize('openChat', "Open Chat ({0})", this.keybindingService.lookupKeybinding(command)?.getLabel() ?? '');
let command = TOGGLE_CHAT_ACTION_ID;
let tooltip: string | IManagedHoverTooltipMarkdownString = localize('openChat', "Open Chat ({0})", this.keybindingService.lookupKeybinding(command)?.getLabel() ?? '');
// Quota Exceeded
const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatQuotasService.quotas;
......@@ -105,7 +108,6 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu
this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Setup.installed.key) === false ||
this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Setup.canSignUp.key) === true
) {
command = CHAT_SETUP_ACTION_ID;
tooltip = CHAT_SETUP_ACTION_LABEL.value;
}
......@@ -118,15 +120,29 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu
// Copilot Limited User
else if (this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Setup.limited.key) === true) {
const { chatTotal, chatRemaining, completionsTotal, completionsRemaining } = this.chatQuotasService.quotas;
if (typeof chatRemaining === 'number' && typeof chatTotal === 'number' && typeof completionsRemaining === 'number' && typeof completionsTotal === 'number') {
tooltip = new MarkdownString([
localize('limitTitle', "You are currently using Copilot Free"),
'---',
localize('limitChatQuota', "<code>{0}</code> of <code>{1}</code> chats remaining", chatRemaining, chatTotal),
localize('limitCompletionsQuota', "<code>{0}</code> of <code>{1}</code> code completions remaining", completionsRemaining, completionsTotal),
].join('\n\n'), { supportHtml: true });
}
const that = this;
tooltip = {
async markdown(token) {
const entitlements = await that.chatEntitlementsService.resolve(token);
if (token.isCancellationRequested || !entitlements?.quotas) {
return;
}
const { chatTotal, chatRemaining, completionsTotal, completionsRemaining } = entitlements.quotas;
if (typeof chatRemaining === 'number' && typeof chatTotal === 'number' && typeof completionsRemaining === 'number' && typeof completionsTotal === 'number') {
return new MarkdownString([
localize('limitTitle', "You are currently using Copilot Free"),
'---',
localize('limitChatQuota', "<code>{0}</code> of <code>{1}</code> chats remaining", chatRemaining, chatTotal),
localize('limitCompletionsQuota', "<code>{0}</code> of <code>{1}</code> code completions remaining", completionsRemaining, completionsTotal),
].join('\n\n'), { supportHtml: true });
}
return undefined;
},
markdownNotSupportedFallback: undefined
};
}
return {
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
export const IChatEntitlementsService = createDecorator<IChatEntitlementsService>('chatEntitlementsService');
export enum ChatEntitlement {
/** Signed out */
Unknown = 1,
/** Signed in but not yet resolved */
Unresolved,
/** Signed in and entitled to Limited */
Available,
/** Signed in but not entitled to Limited */
Unavailable,
/** Signed-up to Limited */
Limited,
/** Signed-up to Pro */
Pro
}
export interface IChatEntitlements {
readonly entitlement: ChatEntitlement;
readonly quotas?: IQuotas;
}
export interface IQuotas {
readonly chatTotal?: number;
readonly completionsTotal?: number;
readonly chatRemaining?: number;
readonly completionsRemaining?: number;
readonly resetDate?: string;
}
export interface IChatEntitlementsService {
_serviceBrand: undefined;
resolve(token: CancellationToken): Promise<IChatEntitlements | undefined>;
}
......@@ -15,7 +15,9 @@ export interface IChatQuotasService {
_serviceBrand: undefined;
readonly onDidChangeQuotas: Event<void>;
readonly onDidChangeQuotaExceeded: Event<void>;
readonly onDidChangeQuotaRemaining: Event<void>;
readonly quotas: IChatQuotas;
acceptQuotas(quotas: IChatQuotas): void;
......@@ -38,8 +40,11 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService
declare _serviceBrand: undefined;
private readonly _onDidChangeQuotas = this._register(new Emitter<void>());
readonly onDidChangeQuotas: Event<void> = this._onDidChangeQuotas.event;
private readonly _onDidChangeQuotaExceeded = this._register(new Emitter<void>());
readonly onDidChangeQuotaExceeded = this._onDidChangeQuotaExceeded.event;
private readonly _onDidChangeQuotaRemaining = this._register(new Emitter<void>());
readonly onDidChangeQuotaRemaining = this._onDidChangeQuotaRemaining.event;
private _quotas: IChatQuotas = { chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined };
get quotas(): IChatQuotas { return this._quotas; }
......@@ -84,16 +89,29 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService
if (changed) {
this.updateContextKeys();
this._onDidChangeQuotas.fire();
this._onDidChangeQuotaExceeded.fire();
}
}));
}
acceptQuotas(quotas: IChatQuotas): void {
const oldQuota = this._quotas;
this._quotas = this.massageQuotas(quotas);
this.updateContextKeys();
this._onDidChangeQuotas.fire();
if (
oldQuota.chatQuotaExceeded !== this._quotas.chatQuotaExceeded ||
oldQuota.completionsQuotaExceeded !== this._quotas.completionsQuotaExceeded
) {
this._onDidChangeQuotaExceeded.fire();
}
if (
oldQuota.chatRemaining !== this._quotas.chatRemaining ||
oldQuota.completionsRemaining !== this._quotas.completionsRemaining
) {
this._onDidChangeQuotaRemaining.fire();
}
}
private massageQuotas(quotas: IChatQuotas): IChatQuotas {
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать