Path: blob/main/extensions/copilot/src/extension/chatInputNotification/vscode-node/chatInputNotification.contribution.ts
13399 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 * as vscode from 'vscode';6import { IAuthenticationService } from '../../../platform/authentication/common/authentication';7import { IChatQuota, IChatQuotaService } from '../../../platform/chat/common/chatQuotaService';8import { Disposable } from '../../../util/vs/base/common/lifecycle';910const QUOTA_NOTIFICATION_ID = 'copilot.quotaStatus';11const THRESHOLDS = [50, 75, 90, 95];1213interface IRateLimitWarning {14percentUsed: number;15type: 'session' | 'weekly';16resetDate: Date;17}1819interface IQuotaWarning {20percentUsed: number;21resetDate: Date;22}2324/**25* Manages a single chat input notification for quota and rate limit status.26*27* Listens to {@link IChatQuotaService.onDidChange} and determines whether a28* new threshold has been crossed, then shows the highest-priority notification:29*30* 1. **Quota exhausted** — info, not auto-dismissed, only dismissible via X.31* 2. **Quota approaching** — info, auto-dismissed on next message.32* 3. **Rate-limit warning** — info, auto-dismissed on next message.33*/34export class ChatInputNotificationContribution extends Disposable {3536private _notification: vscode.ChatInputNotification | undefined;37/** Tracks whether the current notification is the quota-exhausted variant. */38private _showingExhausted = false;3940private readonly _shownQuotaThresholds = new Set<number>();41private readonly _shownSessionThresholds = new Set<number>();42private readonly _shownWeeklyThresholds = new Set<number>();4344constructor(45@IAuthenticationService private readonly _authService: IAuthenticationService,46@IChatQuotaService private readonly _chatQuotaService: IChatQuotaService,47) {48super();49this._register(this._authService.onDidAuthenticationChange(() => this._update()));50this._register(this._chatQuotaService.onDidChange(() => this._update()));51}5253/**54* Single entry point that determines the highest-priority notification55* to show (or whether to hide).56*/57private _update(): void {58// Priority 1: Quota exhausted — sticky info notification59if (this._chatQuotaService.quotaExhausted) {60const isAnonymous = this._authService.copilotToken?.isNoAuthUser;61const isFree = this._authService.copilotToken?.isFreeUser;62if (isAnonymous || isFree) {63this._showExhaustedNotification(!!isAnonymous);64return;65}66}6768// Priority 2: Quota approaching threshold69const quotaWarning = this._computeQuotaWarning();70if (quotaWarning) {71this._showQuotaApproachingWarning(quotaWarning);72return;73}7475// Priority 3: Rate-limit warning (session > weekly)76const rateLimitWarning = this._computeRateLimitWarning();77if (rateLimitWarning) {78this._showRateLimitWarning(rateLimitWarning);79return;80}8182// Nothing new to show — only hide if the exhausted notification is83// active and the quota is no longer exhausted (state-driven).84if (this._showingExhausted && !this._chatQuotaService.quotaExhausted) {85this._hideNotification();86}87}8889// --- Threshold computation -----------------------------------------------9091private _computeQuotaWarning(): IQuotaWarning | undefined {92const info = this._chatQuotaService.quotaInfo;93if (!info || info.unlimited || info.additionalUsageEnabled) {94return undefined;95}96return this._checkThreshold(info, this._shownQuotaThresholds);97}9899private _computeRateLimitWarning(): IRateLimitWarning | undefined {100const { session, weekly } = this._chatQuotaService.rateLimitInfo;101const sessionWarning = this._checkThreshold(session, this._shownSessionThresholds);102if (sessionWarning) {103return { ...sessionWarning, type: 'session' };104}105const weeklyWarning = this._checkThreshold(weekly, this._shownWeeklyThresholds);106if (weeklyWarning) {107return { ...weeklyWarning, type: 'weekly' };108}109return undefined;110}111112/**113* Checks whether a quota/rate-limit info has crossed a new threshold114* that hasn't been shown yet. Clears stale thresholds when usage drops.115*/116private _checkThreshold(info: IChatQuota | undefined, shownThresholds: Set<number>): { percentUsed: number; resetDate: Date } | undefined {117if (!info) {118shownThresholds.clear();119return undefined;120}121if (info.unlimited) {122return undefined;123}124125const percentUsed = 100 - info.percentRemaining;126127// Clear thresholds that are no longer crossed (usage dropped)128for (const threshold of shownThresholds) {129if (percentUsed < threshold) {130shownThresholds.delete(threshold);131}132}133134// Walk thresholds highest-first so we report the most severe crossed threshold135for (let i = THRESHOLDS.length - 1; i >= 0; i--) {136const threshold = THRESHOLDS[i];137if (percentUsed >= threshold && !shownThresholds.has(threshold)) {138// Mark this and all lower thresholds as shown139for (let j = 0; j <= i; j++) {140shownThresholds.add(THRESHOLDS[j]);141}142return { percentUsed: Math.round(percentUsed), resetDate: info.resetDate };143}144}145return undefined;146}147148// --- Quota exhausted ---------------------------------------------------149150private _showExhaustedNotification(isAnonymous: boolean): void {151const notification = this._ensureNotification();152this._showingExhausted = true;153154notification.severity = vscode.ChatInputNotificationSeverity.Info;155notification.dismissible = true;156notification.autoDismissOnMessage = false;157158if (isAnonymous) {159notification.message = vscode.l10n.t('Monthly Limit Reached');160notification.description = vscode.l10n.t("You've made the most of Copilot. Sign in to keep going.");161notification.actions = [162{ label: vscode.l10n.t('View Usage'), commandId: 'workbench.action.chat.openCopilotStatus' },163{ label: vscode.l10n.t('Sign In'), commandId: 'workbench.action.chat.triggerSetup' },164];165} else {166notification.message = vscode.l10n.t('Monthly Limit Reached');167notification.description = vscode.l10n.t("You've made the most of Copilot Free. Upgrade to keep going.");168notification.actions = [169{ label: vscode.l10n.t('View Usage'), commandId: 'workbench.action.chat.openCopilotStatus' },170{ label: vscode.l10n.t('Upgrade'), commandId: 'workbench.action.chat.upgradePlan' },171];172}173174notification.show();175}176177// --- Quota approaching --------------------------------------------------178179private _showQuotaApproachingWarning(warning: IQuotaWarning): void {180const notification = this._ensureNotification();181this._showingExhausted = false;182183notification.severity = vscode.ChatInputNotificationSeverity.Info;184notification.dismissible = true;185notification.autoDismissOnMessage = true;186notification.message = vscode.l10n.t('Monthly Limit at {0}%', warning.percentUsed);187notification.description = vscode.l10n.t("You're getting the most out of Copilot \u2014 upgrade to keep going.");188notification.actions = [189{ label: vscode.l10n.t('View Usage'), commandId: 'workbench.action.chat.openCopilotStatus' },190{ label: vscode.l10n.t('Upgrade'), commandId: 'workbench.action.chat.upgradePlan' },191];192193notification.show();194}195196// --- Rate limit warning -------------------------------------------------197198private _showRateLimitWarning(warning: IRateLimitWarning): void {199const notification = this._ensureNotification();200this._showingExhausted = false;201202const dateStr = this._formatResetDate(warning.resetDate);203notification.severity = vscode.ChatInputNotificationSeverity.Info;204notification.dismissible = true;205notification.autoDismissOnMessage = true;206207notification.message = warning.type === 'session'208? vscode.l10n.t("You've used {0}% of your session rate limit.", warning.percentUsed)209: vscode.l10n.t("You've used {0}% of your weekly rate limit.", warning.percentUsed);210notification.description = vscode.l10n.t('Resets on {0}.', dateStr);211notification.actions = [];212213notification.show();214}215216// --- Helpers ------------------------------------------------------------217218private _formatResetDate(resetDate: Date): string {219const now = new Date();220const includeYear = resetDate.getFullYear() !== now.getFullYear();221return new Intl.DateTimeFormat(undefined, includeYear222? { month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' }223: { month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit' }224).format(resetDate);225}226227private _ensureNotification(): vscode.ChatInputNotification {228if (!this._notification) {229this._notification = vscode.chat.createInputNotification(QUOTA_NOTIFICATION_ID);230this._register({ dispose: () => this._notification?.dispose() });231}232return this._notification;233}234235private _hideNotification(): void {236if (this._notification) {237this._notification.hide();238}239}240}241242243