Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatInputNotification/vscode-node/chatInputNotification.contribution.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as vscode from 'vscode';
7
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
8
import { IChatQuota, IChatQuotaService } from '../../../platform/chat/common/chatQuotaService';
9
import { Disposable } from '../../../util/vs/base/common/lifecycle';
10
11
const QUOTA_NOTIFICATION_ID = 'copilot.quotaStatus';
12
const THRESHOLDS = [50, 75, 90, 95];
13
14
interface IRateLimitWarning {
15
percentUsed: number;
16
type: 'session' | 'weekly';
17
resetDate: Date;
18
}
19
20
interface IQuotaWarning {
21
percentUsed: number;
22
resetDate: Date;
23
}
24
25
/**
26
* Manages a single chat input notification for quota and rate limit status.
27
*
28
* Listens to {@link IChatQuotaService.onDidChange} and determines whether a
29
* new threshold has been crossed, then shows the highest-priority notification:
30
*
31
* 1. **Quota exhausted** — info, not auto-dismissed, only dismissible via X.
32
* 2. **Quota approaching** — info, auto-dismissed on next message.
33
* 3. **Rate-limit warning** — info, auto-dismissed on next message.
34
*/
35
export class ChatInputNotificationContribution extends Disposable {
36
37
private _notification: vscode.ChatInputNotification | undefined;
38
/** Tracks whether the current notification is the quota-exhausted variant. */
39
private _showingExhausted = false;
40
41
private readonly _shownQuotaThresholds = new Set<number>();
42
private readonly _shownSessionThresholds = new Set<number>();
43
private readonly _shownWeeklyThresholds = new Set<number>();
44
45
constructor(
46
@IAuthenticationService private readonly _authService: IAuthenticationService,
47
@IChatQuotaService private readonly _chatQuotaService: IChatQuotaService,
48
) {
49
super();
50
this._register(this._authService.onDidAuthenticationChange(() => this._update()));
51
this._register(this._chatQuotaService.onDidChange(() => this._update()));
52
}
53
54
/**
55
* Single entry point that determines the highest-priority notification
56
* to show (or whether to hide).
57
*/
58
private _update(): void {
59
// Priority 1: Quota exhausted — sticky info notification
60
if (this._chatQuotaService.quotaExhausted) {
61
const isAnonymous = this._authService.copilotToken?.isNoAuthUser;
62
const isFree = this._authService.copilotToken?.isFreeUser;
63
if (isAnonymous || isFree) {
64
this._showExhaustedNotification(!!isAnonymous);
65
return;
66
}
67
}
68
69
// Priority 2: Quota approaching threshold
70
const quotaWarning = this._computeQuotaWarning();
71
if (quotaWarning) {
72
this._showQuotaApproachingWarning(quotaWarning);
73
return;
74
}
75
76
// Priority 3: Rate-limit warning (session > weekly)
77
const rateLimitWarning = this._computeRateLimitWarning();
78
if (rateLimitWarning) {
79
this._showRateLimitWarning(rateLimitWarning);
80
return;
81
}
82
83
// Nothing new to show — only hide if the exhausted notification is
84
// active and the quota is no longer exhausted (state-driven).
85
if (this._showingExhausted && !this._chatQuotaService.quotaExhausted) {
86
this._hideNotification();
87
}
88
}
89
90
// --- Threshold computation -----------------------------------------------
91
92
private _computeQuotaWarning(): IQuotaWarning | undefined {
93
const info = this._chatQuotaService.quotaInfo;
94
if (!info || info.unlimited || info.additionalUsageEnabled) {
95
return undefined;
96
}
97
return this._checkThreshold(info, this._shownQuotaThresholds);
98
}
99
100
private _computeRateLimitWarning(): IRateLimitWarning | undefined {
101
const { session, weekly } = this._chatQuotaService.rateLimitInfo;
102
const sessionWarning = this._checkThreshold(session, this._shownSessionThresholds);
103
if (sessionWarning) {
104
return { ...sessionWarning, type: 'session' };
105
}
106
const weeklyWarning = this._checkThreshold(weekly, this._shownWeeklyThresholds);
107
if (weeklyWarning) {
108
return { ...weeklyWarning, type: 'weekly' };
109
}
110
return undefined;
111
}
112
113
/**
114
* Checks whether a quota/rate-limit info has crossed a new threshold
115
* that hasn't been shown yet. Clears stale thresholds when usage drops.
116
*/
117
private _checkThreshold(info: IChatQuota | undefined, shownThresholds: Set<number>): { percentUsed: number; resetDate: Date } | undefined {
118
if (!info) {
119
shownThresholds.clear();
120
return undefined;
121
}
122
if (info.unlimited) {
123
return undefined;
124
}
125
126
const percentUsed = 100 - info.percentRemaining;
127
128
// Clear thresholds that are no longer crossed (usage dropped)
129
for (const threshold of shownThresholds) {
130
if (percentUsed < threshold) {
131
shownThresholds.delete(threshold);
132
}
133
}
134
135
// Walk thresholds highest-first so we report the most severe crossed threshold
136
for (let i = THRESHOLDS.length - 1; i >= 0; i--) {
137
const threshold = THRESHOLDS[i];
138
if (percentUsed >= threshold && !shownThresholds.has(threshold)) {
139
// Mark this and all lower thresholds as shown
140
for (let j = 0; j <= i; j++) {
141
shownThresholds.add(THRESHOLDS[j]);
142
}
143
return { percentUsed: Math.round(percentUsed), resetDate: info.resetDate };
144
}
145
}
146
return undefined;
147
}
148
149
// --- Quota exhausted ---------------------------------------------------
150
151
private _showExhaustedNotification(isAnonymous: boolean): void {
152
const notification = this._ensureNotification();
153
this._showingExhausted = true;
154
155
notification.severity = vscode.ChatInputNotificationSeverity.Info;
156
notification.dismissible = true;
157
notification.autoDismissOnMessage = false;
158
159
if (isAnonymous) {
160
notification.message = vscode.l10n.t('Monthly Limit Reached');
161
notification.description = vscode.l10n.t("You've made the most of Copilot. Sign in to keep going.");
162
notification.actions = [
163
{ label: vscode.l10n.t('View Usage'), commandId: 'workbench.action.chat.openCopilotStatus' },
164
{ label: vscode.l10n.t('Sign In'), commandId: 'workbench.action.chat.triggerSetup' },
165
];
166
} else {
167
notification.message = vscode.l10n.t('Monthly Limit Reached');
168
notification.description = vscode.l10n.t("You've made the most of Copilot Free. Upgrade to keep going.");
169
notification.actions = [
170
{ label: vscode.l10n.t('View Usage'), commandId: 'workbench.action.chat.openCopilotStatus' },
171
{ label: vscode.l10n.t('Upgrade'), commandId: 'workbench.action.chat.upgradePlan' },
172
];
173
}
174
175
notification.show();
176
}
177
178
// --- Quota approaching --------------------------------------------------
179
180
private _showQuotaApproachingWarning(warning: IQuotaWarning): void {
181
const notification = this._ensureNotification();
182
this._showingExhausted = false;
183
184
notification.severity = vscode.ChatInputNotificationSeverity.Info;
185
notification.dismissible = true;
186
notification.autoDismissOnMessage = true;
187
notification.message = vscode.l10n.t('Monthly Limit at {0}%', warning.percentUsed);
188
notification.description = vscode.l10n.t("You're getting the most out of Copilot \u2014 upgrade to keep going.");
189
notification.actions = [
190
{ label: vscode.l10n.t('View Usage'), commandId: 'workbench.action.chat.openCopilotStatus' },
191
{ label: vscode.l10n.t('Upgrade'), commandId: 'workbench.action.chat.upgradePlan' },
192
];
193
194
notification.show();
195
}
196
197
// --- Rate limit warning -------------------------------------------------
198
199
private _showRateLimitWarning(warning: IRateLimitWarning): void {
200
const notification = this._ensureNotification();
201
this._showingExhausted = false;
202
203
const dateStr = this._formatResetDate(warning.resetDate);
204
notification.severity = vscode.ChatInputNotificationSeverity.Info;
205
notification.dismissible = true;
206
notification.autoDismissOnMessage = true;
207
208
notification.message = warning.type === 'session'
209
? vscode.l10n.t("You've used {0}% of your session rate limit.", warning.percentUsed)
210
: vscode.l10n.t("You've used {0}% of your weekly rate limit.", warning.percentUsed);
211
notification.description = vscode.l10n.t('Resets on {0}.', dateStr);
212
notification.actions = [];
213
214
notification.show();
215
}
216
217
// --- Helpers ------------------------------------------------------------
218
219
private _formatResetDate(resetDate: Date): string {
220
const now = new Date();
221
const includeYear = resetDate.getFullYear() !== now.getFullYear();
222
return new Intl.DateTimeFormat(undefined, includeYear
223
? { month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' }
224
: { month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit' }
225
).format(resetDate);
226
}
227
228
private _ensureNotification(): vscode.ChatInputNotification {
229
if (!this._notification) {
230
this._notification = vscode.chat.createInputNotification(QUOTA_NOTIFICATION_ID);
231
this._register({ dispose: () => this._notification?.dispose() });
232
}
233
return this._notification;
234
}
235
236
private _hideNotification(): void {
237
if (this._notification) {
238
this._notification.hide();
239
}
240
}
241
}
242
243