Path: blob/main/src/vs/workbench/contrib/chat/browser/chatStatus.ts
3296 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 './media/chatStatus.css';6import { safeIntl } from '../../../../base/common/date.js';7import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';8import { language } from '../../../../base/common/platform.js';9import { localize } from '../../../../nls.js';10import { IWorkbenchContribution } from '../../../common/contributions.js';11import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../services/statusbar/browser/statusbar.js';12import { $, addDisposableListener, append, clearNode, disposableWindowInterval, EventHelper, EventType, getWindow } from '../../../../base/browser/dom.js';13import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService, IQuotaSnapshot, isProUser } from '../common/chatEntitlementService.js';14import { CancellationToken } from '../../../../base/common/cancellation.js';15import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js';16import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js';17import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';18import { ICommandService } from '../../../../platform/commands/common/commands.js';19import { Lazy } from '../../../../base/common/lazy.js';20import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js';21import { IHoverService, nativeHoverDelegate } from '../../../../platform/hover/browser/hover.js';22import { Color } from '../../../../base/common/color.js';23import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js';24import { IEditorService } from '../../../services/editor/common/editorService.js';25import product from '../../../../platform/product/common/product.js';26import { isObject } from '../../../../base/common/types.js';27import { ILanguageService } from '../../../../editor/common/languages/language.js';28import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';29import { Button } from '../../../../base/browser/ui/button/button.js';30import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';31import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IAction, toAction } from '../../../../base/common/actions.js';32import { parseLinkedText } from '../../../../base/common/linkedText.js';33import { Link } from '../../../../platform/opener/browser/link.js';34import { IOpenerService } from '../../../../platform/opener/common/opener.js';35import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';36import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js';37import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';38import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js';39import { getCodeEditor } from '../../../../editor/browser/editorBrowser.js';40import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';41import { ThemeIcon } from '../../../../base/common/themables.js';42import { Codicon } from '../../../../base/common/codicons.js';43import { URI } from '../../../../base/common/uri.js';44import { IInlineCompletionsService } from '../../../../editor/browser/services/inlineCompletionsService.js';45import { IChatSessionsService } from '../common/chatSessionsService.js';4647const gaugeForeground = registerColor('gauge.foreground', {48dark: inputValidationInfoBorder,49light: inputValidationInfoBorder,50hcDark: contrastBorder,51hcLight: contrastBorder52}, localize('gaugeForeground', "Gauge foreground color."));5354registerColor('gauge.background', {55dark: transparent(gaugeForeground, 0.3),56light: transparent(gaugeForeground, 0.3),57hcDark: Color.white,58hcLight: Color.white59}, localize('gaugeBackground', "Gauge background color."));6061registerColor('gauge.border', {62dark: null,63light: null,64hcDark: contrastBorder,65hcLight: contrastBorder66}, localize('gaugeBorder', "Gauge border color."));6768const gaugeWarningForeground = registerColor('gauge.warningForeground', {69dark: inputValidationWarningBorder,70light: inputValidationWarningBorder,71hcDark: contrastBorder,72hcLight: contrastBorder73}, localize('gaugeWarningForeground', "Gauge warning foreground color."));7475registerColor('gauge.warningBackground', {76dark: transparent(gaugeWarningForeground, 0.3),77light: transparent(gaugeWarningForeground, 0.3),78hcDark: Color.white,79hcLight: Color.white80}, localize('gaugeWarningBackground', "Gauge warning background color."));8182const gaugeErrorForeground = registerColor('gauge.errorForeground', {83dark: inputValidationErrorBorder,84light: inputValidationErrorBorder,85hcDark: contrastBorder,86hcLight: contrastBorder87}, localize('gaugeErrorForeground', "Gauge error foreground color."));8889registerColor('gauge.errorBackground', {90dark: transparent(gaugeErrorForeground, 0.3),91light: transparent(gaugeErrorForeground, 0.3),92hcDark: Color.white,93hcLight: Color.white94}, localize('gaugeErrorBackground', "Gauge error background color."));9596//#endregion9798const defaultChat = {99extensionId: product.defaultChatAgent?.extensionId ?? '',100completionsEnablementSetting: product.defaultChatAgent?.completionsEnablementSetting ?? '',101nextEditSuggestionsSetting: product.defaultChatAgent?.nextEditSuggestionsSetting ?? '',102manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',103manageOverageUrl: product.defaultChatAgent?.manageOverageUrl ?? '',104};105106export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution {107108static readonly ID = 'workbench.contrib.chatStatusBarEntry';109110private entry: IStatusbarEntryAccessor | undefined = undefined;111112private dashboard = new Lazy<ChatStatusDashboard>(() => this.instantiationService.createInstance(ChatStatusDashboard));113114private readonly activeCodeEditorListener = this._register(new MutableDisposable());115116constructor(117@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,118@IInstantiationService private readonly instantiationService: IInstantiationService,119@IStatusbarService private readonly statusbarService: IStatusbarService,120@IEditorService private readonly editorService: IEditorService,121@IConfigurationService private readonly configurationService: IConfigurationService,122@IInlineCompletionsService private readonly completionsService: IInlineCompletionsService,123@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,124) {125super();126127this.update();128this.registerListeners();129}130131private update(): void {132const sentiment = this.chatEntitlementService.sentiment;133if (!sentiment.hidden) {134const props = this.getEntryProps();135if (this.entry) {136this.entry.update(props);137} else {138this.entry = this.statusbarService.addEntry(props, 'chat.statusBarEntry', StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT });139}140} else {141this.entry?.dispose();142this.entry = undefined;143}144}145146private registerListeners(): void {147this._register(this.chatEntitlementService.onDidChangeQuotaExceeded(() => this.update()));148this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.update()));149this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.update()));150this._register(this.completionsService.onDidChangeIsSnoozing(() => this.update()));151this._register(this.chatSessionsService.onDidChangeInProgress(() => this.update()));152153this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange()));154155this._register(this.configurationService.onDidChangeConfiguration(e => {156if (e.affectsConfiguration(defaultChat.completionsEnablementSetting)) {157this.update();158}159}));160}161162private onDidActiveEditorChange(): void {163this.update();164165this.activeCodeEditorListener.clear();166167// Listen to language changes in the active code editor168const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl);169if (activeCodeEditor) {170this.activeCodeEditorListener.value = activeCodeEditor.onDidChangeModelLanguage(() => {171this.update();172});173}174}175176private getEntryProps(): IStatusbarEntry {177let text = '$(copilot)';178let ariaLabel = localize('chatStatus', "Copilot Status");179let kind: StatusbarEntryKind | undefined;180181// Check if there are any chat sessions in progress182const inProgress = this.chatSessionsService.getInProgress();183const hasInProgressSessions = inProgress.some(item => item.count > 0);184185if (isNewUser(this.chatEntitlementService)) {186const entitlement = this.chatEntitlementService.entitlement;187188// Finish Setup189if (190this.chatEntitlementService.sentiment.later || // user skipped setup191entitlement === ChatEntitlement.Available || // user is entitled192isProUser(entitlement) || // user is already pro193entitlement === ChatEntitlement.Free // user is already free194) {195const finishSetup = localize('copilotLaterStatus', "Finish Setup");196197text = `$(copilot) ${finishSetup}`;198ariaLabel = finishSetup;199kind = 'prominent';200}201} else {202const chatQuotaExceeded = this.chatEntitlementService.quotas.chat?.percentRemaining === 0;203const completionsQuotaExceeded = this.chatEntitlementService.quotas.completions?.percentRemaining === 0;204205// Disabled206if (this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) {207text = `$(copilot-unavailable)`;208ariaLabel = localize('copilotDisabledStatus', "Copilot Disabled");209}210211// Signed out212else if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) {213const signedOutWarning = localize('notSignedIntoCopilot', "Signed out");214215text = `$(copilot-not-connected) ${signedOutWarning}`;216ariaLabel = signedOutWarning;217kind = 'prominent';218}219220// Free Quota Exceeded221else if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && (chatQuotaExceeded || completionsQuotaExceeded)) {222let quotaWarning: string;223if (chatQuotaExceeded && !completionsQuotaExceeded) {224quotaWarning = localize('chatQuotaExceededStatus', "Chat quota reached");225} else if (completionsQuotaExceeded && !chatQuotaExceeded) {226quotaWarning = localize('completionsQuotaExceededStatus', "Completions quota reached");227} else {228quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Quota reached");229}230231text = `$(copilot-warning) ${quotaWarning}`;232ariaLabel = quotaWarning;233kind = 'prominent';234}235236// Completions Disabled237else if (this.editorService.activeTextEditorLanguageId && !isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId)) {238text = `$(copilot-unavailable)`;239ariaLabel = localize('completionsDisabledStatus', "Code completions disabled");240}241242// Completions Snoozed243else if (this.completionsService.isSnoozing()) {244text = `$(copilot-snooze)`;245ariaLabel = localize('completionsSnoozedStatus', "Code completions snoozed");246}247}248249// Show progress indicator when chat sessions are in progress250if (hasInProgressSessions) {251text = `$(loading~spin)\u00A0${text}`;252// Update aria label to include progress information253const sessionCount = inProgress.reduce((total, item) => total + item.count, 0);254ariaLabel = `${ariaLabel}, ${sessionCount} chat session${sessionCount === 1 ? '' : 's'} in progress`;255}256257const baseResult = {258name: localize('chatStatus', "Copilot Status"),259text,260ariaLabel,261command: ShowTooltipCommand,262showInAllWindows: true,263kind,264tooltip: { element: (token: CancellationToken) => this.dashboard.value.show(token) }265};266267return baseResult;268}269270override dispose(): void {271super.dispose();272273this.entry?.dispose();274this.entry = undefined;275}276}277278function isNewUser(chatEntitlementService: IChatEntitlementService): boolean {279return !chatEntitlementService.sentiment.installed || // copilot not installed280chatEntitlementService.entitlement === ChatEntitlement.Available; // not yet signed up to copilot281}282283function canUseCopilot(chatEntitlementService: IChatEntitlementService): boolean {284const newUser = isNewUser(chatEntitlementService);285const disabled = chatEntitlementService.sentiment.disabled || chatEntitlementService.sentiment.untrusted;286const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown;287const free = chatEntitlementService.entitlement === ChatEntitlement.Free;288const allFreeQuotaReached = free && chatEntitlementService.quotas.chat?.percentRemaining === 0 && chatEntitlementService.quotas.completions?.percentRemaining === 0;289290return !newUser && !signedOut && !allFreeQuotaReached && !disabled;291}292293function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean {294const result = configurationService.getValue<Record<string, boolean>>(defaultChat.completionsEnablementSetting);295if (!isObject(result)) {296return false;297}298299if (typeof result[modeId] !== 'undefined') {300return Boolean(result[modeId]); // go with setting if explicitly defined301}302303return Boolean(result['*']); // fallback to global setting otherwise304}305306interface ISettingsAccessor {307readSetting: () => boolean;308writeSetting: (value: boolean) => Promise<void>;309}310311type ChatSettingChangedClassification = {312owner: 'bpasero';313comment: 'Provides insight into chat settings changed from the chat status entry.';314settingIdentifier: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the setting that changed.' };315settingMode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The optional editor language for which the setting changed.' };316settingEnablement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting got enabled or disabled.' };317};318type ChatSettingChangedEvent = {319settingIdentifier: string;320settingMode?: string;321settingEnablement: 'enabled' | 'disabled';322};323324class ChatStatusDashboard extends Disposable {325326private readonly element = $('div.chat-status-bar-entry-tooltip');327328private readonly dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' });329private readonly dateTimeFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });330private readonly quotaPercentageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 0 });331private readonly quotaOverageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 0 });332333private readonly entryDisposables = this._register(new MutableDisposable());334335constructor(336@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,337@IChatStatusItemService private readonly chatStatusItemService: IChatStatusItemService,338@ICommandService private readonly commandService: ICommandService,339@IConfigurationService private readonly configurationService: IConfigurationService,340@IEditorService private readonly editorService: IEditorService,341@IHoverService private readonly hoverService: IHoverService,342@ILanguageService private readonly languageService: ILanguageService,343@IOpenerService private readonly openerService: IOpenerService,344@ITelemetryService private readonly telemetryService: ITelemetryService,345@ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService,346@IInlineCompletionsService private readonly inlineCompletionsService: IInlineCompletionsService,347@IChatSessionsService private readonly chatSessionsService: IChatSessionsService348) {349super();350}351352show(token: CancellationToken): HTMLElement {353clearNode(this.element);354355const disposables = this.entryDisposables.value = new DisposableStore();356disposables.add(token.onCancellationRequested(() => disposables.dispose()));357358let needsSeparator = false;359const addSeparator = (label?: string, action?: IAction) => {360if (needsSeparator) {361this.element.appendChild($('hr'));362}363364if (label || action) {365this.renderHeader(this.element, disposables, label ?? '', action);366}367368needsSeparator = true;369};370371// Quota Indicator372const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota, resetDate, resetDateHasTime } = this.chatEntitlementService.quotas;373if (chatQuota || completionsQuota || premiumChatQuota) {374375addSeparator(localize('usageTitle', "Copilot Usage"), toAction({376id: 'workbench.action.manageCopilot',377label: localize('quotaLabel', "Manage Chat"),378tooltip: localize('quotaTooltip', "Manage Chat"),379class: ThemeIcon.asClassName(Codicon.settings),380run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageSettingsUrl))),381}));382383const completionsQuotaIndicator = completionsQuota && (completionsQuota.total > 0 || completionsQuota.unlimited) ? this.createQuotaIndicator(this.element, disposables, completionsQuota, localize('completionsLabel', "Code completions"), false) : undefined;384const chatQuotaIndicator = chatQuota && (chatQuota.total > 0 || chatQuota.unlimited) ? this.createQuotaIndicator(this.element, disposables, chatQuota, localize('chatsLabel', "Chat messages"), false) : undefined;385const premiumChatQuotaIndicator = premiumChatQuota && (premiumChatQuota.total > 0 || premiumChatQuota.unlimited) ? this.createQuotaIndicator(this.element, disposables, premiumChatQuota, localize('premiumChatsLabel', "Premium requests"), true) : undefined;386387if (resetDate) {388this.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)))));389}390391if (this.chatEntitlementService.entitlement === ChatEntitlement.Free && (Number(chatQuota?.percentRemaining) <= 25 || Number(completionsQuota?.percentRemaining) <= 25)) {392const upgradeProButton = disposables.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: canUseCopilot(this.chatEntitlementService) /* use secondary color when copilot can still be used */ }));393upgradeProButton.label = localize('upgradeToCopilotPro', "Upgrade to GitHub Copilot Pro");394disposables.add(upgradeProButton.onDidClick(() => this.runCommandAndClose('workbench.action.chat.upgradePlan')));395}396397(async () => {398await this.chatEntitlementService.update(token);399if (token.isCancellationRequested) {400return;401}402403const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota } = this.chatEntitlementService.quotas;404if (completionsQuota) {405completionsQuotaIndicator?.(completionsQuota);406}407if (chatQuota) {408chatQuotaIndicator?.(chatQuota);409}410if (premiumChatQuota) {411premiumChatQuotaIndicator?.(premiumChatQuota);412}413})();414}415416// Chat sessions417{418let chatSessionsElement: HTMLElement | undefined;419const updateStatus = () => {420const inProgress = this.chatSessionsService.getInProgress();421if (inProgress.some(item => item.count > 0)) {422addSeparator(localize('chatSessionsTitle', "Chat Sessions"), toAction({423id: 'workbench.view.chat.status.sessions',424label: localize('viewChatSessionsLabel', "View Chat Sessions"),425tooltip: localize('viewChatSessionsTooltip', "View Chat Sessions"),426class: ThemeIcon.asClassName(Codicon.eye),427run: () => this.runCommandAndClose('workbench.view.chat.sessions'),428}));429430for (const { displayName, count } of inProgress) {431if (count > 0) {432let lowerCaseName = displayName.toLocaleLowerCase();433// Very specific case for providers that end in session/sessions to ensure we pluralize correctly434if (lowerCaseName.endsWith('session') || lowerCaseName.endsWith('sessions')) {435lowerCaseName = lowerCaseName.replace(/session$|sessions$/g, count > 1 ? 'sessions' : 'session');436}437const text = localize('inProgressChatSession', "$(loading~spin) {0} {1} in progress", count, lowerCaseName);438chatSessionsElement = this.element.appendChild($('div.description'));439const parts = renderLabelWithIcons(text);440chatSessionsElement.append(...parts);441}442}443}444else {445chatSessionsElement?.remove();446}447};448updateStatus();449disposables.add(this.chatSessionsService.onDidChangeInProgress(updateStatus));450}451452// Contributions453{454for (const item of this.chatStatusItemService.getEntries()) {455addSeparator();456457const itemDisposables = disposables.add(new MutableDisposable());458459let rendered = this.renderContributedChatStatusItem(item);460itemDisposables.value = rendered.disposables;461this.element.appendChild(rendered.element);462463disposables.add(this.chatStatusItemService.onDidChange(e => {464if (e.entry.id === item.id) {465const previousElement = rendered.element;466467rendered = this.renderContributedChatStatusItem(e.entry);468itemDisposables.value = rendered.disposables;469470previousElement.replaceWith(rendered.element);471}472}));473}474}475476// Settings477{478const chatSentiment = this.chatEntitlementService.sentiment;479addSeparator(localize('codeCompletions', "Code Completions"), chatSentiment.installed && !chatSentiment.disabled && !chatSentiment.untrusted ? toAction({480id: 'workbench.action.openChatSettings',481label: localize('settingsLabel', "Settings"),482tooltip: localize('settingsTooltip', "Open Settings"),483class: ThemeIcon.asClassName(Codicon.settingsGear),484run: () => this.runCommandAndClose(() => this.commandService.executeCommand('workbench.action.openSettings', { query: `@id:${defaultChat.completionsEnablementSetting} @id:${defaultChat.nextEditSuggestionsSetting}` })),485}) : undefined);486487this.createSettings(this.element, disposables);488}489490// Completions Snooze491if (canUseCopilot(this.chatEntitlementService)) {492const snooze = append(this.element, $('div.snooze-completions'));493this.createCompletionsSnooze(snooze, localize('settings.snooze', "Snooze"), disposables);494}495496// New to Copilot / Signed out497{498const newUser = isNewUser(this.chatEntitlementService);499const disabled = this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted;500const signedOut = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown;501if (newUser || signedOut || disabled) {502addSeparator();503504let descriptionText: string;505if (newUser) {506descriptionText = localize('activateDescription', "Set up Copilot to use AI features.");507} else if (disabled) {508descriptionText = localize('enableDescription', "Enable Copilot to use AI features.");509} else {510descriptionText = localize('signInDescription', "Sign in to use Copilot AI features.");511}512513let buttonLabel: string;514if (newUser) {515buttonLabel = localize('activateCopilotButton', "Set up Copilot");516} else if (disabled) {517buttonLabel = localize('enableCopilotButton', "Enable Copilot");518} else {519buttonLabel = localize('signInToUseCopilotButton', "Sign in to use Copilot");520}521522this.element.appendChild($('div.description', undefined, descriptionText));523524const button = disposables.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate }));525button.label = buttonLabel;526disposables.add(button.onDidClick(() => this.runCommandAndClose('workbench.action.chat.triggerSetup')));527}528}529530return this.element;531}532533private renderHeader(container: HTMLElement, disposables: DisposableStore, label: string, action?: IAction): void {534const header = container.appendChild($('div.header', undefined, label ?? ''));535536if (action) {537const toolbar = disposables.add(new ActionBar(header, { hoverDelegate: nativeHoverDelegate }));538toolbar.push([action], { icon: true, label: false });539}540}541542private renderContributedChatStatusItem(item: ChatStatusEntry): { element: HTMLElement; disposables: DisposableStore } {543const disposables = new DisposableStore();544545const itemElement = $('div.contribution');546547const headerLabel = typeof item.label === 'string' ? item.label : item.label.label;548const headerLink = typeof item.label === 'string' ? undefined : item.label.link;549this.renderHeader(itemElement, disposables, headerLabel, headerLink ? toAction({550id: 'workbench.action.openChatStatusItemLink',551label: localize('learnMore', "Learn More"),552tooltip: localize('learnMore', "Learn More"),553class: ThemeIcon.asClassName(Codicon.linkExternal),554run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(headerLink))),555}) : undefined);556557const itemBody = itemElement.appendChild($('div.body'));558559const description = itemBody.appendChild($('span.description'));560this.renderTextPlus(description, item.description, disposables);561562if (item.detail) {563const detail = itemBody.appendChild($('div.detail-item'));564this.renderTextPlus(detail, item.detail, disposables);565}566567return { element: itemElement, disposables };568}569570private renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void {571for (const node of parseLinkedText(text).nodes) {572if (typeof node === 'string') {573const parts = renderLabelWithIcons(node);574target.append(...parts);575} else {576store.add(new Link(target, node, undefined, this.hoverService, this.openerService));577}578}579}580581private runCommandAndClose(commandOrFn: string | Function): void {582if (typeof commandOrFn === 'function') {583commandOrFn();584} else {585this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: commandOrFn, from: 'chat-status' });586this.commandService.executeCommand(commandOrFn);587}588589this.hoverService.hideHover(true);590}591592private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot) => void {593const quotaValue = $('span.quota-value');594const quotaBit = $('div.quota-bit');595const overageLabel = $('span.overage-label');596597const quotaIndicator = container.appendChild($('div.quota-indicator', undefined,598$('div.quota-label', undefined,599$('span', undefined, label),600quotaValue601),602$('div.quota-bar', undefined,603quotaBit604),605$('div.description', undefined,606overageLabel607)608));609610if (supportsOverage && (this.chatEntitlementService.entitlement === ChatEntitlement.Pro || this.chatEntitlementService.entitlement === ChatEntitlement.ProPlus)) {611const manageOverageButton = disposables.add(new Button(quotaIndicator, { ...defaultButtonStyles, secondary: true, hoverDelegate: nativeHoverDelegate }));612manageOverageButton.label = localize('enableAdditionalUsage', "Manage paid premium requests");613disposables.add(manageOverageButton.onDidClick(() => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageOverageUrl)))));614}615616const update = (quota: IQuotaSnapshot) => {617quotaIndicator.classList.remove('error');618quotaIndicator.classList.remove('warning');619620let usedPercentage: number;621if (quota.unlimited) {622usedPercentage = 0;623} else {624usedPercentage = Math.max(0, 100 - quota.percentRemaining);625}626627if (quota.unlimited) {628quotaValue.textContent = localize('quotaUnlimited', "Included");629} else if (quota.overageCount) {630quotaValue.textContent = localize('quotaDisplayWithOverage', "+{0} requests", this.quotaOverageFormatter.value.format(quota.overageCount));631} else {632quotaValue.textContent = localize('quotaDisplay', "{0}%", this.quotaPercentageFormatter.value.format(usedPercentage));633}634635quotaBit.style.width = `${usedPercentage}%`;636637if (usedPercentage >= 90) {638quotaIndicator.classList.add('error');639} else if (usedPercentage >= 75) {640quotaIndicator.classList.add('warning');641}642643if (supportsOverage) {644if (quota.overageEnabled) {645overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests enabled.");646} else {647overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled.");648}649} else {650overageLabel.textContent = '';651}652};653654update(quota);655656return update;657}658659private createSettings(container: HTMLElement, disposables: DisposableStore): HTMLElement {660const modeId = this.editorService.activeTextEditorLanguageId;661const settings = container.appendChild($('div.settings'));662663// --- Code completions664{665const globalSetting = append(settings, $('div.setting'));666this.createCodeCompletionsSetting(globalSetting, localize('settings.codeCompletions.allFiles', "All files"), '*', disposables);667668if (modeId) {669const languageSetting = append(settings, $('div.setting'));670this.createCodeCompletionsSetting(languageSetting, localize('settings.codeCompletions.language', "{0}", this.languageService.getLanguageName(modeId) ?? modeId), modeId, disposables);671}672}673674// --- Next edit suggestions675{676const setting = append(settings, $('div.setting'));677this.createNextEditSuggestionsSetting(setting, localize('settings.nextEditSuggestions', "Next edit suggestions"), this.getCompletionsSettingAccessor(modeId), disposables);678}679680return settings;681}682683private createSetting(container: HTMLElement, settingIdsToReEvaluate: string[], label: string, accessor: ISettingsAccessor, disposables: DisposableStore): Checkbox {684const checkbox = disposables.add(new Checkbox(label, Boolean(accessor.readSetting()), { ...defaultCheckboxStyles, hoverDelegate: nativeHoverDelegate }));685container.appendChild(checkbox.domNode);686687const settingLabel = append(container, $('span.setting-label', undefined, label));688disposables.add(Gesture.addTarget(settingLabel));689[EventType.CLICK, TouchEventType.Tap].forEach(eventType => {690disposables.add(addDisposableListener(settingLabel, eventType, e => {691if (checkbox?.enabled) {692EventHelper.stop(e, true);693694checkbox.checked = !checkbox.checked;695accessor.writeSetting(checkbox.checked);696checkbox.focus();697}698}));699});700701disposables.add(checkbox.onChange(() => {702accessor.writeSetting(checkbox.checked);703}));704705disposables.add(this.configurationService.onDidChangeConfiguration(e => {706if (settingIdsToReEvaluate.some(id => e.affectsConfiguration(id))) {707checkbox.checked = Boolean(accessor.readSetting());708}709}));710711if (!canUseCopilot(this.chatEntitlementService)) {712container.classList.add('disabled');713checkbox.disable();714checkbox.checked = false;715}716717return checkbox;718}719720private createCodeCompletionsSetting(container: HTMLElement, label: string, modeId: string | undefined, disposables: DisposableStore): void {721this.createSetting(container, [defaultChat.completionsEnablementSetting], label, this.getCompletionsSettingAccessor(modeId), disposables);722}723724private getCompletionsSettingAccessor(modeId = '*'): ISettingsAccessor {725const settingId = defaultChat.completionsEnablementSetting;726727return {728readSetting: () => isCompletionsEnabled(this.configurationService, modeId),729writeSetting: (value: boolean) => {730this.telemetryService.publicLog2<ChatSettingChangedEvent, ChatSettingChangedClassification>('chatStatus.settingChanged', {731settingIdentifier: settingId,732settingMode: modeId,733settingEnablement: value ? 'enabled' : 'disabled'734});735736let result = this.configurationService.getValue<Record<string, boolean>>(settingId);737if (!isObject(result)) {738result = Object.create(null);739}740741return this.configurationService.updateValue(settingId, { ...result, [modeId]: value });742}743};744}745746private createNextEditSuggestionsSetting(container: HTMLElement, label: string, completionsSettingAccessor: ISettingsAccessor, disposables: DisposableStore): void {747const nesSettingId = defaultChat.nextEditSuggestionsSetting;748const completionsSettingId = defaultChat.completionsEnablementSetting;749const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });750751const checkbox = this.createSetting(container, [nesSettingId, completionsSettingId], label, {752readSetting: () => completionsSettingAccessor.readSetting() && this.textResourceConfigurationService.getValue<boolean>(resource, nesSettingId),753writeSetting: (value: boolean) => {754this.telemetryService.publicLog2<ChatSettingChangedEvent, ChatSettingChangedClassification>('chatStatus.settingChanged', {755settingIdentifier: nesSettingId,756settingEnablement: value ? 'enabled' : 'disabled'757});758759return this.textResourceConfigurationService.updateValue(resource, nesSettingId, value);760}761}, disposables);762763// enablement of NES depends on completions setting764// so we have to update our checkbox state accordingly765766if (!completionsSettingAccessor.readSetting()) {767container.classList.add('disabled');768checkbox.disable();769}770771disposables.add(this.configurationService.onDidChangeConfiguration(e => {772if (e.affectsConfiguration(completionsSettingId)) {773if (completionsSettingAccessor.readSetting() && canUseCopilot(this.chatEntitlementService)) {774checkbox.enable();775container.classList.remove('disabled');776} else {777checkbox.disable();778container.classList.add('disabled');779}780}781}));782}783784private createCompletionsSnooze(container: HTMLElement, label: string, disposables: DisposableStore): void {785const isEnabled = () => {786const completionsEnabled = isCompletionsEnabled(this.configurationService);787const completionsEnabledActiveLanguage = isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId);788return completionsEnabled || completionsEnabledActiveLanguage;789};790791const button = disposables.add(new Button(container, { disabled: !isEnabled(), ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate, secondary: true }));792793const timerDisplay = container.appendChild($('span.snooze-label'));794795const actionBar = container.appendChild($('div.snooze-action-bar'));796const toolbar = disposables.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate }));797const cancelAction = toAction({798id: 'workbench.action.cancelSnoozeStatusBarLink',799label: localize('cancelSnooze', "Cancel Snooze"),800run: () => this.inlineCompletionsService.cancelSnooze(),801class: ThemeIcon.asClassName(Codicon.stopCircle)802});803804const update = (isEnabled: boolean) => {805container.classList.toggle('disabled', !isEnabled);806toolbar.clear();807808const timeLeftMs = this.inlineCompletionsService.snoozeTimeLeft;809if (!isEnabled || timeLeftMs <= 0) {810timerDisplay.textContent = localize('completions.snooze5minutesTitle', "Hide completions for 5 min");811timerDisplay.title = '';812button.label = label;813button.setTitle(localize('completions.snooze5minutes', "Hide completions and NES for 5 min"));814return true;815}816817const timeLeftSeconds = Math.ceil(timeLeftMs / 1000);818const minutes = Math.floor(timeLeftSeconds / 60);819const seconds = timeLeftSeconds % 60;820821timerDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds} ${localize('completions.remainingTime', "remaining")}`;822timerDisplay.title = localize('completions.snoozeTimeDescription', "Completions are hidden for the remaining duration");823button.label = localize('completions.plus5min', "+5 min");824button.setTitle(localize('completions.snoozeAdditional5minutes', "Snooze additional 5 min"));825toolbar.push([cancelAction], { icon: true, label: false });826827return false;828};829830// Update every second if there's time remaining831const timerDisposables = disposables.add(new DisposableStore());832function updateIntervalTimer() {833timerDisposables.clear();834const enabled = isEnabled();835836if (update(enabled)) {837return;838}839840timerDisposables.add(disposableWindowInterval(841getWindow(container),842() => update(enabled),8431_000,844));845}846updateIntervalTimer();847848disposables.add(button.onDidClick(() => {849this.inlineCompletionsService.snooze();850update(isEnabled());851}));852853disposables.add(this.configurationService.onDidChangeConfiguration(e => {854if (e.affectsConfiguration(defaultChat.completionsEnablementSetting)) {855button.enabled = isEnabled();856}857updateIntervalTimer();858}));859860disposables.add(this.inlineCompletionsService.onDidChangeIsSnoozing(e => {861updateIntervalTimer();862}));863}864}865866867