Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts
4780 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 { $, append, EventType, addDisposableListener, EventHelper, disposableWindowInterval, getWindow } from '../../../../../base/browser/dom.js';
7
import { Gesture, EventType as TouchEventType } from '../../../../../base/browser/touch.js';
8
import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js';
9
import { Button } from '../../../../../base/browser/ui/button/button.js';
10
import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js';
11
import { Checkbox } from '../../../../../base/browser/ui/toggle/toggle.js';
12
import { IAction, toAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../../base/common/actions.js';
13
import { cancelOnDispose } from '../../../../../base/common/cancellation.js';
14
import { Codicon } from '../../../../../base/common/codicons.js';
15
import { safeIntl } from '../../../../../base/common/date.js';
16
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
17
import { MutableDisposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
18
import { parseLinkedText } from '../../../../../base/common/linkedText.js';
19
import { language } from '../../../../../base/common/platform.js';
20
import { ThemeIcon } from '../../../../../base/common/themables.js';
21
import { isObject } from '../../../../../base/common/types.js';
22
import { URI } from '../../../../../base/common/uri.js';
23
import { IInlineCompletionsService } from '../../../../../editor/browser/services/inlineCompletionsService.js';
24
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
25
import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js';
26
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
27
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
28
import * as languages from '../../../../../editor/common/languages.js';
29
import { localize } from '../../../../../nls.js';
30
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
31
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
32
import { IHoverService, nativeHoverDelegate } from '../../../../../platform/hover/browser/hover.js';
33
import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';
34
import { Link } from '../../../../../platform/opener/browser/link.js';
35
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
36
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
37
import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
38
import { DomWidget } from '../../../../../platform/domWidget/browser/domWidget.js';
39
import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js';
40
import { IChatEntitlementService, ChatEntitlementService, ChatEntitlement, IQuotaSnapshot } from '../../../../services/chat/common/chatEntitlementService.js';
41
import { IEditorService } from '../../../../services/editor/common/editorService.js';
42
import { IChatSessionsService } from '../../common/chatSessionsService.js';
43
import { isNewUser, isCompletionsEnabled } from './chatStatus.js';
44
import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js';
45
import product from '../../../../../platform/product/common/product.js';
46
import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js';
47
import { Color } from '../../../../../base/common/color.js';
48
import { IViewsService } from '../../../../services/views/common/viewsService.js';
49
import { ChatViewId } from '../chat.js';
50
51
const defaultChat = product.defaultChatAgent;
52
53
interface ISettingsAccessor {
54
readSetting: () => boolean;
55
writeSetting: (value: boolean) => Promise<void>;
56
}
57
type ChatSettingChangedClassification = {
58
owner: 'bpasero';
59
comment: 'Provides insight into chat settings changed from the chat status entry.';
60
settingIdentifier: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the setting that changed.' };
61
settingMode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The optional editor language for which the setting changed.' };
62
settingEnablement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting got enabled or disabled.' };
63
};
64
type ChatSettingChangedEvent = {
65
settingIdentifier: string;
66
settingMode?: string;
67
settingEnablement: 'enabled' | 'disabled';
68
};
69
70
const gaugeForeground = registerColor('gauge.foreground', {
71
dark: inputValidationInfoBorder,
72
light: inputValidationInfoBorder,
73
hcDark: contrastBorder,
74
hcLight: contrastBorder
75
}, localize('gaugeForeground', "Gauge foreground color."));
76
77
registerColor('gauge.background', {
78
dark: transparent(gaugeForeground, 0.3),
79
light: transparent(gaugeForeground, 0.3),
80
hcDark: Color.white,
81
hcLight: Color.white
82
}, localize('gaugeBackground', "Gauge background color."));
83
84
registerColor('gauge.border', {
85
dark: null,
86
light: null,
87
hcDark: contrastBorder,
88
hcLight: contrastBorder
89
}, localize('gaugeBorder', "Gauge border color."));
90
91
const gaugeWarningForeground = registerColor('gauge.warningForeground', {
92
dark: inputValidationWarningBorder,
93
light: inputValidationWarningBorder,
94
hcDark: contrastBorder,
95
hcLight: contrastBorder
96
}, localize('gaugeWarningForeground', "Gauge warning foreground color."));
97
98
registerColor('gauge.warningBackground', {
99
dark: transparent(gaugeWarningForeground, 0.3),
100
light: transparent(gaugeWarningForeground, 0.3),
101
hcDark: Color.white,
102
hcLight: Color.white
103
}, localize('gaugeWarningBackground', "Gauge warning background color."));
104
105
const gaugeErrorForeground = registerColor('gauge.errorForeground', {
106
dark: inputValidationErrorBorder,
107
light: inputValidationErrorBorder,
108
hcDark: contrastBorder,
109
hcLight: contrastBorder
110
}, localize('gaugeErrorForeground', "Gauge error foreground color."));
111
112
registerColor('gauge.errorBackground', {
113
dark: transparent(gaugeErrorForeground, 0.3),
114
light: transparent(gaugeErrorForeground, 0.3),
115
hcDark: Color.white,
116
hcLight: Color.white
117
}, localize('gaugeErrorBackground', "Gauge error background color."));
118
119
export class ChatStatusDashboard extends DomWidget {
120
121
readonly element = $('div.chat-status-bar-entry-tooltip');
122
123
private readonly dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' });
124
private readonly dateTimeFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
125
private readonly quotaPercentageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 0 });
126
private readonly quotaOverageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 0 });
127
128
constructor(
129
@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,
130
@IChatStatusItemService private readonly chatStatusItemService: IChatStatusItemService,
131
@ICommandService private readonly commandService: ICommandService,
132
@IConfigurationService private readonly configurationService: IConfigurationService,
133
@IEditorService private readonly editorService: IEditorService,
134
@IHoverService private readonly hoverService: IHoverService,
135
@ILanguageService private readonly languageService: ILanguageService,
136
@IOpenerService private readonly openerService: IOpenerService,
137
@ITelemetryService private readonly telemetryService: ITelemetryService,
138
@ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService,
139
@IInlineCompletionsService private readonly inlineCompletionsService: IInlineCompletionsService,
140
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
141
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
142
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
143
@IQuickInputService private readonly quickInputService: IQuickInputService,
144
@IViewsService private readonly viewService: IViewsService,
145
) {
146
super();
147
148
this.render();
149
}
150
151
private render(): void {
152
const token = cancelOnDispose(this._store);
153
154
let needsSeparator = false;
155
const addSeparator = (label?: string, action?: IAction) => {
156
if (needsSeparator) {
157
this.element.appendChild($('hr'));
158
}
159
160
if (label || action) {
161
this.renderHeader(this.element, this._store, label ?? '', action);
162
}
163
164
needsSeparator = true;
165
};
166
167
// Quota Indicator
168
const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota, resetDate, resetDateHasTime } = this.chatEntitlementService.quotas;
169
if (chatQuota || completionsQuota || premiumChatQuota) {
170
addSeparator(localize('usageTitle', "Copilot Usage"), toAction({
171
id: 'workbench.action.manageCopilot',
172
label: localize('quotaLabel', "Manage Chat"),
173
tooltip: localize('quotaTooltip', "Manage Chat"),
174
class: ThemeIcon.asClassName(Codicon.settings),
175
run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageSettingsUrl))),
176
}));
177
178
const completionsQuotaIndicator = completionsQuota && (completionsQuota.total > 0 || completionsQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, completionsQuota, localize('completionsLabel', "Inline Suggestions"), false) : undefined;
179
const chatQuotaIndicator = chatQuota && (chatQuota.total > 0 || chatQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, chatQuota, localize('chatsLabel', "Chat messages"), false) : undefined;
180
const premiumChatQuotaIndicator = premiumChatQuota && (premiumChatQuota.total > 0 || premiumChatQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, premiumChatQuota, localize('premiumChatsLabel', "Premium requests"), true) : undefined;
181
182
if (resetDate) {
183
this.element.appendChild($('div.description', undefined, localize('limitQuota', "Allowance resets {0}.", resetDateHasTime ? this.dateTimeFormatter.value.format(new Date(resetDate)) : this.dateFormatter.value.format(new Date(resetDate)))));
184
}
185
186
if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && (Number(chatQuota?.percentRemaining) <= 25 || Number(completionsQuota?.percentRemaining) <= 25)) {
187
const upgradeProButton = this._store.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: this.canUseChat() /* use secondary color when chat can still be used */ }));
188
upgradeProButton.label = localize('upgradeToCopilotPro', "Upgrade to GitHub Copilot Pro");
189
this._store.add(upgradeProButton.onDidClick(() => this.runCommandAndClose('workbench.action.chat.upgradePlan')));
190
}
191
192
(async () => {
193
await this.chatEntitlementService.update(token);
194
if (token.isCancellationRequested) {
195
return;
196
}
197
198
const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota } = this.chatEntitlementService.quotas;
199
if (completionsQuota) {
200
completionsQuotaIndicator?.(completionsQuota);
201
}
202
if (chatQuota) {
203
chatQuotaIndicator?.(chatQuota);
204
}
205
if (premiumChatQuota) {
206
premiumChatQuotaIndicator?.(premiumChatQuota);
207
}
208
})();
209
}
210
211
// Anonymous Indicator
212
else if (this.chatEntitlementService.anonymous && this.chatEntitlementService.sentiment.installed) {
213
addSeparator(localize('anonymousTitle', "Copilot Usage"));
214
215
this.createQuotaIndicator(this.element, this._store, localize('quotaLimited', "Limited"), localize('completionsLabel', "Inline Suggestions"), false);
216
this.createQuotaIndicator(this.element, this._store, localize('quotaLimited', "Limited"), localize('chatsLabel', "Chat messages"), false);
217
}
218
219
// Chat sessions
220
{
221
const inProgress = this.chatSessionsService.getInProgress();
222
if (inProgress.some(item => item.count > 0)) {
223
224
addSeparator(localize('chatAgentSessionsTitle', "Agent Sessions"), toAction({
225
id: 'workbench.view.chat.status.sessions',
226
label: localize('viewChatSessionsLabel', "View Agent Sessions"),
227
tooltip: localize('viewChatSessionsTooltip', "View Agent Sessions"),
228
class: ThemeIcon.asClassName(Codicon.eye),
229
run: () => {
230
this.viewService.openView(ChatViewId, true);
231
this.hoverService.hideHover(true);
232
}
233
}));
234
235
for (const { displayName, count } of inProgress) {
236
if (count > 0) {
237
const text = localize('inProgressChatSession', "$(loading~spin) {0} in progress", displayName);
238
const chatSessionsElement = this.element.appendChild($('div.description'));
239
const parts = renderLabelWithIcons(text);
240
chatSessionsElement.append(...parts);
241
}
242
}
243
}
244
}
245
246
// Contributions
247
{
248
for (const item of this.chatStatusItemService.getEntries()) {
249
addSeparator();
250
251
const itemDisposables = this._store.add(new MutableDisposable());
252
253
let rendered = this.renderContributedChatStatusItem(item);
254
itemDisposables.value = rendered.disposables;
255
this.element.appendChild(rendered.element);
256
257
this._store.add(this.chatStatusItemService.onDidChange(e => {
258
if (e.entry.id === item.id) {
259
const previousElement = rendered.element;
260
261
rendered = this.renderContributedChatStatusItem(e.entry);
262
itemDisposables.value = rendered.disposables;
263
264
previousElement.replaceWith(rendered.element);
265
}
266
}));
267
}
268
}
269
270
// Settings
271
{
272
const chatSentiment = this.chatEntitlementService.sentiment;
273
addSeparator(localize('inlineSuggestions', "Inline Suggestions"), chatSentiment.installed && !chatSentiment.disabled && !chatSentiment.untrusted ? toAction({
274
id: 'workbench.action.openChatSettings',
275
label: localize('settingsLabel', "Settings"),
276
tooltip: localize('settingsTooltip', "Open Settings"),
277
class: ThemeIcon.asClassName(Codicon.settingsGear),
278
run: () => this.runCommandAndClose(() => this.commandService.executeCommand('workbench.action.openSettings', { query: `@id:${defaultChat.completionsEnablementSetting} @id:${defaultChat.nextEditSuggestionsSetting}` })),
279
}) : undefined);
280
281
this.createSettings(this.element, this._store);
282
}
283
284
// Model Selection
285
{
286
const providers = this.languageFeaturesService.inlineCompletionsProvider.allNoModel();
287
const provider = providers.find(p => p.modelInfo && p.modelInfo.models.length > 0);
288
289
if (provider) {
290
const modelInfo = provider.modelInfo!;
291
const currentModel = modelInfo.models.find(m => m.id === modelInfo.currentModelId);
292
293
if (currentModel) {
294
const modelContainer = this.element.appendChild($('div.model-selection'));
295
296
modelContainer.appendChild($('span.model-text', undefined, localize('modelLabel', "Model: {0}", currentModel.name)));
297
298
const actionBar = modelContainer.appendChild($('div.model-action-bar'));
299
const toolbar = this._store.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate }));
300
toolbar.push([toAction({
301
id: 'workbench.action.selectInlineCompletionsModel',
302
label: localize('selectModel', "Select Model"),
303
tooltip: localize('selectModel', "Select Model"),
304
class: ThemeIcon.asClassName(Codicon.gear),
305
run: async () => {
306
await this.showModelPicker(provider);
307
}
308
})], { icon: true, label: false });
309
}
310
}
311
}
312
313
// Completions Snooze
314
if (this.canUseChat()) {
315
const snooze = append(this.element, $('div.snooze-completions'));
316
this.createCompletionsSnooze(snooze, localize('settings.snooze', "Snooze"), this._store);
317
}
318
319
// New to Chat / Signed out
320
{
321
const newUser = isNewUser(this.chatEntitlementService);
322
const anonymousUser = this.chatEntitlementService.anonymous;
323
const disabled = this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted;
324
const signedOut = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown;
325
if (newUser || signedOut || disabled) {
326
addSeparator();
327
328
let descriptionText: string | MarkdownString;
329
let descriptionClass = '.description';
330
if (newUser && anonymousUser) {
331
descriptionText = new MarkdownString(localize({ key: 'activeDescriptionAnonymous', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", defaultChat.provider.default.name, defaultChat.provider.default.name, defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl), { isTrusted: true });
332
descriptionClass = `${descriptionClass}.terms`;
333
} else if (newUser) {
334
descriptionText = localize('activateDescription', "Set up Copilot to use AI features.");
335
} else if (anonymousUser) {
336
descriptionText = localize('enableMoreDescription', "Sign in to enable more Copilot AI features.");
337
} else if (disabled) {
338
descriptionText = localize('enableDescription', "Enable Copilot to use AI features.");
339
} else {
340
descriptionText = localize('signInDescription', "Sign in to use Copilot AI features.");
341
}
342
343
let buttonLabel: string;
344
if (newUser) {
345
buttonLabel = localize('enableAIFeatures', "Use AI Features");
346
} else if (anonymousUser) {
347
buttonLabel = localize('enableMoreAIFeatures', "Enable more AI Features");
348
} else if (disabled) {
349
buttonLabel = localize('enableCopilotButton', "Enable AI Features");
350
} else {
351
buttonLabel = localize('signInToUseAIFeatures', "Sign in to use AI Features");
352
}
353
354
let commandId: string;
355
if (newUser && anonymousUser) {
356
commandId = 'workbench.action.chat.triggerSetupAnonymousWithoutDialog';
357
} else {
358
commandId = 'workbench.action.chat.triggerSetup';
359
}
360
361
if (typeof descriptionText === 'string') {
362
this.element.appendChild($(`div${descriptionClass}`, undefined, descriptionText));
363
} else {
364
this.element.appendChild($(`div${descriptionClass}`, undefined, this._store.add(this.markdownRendererService.render(descriptionText)).element));
365
}
366
367
const button = this._store.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate }));
368
button.label = buttonLabel;
369
this._store.add(button.onDidClick(() => this.runCommandAndClose(commandId)));
370
}
371
}
372
}
373
374
private canUseChat(): boolean {
375
if (!this.chatEntitlementService.sentiment.installed || this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) {
376
return false; // chat not installed or not enabled
377
}
378
379
if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown || this.chatEntitlementService.entitlement === ChatEntitlement.Available) {
380
return this.chatEntitlementService.anonymous; // signed out or not-yet-signed-up users can only use Chat if anonymous access is allowed
381
}
382
383
if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && this.chatEntitlementService.quotas.chat?.percentRemaining === 0 && this.chatEntitlementService.quotas.completions?.percentRemaining === 0) {
384
return false; // free user with no quota left
385
}
386
387
return true;
388
}
389
390
private renderHeader(container: HTMLElement, disposables: DisposableStore, label: string, action?: IAction): void {
391
const header = container.appendChild($('div.header', undefined, label ?? ''));
392
393
if (action) {
394
const toolbar = disposables.add(new ActionBar(header, { hoverDelegate: nativeHoverDelegate }));
395
toolbar.push([action], { icon: true, label: false });
396
}
397
}
398
399
private renderContributedChatStatusItem(item: ChatStatusEntry): { element: HTMLElement; disposables: DisposableStore } {
400
const disposables = new DisposableStore();
401
402
const itemElement = $('div.contribution');
403
404
const headerLabel = typeof item.label === 'string' ? item.label : item.label.label;
405
const headerLink = typeof item.label === 'string' ? undefined : item.label.link;
406
this.renderHeader(itemElement, disposables, headerLabel, headerLink ? toAction({
407
id: 'workbench.action.openChatStatusItemLink',
408
label: localize('learnMore', "Learn More"),
409
tooltip: localize('learnMore', "Learn More"),
410
class: ThemeIcon.asClassName(Codicon.linkExternal),
411
run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(headerLink))),
412
}) : undefined);
413
414
const itemBody = itemElement.appendChild($('div.body'));
415
416
const description = itemBody.appendChild($('span.description'));
417
this.renderTextPlus(description, item.description, disposables);
418
419
if (item.detail) {
420
const detail = itemBody.appendChild($('div.detail-item'));
421
this.renderTextPlus(detail, item.detail, disposables);
422
}
423
424
return { element: itemElement, disposables };
425
}
426
427
private renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void {
428
for (const node of parseLinkedText(text).nodes) {
429
if (typeof node === 'string') {
430
const parts = renderLabelWithIcons(node);
431
target.append(...parts);
432
} else {
433
store.add(new Link(target, node, undefined, this.hoverService, this.openerService));
434
}
435
}
436
}
437
438
private runCommandAndClose(commandOrFn: string | Function, ...args: unknown[]): void {
439
if (typeof commandOrFn === 'function') {
440
commandOrFn(...args);
441
} else {
442
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: commandOrFn, from: 'chat-status' });
443
this.commandService.executeCommand(commandOrFn, ...args);
444
}
445
446
this.hoverService.hideHover(true);
447
}
448
449
private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot | string, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot | string) => void {
450
const quotaValue = $('span.quota-value');
451
const quotaBit = $('div.quota-bit');
452
const overageLabel = $('span.overage-label');
453
454
const quotaIndicator = container.appendChild($('div.quota-indicator', undefined,
455
$('div.quota-label', undefined,
456
$('span', undefined, label),
457
quotaValue
458
),
459
$('div.quota-bar', undefined,
460
quotaBit
461
),
462
$('div.description', undefined,
463
overageLabel
464
)
465
));
466
467
if (supportsOverage && (this.chatEntitlementService.entitlement === ChatEntitlement.Pro || this.chatEntitlementService.entitlement === ChatEntitlement.ProPlus)) {
468
const manageOverageButton = disposables.add(new Button(quotaIndicator, { ...defaultButtonStyles, secondary: true, hoverDelegate: nativeHoverDelegate }));
469
manageOverageButton.label = localize('enableAdditionalUsage', "Manage paid premium requests");
470
disposables.add(manageOverageButton.onDidClick(() => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageOverageUrl)))));
471
}
472
473
const update = (quota: IQuotaSnapshot | string) => {
474
quotaIndicator.classList.remove('error');
475
quotaIndicator.classList.remove('warning');
476
477
let usedPercentage: number;
478
if (typeof quota === 'string' || quota.unlimited) {
479
usedPercentage = 0;
480
} else {
481
usedPercentage = Math.max(0, 100 - quota.percentRemaining);
482
}
483
484
if (typeof quota === 'string') {
485
quotaValue.textContent = quota;
486
} else if (quota.unlimited) {
487
quotaValue.textContent = localize('quotaUnlimited', "Included");
488
} else if (quota.overageCount) {
489
quotaValue.textContent = localize('quotaDisplayWithOverage', "+{0} requests", this.quotaOverageFormatter.value.format(quota.overageCount));
490
} else {
491
quotaValue.textContent = localize('quotaDisplay', "{0}%", this.quotaPercentageFormatter.value.format(usedPercentage));
492
}
493
494
quotaBit.style.width = `${usedPercentage}%`;
495
496
if (usedPercentage >= 90) {
497
quotaIndicator.classList.add('error');
498
} else if (usedPercentage >= 75) {
499
quotaIndicator.classList.add('warning');
500
}
501
502
if (supportsOverage) {
503
if (typeof quota !== 'string' && quota?.overageEnabled) {
504
overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests enabled.");
505
} else {
506
overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled.");
507
}
508
} else {
509
overageLabel.textContent = '';
510
}
511
};
512
513
update(quota);
514
515
return update;
516
}
517
518
private createSettings(container: HTMLElement, disposables: DisposableStore): HTMLElement {
519
const modeId = this.editorService.activeTextEditorLanguageId;
520
const settings = container.appendChild($('div.settings'));
521
522
// --- Inline Suggestions
523
{
524
const globalSetting = append(settings, $('div.setting'));
525
this.createInlineSuggestionsSetting(globalSetting, localize('settings.codeCompletions.allFiles', "All files"), '*', disposables);
526
527
if (modeId) {
528
const languageSetting = append(settings, $('div.setting'));
529
this.createInlineSuggestionsSetting(languageSetting, localize('settings.codeCompletions.language', "{0}", this.languageService.getLanguageName(modeId) ?? modeId), modeId, disposables);
530
}
531
}
532
533
// --- Next edit suggestions
534
{
535
const setting = append(settings, $('div.setting'));
536
this.createNextEditSuggestionsSetting(setting, localize('settings.nextEditSuggestions', "Next edit suggestions"), this.getCompletionsSettingAccessor(modeId), disposables);
537
}
538
539
return settings;
540
}
541
542
private createSetting(container: HTMLElement, settingIdsToReEvaluate: string[], label: string, accessor: ISettingsAccessor, disposables: DisposableStore): Checkbox {
543
const checkbox = disposables.add(new Checkbox(label, Boolean(accessor.readSetting()), { ...defaultCheckboxStyles }));
544
container.appendChild(checkbox.domNode);
545
546
const settingLabel = append(container, $('span.setting-label', undefined, label));
547
disposables.add(Gesture.addTarget(settingLabel));
548
[EventType.CLICK, TouchEventType.Tap].forEach(eventType => {
549
disposables.add(addDisposableListener(settingLabel, eventType, e => {
550
if (checkbox?.enabled) {
551
EventHelper.stop(e, true);
552
553
checkbox.checked = !checkbox.checked;
554
accessor.writeSetting(checkbox.checked);
555
checkbox.focus();
556
}
557
}));
558
});
559
560
disposables.add(checkbox.onChange(() => {
561
accessor.writeSetting(checkbox.checked);
562
}));
563
564
disposables.add(this.configurationService.onDidChangeConfiguration(e => {
565
if (settingIdsToReEvaluate.some(id => e.affectsConfiguration(id))) {
566
checkbox.checked = Boolean(accessor.readSetting());
567
}
568
}));
569
570
if (!this.canUseChat()) {
571
container.classList.add('disabled');
572
checkbox.disable();
573
checkbox.checked = false;
574
}
575
576
return checkbox;
577
}
578
579
private createInlineSuggestionsSetting(container: HTMLElement, label: string, modeId: string | undefined, disposables: DisposableStore): void {
580
this.createSetting(container, [defaultChat.completionsEnablementSetting], label, this.getCompletionsSettingAccessor(modeId), disposables);
581
}
582
583
private getCompletionsSettingAccessor(modeId = '*'): ISettingsAccessor {
584
const settingId = defaultChat.completionsEnablementSetting;
585
586
return {
587
readSetting: () => isCompletionsEnabled(this.configurationService, modeId),
588
writeSetting: (value: boolean) => {
589
this.telemetryService.publicLog2<ChatSettingChangedEvent, ChatSettingChangedClassification>('chatStatus.settingChanged', {
590
settingIdentifier: settingId,
591
settingMode: modeId,
592
settingEnablement: value ? 'enabled' : 'disabled'
593
});
594
595
let result = this.configurationService.getValue<Record<string, boolean>>(settingId);
596
if (!isObject(result)) {
597
result = Object.create(null);
598
}
599
600
return this.configurationService.updateValue(settingId, { ...result, [modeId]: value });
601
}
602
};
603
}
604
605
private createNextEditSuggestionsSetting(container: HTMLElement, label: string, completionsSettingAccessor: ISettingsAccessor, disposables: DisposableStore): void {
606
const nesSettingId = defaultChat.nextEditSuggestionsSetting;
607
const completionsSettingId = defaultChat.completionsEnablementSetting;
608
const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
609
610
const checkbox = this.createSetting(container, [nesSettingId, completionsSettingId], label, {
611
readSetting: () => completionsSettingAccessor.readSetting() && this.textResourceConfigurationService.getValue<boolean>(resource, nesSettingId),
612
writeSetting: (value: boolean) => {
613
this.telemetryService.publicLog2<ChatSettingChangedEvent, ChatSettingChangedClassification>('chatStatus.settingChanged', {
614
settingIdentifier: nesSettingId,
615
settingEnablement: value ? 'enabled' : 'disabled'
616
});
617
618
return this.textResourceConfigurationService.updateValue(resource, nesSettingId, value);
619
}
620
}, disposables);
621
622
// enablement of NES depends on completions setting
623
// so we have to update our checkbox state accordingly
624
if (!completionsSettingAccessor.readSetting()) {
625
container.classList.add('disabled');
626
checkbox.disable();
627
}
628
629
disposables.add(this.configurationService.onDidChangeConfiguration(e => {
630
if (e.affectsConfiguration(completionsSettingId)) {
631
if (completionsSettingAccessor.readSetting() && this.canUseChat()) {
632
checkbox.enable();
633
container.classList.remove('disabled');
634
} else {
635
checkbox.disable();
636
container.classList.add('disabled');
637
}
638
}
639
}));
640
}
641
642
private createCompletionsSnooze(container: HTMLElement, label: string, disposables: DisposableStore): void {
643
const isEnabled = () => {
644
const completionsEnabled = isCompletionsEnabled(this.configurationService);
645
const completionsEnabledActiveLanguage = isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId);
646
return completionsEnabled || completionsEnabledActiveLanguage;
647
};
648
649
const button = disposables.add(new Button(container, { disabled: !isEnabled(), ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: true }));
650
651
const timerDisplay = container.appendChild($('span.snooze-label'));
652
653
const actionBar = container.appendChild($('div.snooze-action-bar'));
654
const toolbar = disposables.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate }));
655
const cancelAction = toAction({
656
id: 'workbench.action.cancelSnoozeStatusBarLink',
657
label: localize('cancelSnooze', "Cancel Snooze"),
658
run: () => this.inlineCompletionsService.cancelSnooze(),
659
class: ThemeIcon.asClassName(Codicon.stopCircle)
660
});
661
662
const update = (isEnabled: boolean) => {
663
container.classList.toggle('disabled', !isEnabled);
664
toolbar.clear();
665
666
const timeLeftMs = this.inlineCompletionsService.snoozeTimeLeft;
667
if (!isEnabled || timeLeftMs <= 0) {
668
timerDisplay.textContent = localize('completions.snooze5minutesTitle', "Hide suggestions for 5 min");
669
timerDisplay.title = '';
670
button.label = label;
671
button.setTitle(localize('completions.snooze5minutes', "Hide inline suggestions for 5 min"));
672
return true;
673
}
674
675
const timeLeftSeconds = Math.ceil(timeLeftMs / 1000);
676
const minutes = Math.floor(timeLeftSeconds / 60);
677
const seconds = timeLeftSeconds % 60;
678
679
timerDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds} ${localize('completions.remainingTime', "remaining")}`;
680
timerDisplay.title = localize('completions.snoozeTimeDescription', "Inline suggestions are hidden for the remaining duration");
681
button.label = localize('completions.plus5min', "+5 min");
682
button.setTitle(localize('completions.snoozeAdditional5minutes', "Snooze additional 5 min"));
683
toolbar.push([cancelAction], { icon: true, label: false });
684
685
return false;
686
};
687
688
// Update every second if there's time remaining
689
const timerDisposables = disposables.add(new DisposableStore());
690
function updateIntervalTimer() {
691
timerDisposables.clear();
692
const enabled = isEnabled();
693
694
if (update(enabled)) {
695
return;
696
}
697
698
timerDisposables.add(disposableWindowInterval(
699
getWindow(container),
700
() => update(enabled),
701
1000
702
));
703
}
704
updateIntervalTimer();
705
706
disposables.add(button.onDidClick(() => {
707
this.inlineCompletionsService.snooze();
708
update(isEnabled());
709
}));
710
711
disposables.add(this.configurationService.onDidChangeConfiguration(e => {
712
if (e.affectsConfiguration(defaultChat.completionsEnablementSetting)) {
713
button.enabled = isEnabled();
714
}
715
updateIntervalTimer();
716
}));
717
718
disposables.add(this.inlineCompletionsService.onDidChangeIsSnoozing(e => {
719
updateIntervalTimer();
720
}));
721
}
722
723
private async showModelPicker(provider: languages.InlineCompletionsProvider): Promise<void> {
724
if (!provider.modelInfo || !provider.setModelId) {
725
return;
726
}
727
728
const modelInfo = provider.modelInfo;
729
const items: IQuickPickItem[] = modelInfo.models.map(model => ({
730
id: model.id,
731
label: model.name,
732
description: model.id === modelInfo.currentModelId ? localize('currentModel.description', "Currently selected") : undefined,
733
picked: model.id === modelInfo.currentModelId
734
}));
735
736
const selected = await this.quickInputService.pick(items, {
737
placeHolder: localize('selectModelFor', "Select a model for {0}", provider.displayName || 'inline completions'),
738
canPickMany: false
739
});
740
741
if (selected && selected.id && selected.id !== modelInfo.currentModelId) {
742
await provider.setModelId(selected.id);
743
}
744
745
this.hoverService.hideHover(true);
746
}
747
}
748
749