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